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 @@ [![npm publish](https://img.shields.io/npm/dt/rsshub?label=npm%20downloads&logo=npm&style=flat-square)](https://www.npmjs.com/package/rsshub) [![test](https://img.shields.io/github/actions/workflow/status/DIYgod/RSSHub/test.yml?branch=master&label=test&logo=github&style=flat-square)](https://github.com/DIYgod/RSSHub/actions/workflows/test.yml?query=event%3Apush+branch%3Amaster) [![Test coverage](https://img.shields.io/codecov/c/github/DIYgod/RSSHub.svg?style=flat-square&logo=codecov)](https://app.codecov.io/gh/DIYgod/RSSHub/branch/master) +[![Visitors](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2FDIYgod%2FRSSHub&count_bg=%23FF752E&title_bg=%23555555&icon=rss.svg&icon_color=%23FF752E&title=RSS+lovers&edge_flat=true)](https://github.com/DIYgod/RSSHub) -[![Telegram group](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.swo.moe%2Fstats%2Ftelegram%2Frsshub&query=count&color=2CA5E0&label=Telegram%20Group&logo=telegram&cacheSeconds=3600&style=flat-square)](https://t.me/rsshub) [![Telegram channel](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.swo.moe%2Fstats%2Ftelegram%2FawesomeRSSHub&query=count&color=2CA5E0&label=Telegram%20Channel&logo=telegram&cacheSeconds=3600&style=flat-square)](https://t.me/awesomeRSSHub) [![Twitter](https://img.shields.io/badge/any_text-Follow-blue?color=2CA5E0&label=Twitter&logo=twitter&cacheSeconds=3600&style=flat-square)](https://twitter.com/intent/follow?screen_name=_RSSHub) +[![Telegram group](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.swo.moe%2Fstats%2Ftelegram%2Frsshub&query=count&color=2CA5E0&label=Telegram%20Group&logo=telegram&cacheSeconds=3600&style=flat-square)](https://t.me/rsshub) [![Telegram channel](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.swo.moe%2Fstats%2Ftelegram%2FawesomeRSSHub&query=count&color=2CA5E0&label=Telegram%20Channel&logo=telegram&cacheSeconds=3600&style=flat-square)](https://t.me/awesomeRSSHub) [![X (Twitter)](https://img.shields.io/badge/any_text-Follow-blue?color=2CA5E0&label=Twitter&logo=X&cacheSeconds=3600&style=flat-square)](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://opencollective.com/static/images/become_sponsor.svg)](https://docs.rsshub.app/sponsor/) - -### Contributors - -[![](https://opencollective.com/RSSHub/contributors.svg?width=890)](https://github.com/DIYgod/RSSHub/graphs/contributors) - -Logo designer [sheldonrrr](https://dribbble.com/sheldonrrr) - -### Backers - -               +[Documentation](https://docs.rsshub.app) | [Telegram Group](https://t.me/rsshub) | [Telegram Channel](https://t.me/awesomeRSSHub) | [X (Twitter)](https://x.com/intent/follow?screen_name=_RSSHub) ## Related Projects @@ -50,53 +29,32 @@ Logo designer [sheldonrrr](https://dribbble.com/sheldonrrr) - [RSSAid](https://github.com/LeetaoGoooo/RSSAid) | RSSHub Radar for Android platform built with Flutter. - [DocSearch](https://github.com/Fatpandac/DocSearch) | Link RSSHub DocSearch into Raycast -## Join Us +## Contribute We welcome all pull requests. Suggestions and feedback are also welcomed [here](https://github.com/DIYgod/RSSHub/issues). -Refer to [Join Us](https://docs.rsshub.app/joinus/) +Refer to [Quick Start](https://docs.rsshub.app/joinus/) ## Deployment Refer to [Deployment](https://docs.rsshub.app/deploy/) -## Support RSSHub - -Refer to [Support RSSHub](https://docs.rsshub.app/sponsor/) - -RSSHub is open source and completely free under the MIT license. However, just like any other open source project, as the project grows, the hosting, development and maintenance requires funding support. - -You can support RSSHub via donations. - -### Recurring Donation - -Recurring donors will be rewarded via express issue response, or even have your name displayed on our GitHub page and website. - -- Become a Sponser on [GitHub](https://github.com/sponsors/DIYgod) -- Become a Sponser on [Patreon](https://www.patreon.com/DIYgod) -- Become a Sponser on [Open Collective](https://opencollective.com/RSSHub) -- Become a Sponser on [爱发电](https://afdian.net/@diygod) -- Contact us directly: +## Special Thanks -### One-time Donation +
-We accept donations via the following ways: +[![](https://opencollective.com/RSSHub/contributors.svg?width=890)](https://github.com/DIYgod/RSSHub/graphs/contributors) -- [WeChat Pay](https://archive.diygod.me/images/wx.jpg) -- [Alipay](https://archive.diygod.me/images/zfb.jpg) -- [Paypal](https://www.paypal.me/DIYgod) +Logo designer [sheldonrrr](https://dribbble.com/sheldonrrr) -Open source is a very expensive thing. RSSHub would not be possible without the help of these individuals and organizations. +[![](https://raw.githubusercontent.com/DIYgod/sponsors/main/sponsors.simple.svg)](https://github.com/DIYgod/sponsors) -

- - - -

+               +
## Author **RSSHub** © [DIYgod](https://github.com/DIYgod), Released under the [MIT](./LICENSE) License.
Authored and maintained by DIYgod with help from contributors ([list](https://github.com/DIYgod/RSSHub/contributors)). -> Blog [@DIYgod](https://diygod.cc) · GitHub [@DIYgod](https://github.com/DIYgod) · Twitter [@DIYgod](https://twitter.com/DIYgod) · Telegram Channel [@awesomeDIYgod](https://t.me/awesomeDIYgod) +> Blog [@DIYgod](https://diygod.cc) · GitHub [@DIYgod](https://github.com/DIYgod) · X (Twitter) [@DIYgod](https://x.com/DIYgod) · Telegram Channel [@awesomeDIYgod](https://t.me/awesomeDIYgod) diff --git a/docker-compose.yml b/docker-compose.yml index 595dfad97b8435..8b79ddf8085c28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.9' - services: rsshub: # two ways to enable puppeteer: @@ -8,29 +6,45 @@ services: image: diygod/rsshub restart: always ports: - - '1200:1200' + - "1200:1200" environment: NODE_ENV: production CACHE_TYPE: redis - REDIS_URL: 'redis://redis:6379/' - PUPPETEER_WS_ENDPOINT: 'ws://browserless:3000' # marked + REDIS_URL: "redis://redis:6379/" + PUPPETEER_WS_ENDPOINT: "ws://browserless:3000" # marked + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:1200/healthz"] + interval: 30s + timeout: 10s + retries: 3 depends_on: - redis - - browserless # marked + - browserless # marked - browserless: # marked - image: browserless/chrome # marked - restart: always # marked - ulimits: # marked - core: # marked - hard: 0 # marked - soft: 0 # marked + browserless: # marked + image: browserless/chrome # marked + restart: always # marked + ulimits: # marked + core: # marked + hard: 0 # marked + soft: 0 # marked + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/pressure"] + interval: 30s + timeout: 10s + retries: 3 redis: image: redis:alpine restart: always volumes: - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 5s volumes: redis-data: diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000000000..9ac9359745c37c --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,277 @@ +import prettier from 'eslint-plugin-prettier'; +import stylistic from '@stylistic/eslint-plugin'; +import unicorn from 'eslint-plugin-unicorn'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import n from 'eslint-plugin-n'; +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import yamlParser from 'yaml-eslint-parser'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, +}); + +export default [{ + ignores: [ + '**/coverage', + '**/.vscode', + '**/docker-compose.yml', + '!.github', + 'assets/build/radar-rules.js', + 'lib/routes-deprecated', + 'lib/router.js', + '**/babel.config.js', + 'scripts/docker/minify-docker.js', + ], +}, ...compat.extends( + 'eslint:recommended', + 'plugin:prettier/recommended', + 'plugin:yml/recommended', + 'plugin:@typescript-eslint/recommended', +), +n.configs['flat/recommended-script'], +unicorn.configs.recommended, +{ + plugins: { + prettier, + '@stylistic': stylistic, + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.node, + ...globals.browser, + }, + + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + }, + + rules: { + // possible problems + 'array-callback-return': ['error', { + allowImplicit: true, + }], + + 'no-await-in-loop': 'error', + 'no-control-regex': 'off', + 'no-duplicate-imports': 'error', + 'no-prototype-builtins': 'off', + + // suggestions + 'arrow-body-style': 'error', + 'block-scoped-var': 'error', + 'curly': 'error', + 'dot-notation': 'error', + 'eqeqeq': 'error', + + 'default-case': ['warn', { + commentPattern: '^no default$', + }], + + 'default-case-last': 'error', + 'no-console': 'error', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-label': 'error', + + 'no-implicit-coercion': ['error', { + boolean: false, + number: false, + string: false, + disallowTemplateShorthand: true, + }], + + 'no-implicit-globals': 'error', + 'no-labels': 'error', + 'no-multi-str': 'error', + 'no-new-func': 'error', + 'no-restricted-imports': 'error', + + 'no-restricted-syntax': ['warn', { + selector: "CallExpression[callee.property.name='get'][arguments.length=0]", + message: "Please use .toArray() instead.", + }, { + selector: "CallExpression[callee.property.name='toArray'] MemberExpression[object.callee.property.name='map']", + message: "Please use .toArray() before .map().", + }], + + 'no-unneeded-ternary': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'warn', + 'no-useless-rename': 'error', + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-arrow-callback': 'error', + 'prefer-const': 'error', + 'prefer-object-has-own': 'error', + 'no-useless-escape': 'warn', + + 'prefer-regex-literals': ['error', { + disallowRedundantWrapping: true, + }], + + 'require-await': 'error', + + // typescript + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 'off', + + '@typescript-eslint/no-unused-expressions': ['error', { + allowShortCircuit: true, + allowTernary: true, + }], + + // unicorn + 'unicorn/consistent-destructuring': 'warn', + 'unicorn/consistent-function-scoping': 'warn', + 'unicorn/explicit-length-check': 'off', + + 'unicorn/filename-case': ['error', { + case: 'kebabCase', + ignore: [String.raw`.*\.(yaml|yml)$`, String.raw`RequestInProgress\.js$`], + }], + + 'unicorn/new-for-builtins': 'off', + 'unicorn/no-array-callback-reference': 'warn', + 'unicorn/no-array-reduce': 'warn', + 'unicorn/no-await-expression-member': 'off', + 'unicorn/no-empty-file': 'warn', + 'unicorn/no-hex-escape': 'warn', + 'unicorn/no-null': 'off', + 'unicorn/no-object-as-default-parameter': 'warn', + 'unicorn/no-nested-ternary': 'warn', + 'unicorn/no-process-exit': 'off', + 'unicorn/no-useless-switch-case': 'off', + + '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': 'warn', + 'unicorn/prefer-global-this': 'off', + 'unicorn/prefer-logical-operator-over-ternary': 'warn', + 'unicorn/prefer-module': 'off', + 'unicorn/prefer-node-protocol': 'off', + + 'unicorn/prefer-number-properties': ['warn', { + checkInfinity: false, + }], + + 'unicorn/prefer-object-from-entries': 'warn', + 'unicorn/prefer-regexp-test': 'warn', + 'unicorn/prefer-spread': 'warn', + 'unicorn/prefer-string-replace-all': 'warn', + 'unicorn/prefer-string-slice': 'off', + + 'unicorn/prefer-switch': ['warn', { + emptyDefaultCase: 'do-nothing-comment', + }], + + 'unicorn/prefer-top-level-await': 'off', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/switch-case-braces': ['error', 'avoid'], + 'unicorn/text-encoding-identifier-case': 'off', + + // formatting rules + '@stylistic/arrow-parens': 'error', + '@stylistic/arrow-spacing': 'error', + '@stylistic/comma-spacing': 'error', + '@stylistic/comma-style': 'error', + '@stylistic/function-call-spacing': 'error', + '@stylistic/keyword-spacing': 'error', + '@stylistic/linebreak-style': 'error', + + '@stylistic/lines-around-comment': ['error', { + beforeBlockComment: false, + }], + + '@stylistic/no-multiple-empty-lines': 'error', + '@stylistic/no-trailing-spaces': 'error', + '@stylistic/rest-spread-spacing': 'error', + '@stylistic/semi': 'error', + '@stylistic/space-before-blocks': 'error', + '@stylistic/space-in-parens': 'error', + '@stylistic/space-infix-ops': 'error', + '@stylistic/space-unary-ops': 'error', + '@stylistic/spaced-comment': 'error', + + // https://github.com/eslint-community/eslint-plugin-n + // node specific rules + 'n/no-extraneous-require': ['error', { + allowModules: [ + 'puppeteer-extra-plugin-user-preferences', + 'puppeteer-extra-plugin-user-data-dir', + ], + }], + + 'n/no-deprecated-api': 'warn', + 'n/no-missing-import': 'off', + 'n/no-missing-require': 'off', + 'n/no-process-exit': 'off', + 'n/no-unpublished-import': 'off', + + 'n/no-unpublished-require': ['error', { + allowModules: ['tosource'], + }], + + 'prettier/prettier': 'off', + + 'yml/quotes': ['error', { + prefer: 'single', + }], + + 'yml/no-empty-mapping-value': 'off', + }, +}, { + files: ['.puppeteerrc.cjs', 'api/vercel.ts'], + rules: { + '@typescript-eslint/no-require-imports': 'off', + }, +}, { + files: ['**/*.yaml', '**/*.yml'], + + languageOptions: { + parser: yamlParser, + }, + + rules: { + 'lines-around-comment': ['error', { + beforeBlockComment: false, + }], + }, +}]; diff --git a/lib/api/category/one.ts b/lib/api/category/one.ts new file mode 100644 index 00000000000000..06cadeb6504bf4 --- /dev/null +++ b/lib/api/category/one.ts @@ -0,0 +1,81 @@ +import { namespaces } from '@/registry'; +import { z, createRoute, RouteHandler } from '@hono/zod-openapi'; + +const categoryList: Record = {}; + +for (const namespace in namespaces) { + for (const path in namespaces[namespace].routes) { + if (namespaces[namespace].routes[path].categories?.length) { + for (const category of namespaces[namespace].routes[path].categories!) { + if (!categoryList[category]) { + categoryList[category] = {}; + } + if (!categoryList[category][namespace]) { + categoryList[category][namespace] = { + ...namespaces[namespace], + routes: {}, + }; + } + categoryList[category][namespace].routes[path] = namespaces[namespace].routes[path]; + } + } + } +} + +const ParamsSchema = z.object({ + category: z.string().openapi({ + param: { + name: 'category', + in: 'path', + }, + example: 'popular', + }), +}); + +const QuerySchema = z.object({ + categories: z + .string() + .transform((val) => val.split(',')) + .optional(), + lang: z.string().optional(), +}); + +const route = createRoute({ + method: 'get', + path: '/category/{category}', + tags: ['Category'], + request: { + query: QuerySchema, + params: ParamsSchema, + }, + responses: { + 200: { + description: 'Namespace list by categories and language', + }, + }, +}); + +const handler: RouteHandler = (ctx) => { + const { categories, lang } = ctx.req.valid('query'); + const { category } = ctx.req.valid('param'); + + let allCategories = [category]; + if (categories && categories.length > 0) { + allCategories = [...allCategories, ...categories]; + } + + // Get namespaces that exist in all requested categories + const commonNamespaces = Object.keys(categoryList[category] || {}).filter((namespace) => allCategories.every((cat) => categoryList[cat]?.[namespace])); + + // Create result directly from common namespaces + let result = Object.fromEntries(commonNamespaces.map((namespace) => [namespace, categoryList[category][namespace]])); + + // Filter by language if provided + if (lang) { + result = Object.fromEntries(Object.entries(result).filter(([, value]) => value.lang === lang)); + } + + return ctx.json(result); +}; + +export { route, handler }; diff --git a/lib/api/follow/config.ts b/lib/api/follow/config.ts new file mode 100644 index 00000000000000..bb44f94ffb4229 --- /dev/null +++ b/lib/api/follow/config.ts @@ -0,0 +1,27 @@ +import { config } from '@/config'; +import { createRoute, RouteHandler } from '@hono/zod-openapi'; +import { gitHash, gitDate } from '@/utils/git-hash'; + +const route = createRoute({ + method: 'get', + path: '/follow/config', + tags: ['Follow'], + responses: { + 200: { + description: 'Follow config', + }, + }, +}); + +const handler: RouteHandler = (ctx) => + ctx.json({ + ownerUserId: config.follow.ownerUserId, + description: config.follow.description, + price: config.follow.price, + userLimit: config.follow.userLimit, + cacheTime: config.cache.routeExpire, + gitHash, + gitDate: gitDate?.getTime(), + }); + +export { route, handler }; diff --git a/lib/api/index.ts b/lib/api/index.ts index bf6c16694a6fe3..a6eb4629589cf3 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -3,8 +3,10 @@ import { route as namespaceAllRoute, handler as namespaceAllHandler } from '@/ap import { route as namespaceOneRoute, handler as namespaceOneHandler } from '@/api/namespace/one'; import { route as radarRulesAllRoute, handler as radarRulesAllHandler } from '@/api/radar/rules/all'; import { route as radarRulesOneRoute, handler as radarRulesOneHandler } from '@/api/radar/rules/one'; +import { route as categoryOneRoute, handler as categoryOneHandler } from '@/api/category/one'; +import { route as followConfigRoute, handler as followConfigHandler } from '@/api/follow/config'; import { OpenAPIHono } from '@hono/zod-openapi'; -import { swaggerUI } from '@hono/swagger-ui'; +import { apiReference } from '@scalar/hono-api-reference'; const app = new OpenAPIHono(); @@ -12,6 +14,8 @@ app.openapi(namespaceAllRoute, namespaceAllHandler); app.openapi(namespaceOneRoute, namespaceOneHandler); app.openapi(radarRulesAllRoute, radarRulesAllHandler); app.openapi(radarRulesOneRoute, radarRulesOneHandler); +app.openapi(categoryOneRoute, categoryOneHandler); +app.openapi(followConfigRoute, followConfigHandler); const docs = app.getOpenAPI31Document({ openapi: '3.1.0', @@ -24,8 +28,7 @@ for (const path in docs.paths) { docs.paths[`/api${path}`] = docs.paths[path]; delete docs.paths[path]; } -app.get('/docs', (ctx) => ctx.json(docs)); - -app.get('/ui', swaggerUI({ url: '/api/docs' })); +app.get('/openapi.json', (ctx) => ctx.json(docs)); +app.get('/reference', apiReference({ content: docs })); export default app; diff --git a/lib/api/radar/rules/all.ts b/lib/api/radar/rules/all.ts index 372b7823450dc4..7266a8291043b3 100644 --- a/lib/api/radar/rules/all.ts +++ b/lib/api/radar/rules/all.ts @@ -1,13 +1,10 @@ import { namespaces } from '@/registry'; import { parse } from 'tldts'; -import { RadarItem } from '@/types'; +import { RadarDomain } from '@/types'; import { createRoute, RouteHandler } from '@hono/zod-openapi'; const radar: { - [domain: string]: { - _name: string; - [subdomain: string]: RadarItem[] | string; - }; + [domain: string]: RadarDomain; } = {}; for (const namespace in namespaces) { @@ -23,7 +20,7 @@ for (const namespace in namespaces) { if (!radar[domain]) { radar[domain] = { _name: namespaces[namespace].name, - }; + } as RadarDomain; } if (!radar[domain][subdomain]) { radar[domain][subdomain] = []; diff --git a/lib/api/radar/rules/one.ts b/lib/api/radar/rules/one.ts index 8867902f824275..6cf859875bc3d8 100644 --- a/lib/api/radar/rules/one.ts +++ b/lib/api/radar/rules/one.ts @@ -1,13 +1,10 @@ import { namespaces } from '@/registry'; import { parse } from 'tldts'; -import { RadarItem } from '@/types'; +import { RadarDomain } from '@/types'; import { z, createRoute, RouteHandler } from '@hono/zod-openapi'; const radar: { - [domain: string]: { - _name: string; - [subdomain: string]: RadarItem[] | string; - }; + [domain: string]: RadarDomain; } = {}; for (const namespace in namespaces) { @@ -23,7 +20,7 @@ for (const namespace in namespaces) { if (!radar[domain]) { radar[domain] = { _name: namespaces[namespace].name, - }; + } as RadarDomain; } if (!radar[domain][subdomain]) { radar[domain][subdomain] = []; diff --git a/lib/app.tsx b/lib/app.tsx index 65feef3abfc31f..2747dcfd27a3c3 100644 --- a/lib/app.tsx +++ b/lib/app.tsx @@ -12,6 +12,7 @@ import debug from '@/middleware/debug'; import header from '@/middleware/header'; import antiHotlink from '@/middleware/anti-hotlink'; import parameter from '@/middleware/parameter'; +import trace from '@/middleware/trace'; import { jsxRenderer } from 'hono/jsx-renderer'; import { trimTrailingSlash } from 'hono/trailing-slash'; @@ -37,6 +38,7 @@ app.use( }) ); app.use(mLogger); +app.use(trace); app.use(sentry); app.use(accessControl); app.use(debug); diff --git a/lib/config.ts b/lib/config.ts index a7837ebe7497f1..735e9614b36cab 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -1,16 +1,18 @@ -import 'dotenv/config'; import randUserAgent from '@/utils/rand-user-agent'; +import 'dotenv/config'; import { ofetch } from 'ofetch'; let envs = process.env; export type Config = { + // app config disallowRobot: boolean; enableCluster?: string; isPackage: boolean; nodeName?: string; puppeteerWSEndpoint?: string; chromiumExecutablePath?: string; + // network connect: { port: number; }; @@ -20,6 +22,7 @@ export type Config = { ua: string; trueUA: string; allowOrigin?: string; + // cache cache: { type: string; requestTimeout: number; @@ -32,6 +35,7 @@ export type Config = { redis: { url: string; }; + // proxy proxyUri?: string; proxy: { protocol?: string; @@ -39,19 +43,27 @@ export type Config = { port?: string; auth?: string; url_regex: string; + strategy: 'on_retry' | 'all'; }; - proxyStrategy: string; pacUri?: string; pacScript?: string; + // access control accessKey?: string; + // logging debugInfo: string; loggerLevel: string; noLogfiles?: boolean; + otel: { + seconds_bucket?: string; + milliseconds_bucket?: string; + }; showLoggerTimestamp?: boolean; sentry: { dsn?: string; routeTimeout: number; }; + enableRemoteDebugging?: boolean; + // feed config hotlink: { template?: string; includePaths?: string[]; @@ -70,11 +82,22 @@ export type Config = { temperature?: number; maxTokens?: number; endpoint: string; - prompt?: string; + inputOption: string; + promptTitle: string; + promptDescription: string; }; + follow: { + ownerUserId?: string; + description?: string; + price?: number; + userLimit?: number; + }; + + // Route-specific Configurations bilibili: { cookies: Record; dmImgList?: string; + dmImgInter?: string; }; bitbucket: { username?: string; @@ -87,9 +110,15 @@ export type Config = { bupt: { portal_cookie?: string; }; + caixin: { + cookie?: string; + }; civitai: { cookie?: string; }; + dianping: { + cookie?: string; + }; dida365: { username?: string; password?: string; @@ -135,6 +164,10 @@ export type Config = { game4399: { cookie?: string; }; + gelbooru: { + apiKey?: string; + userId?: string; + }; github: { access_token?: string; }; @@ -144,6 +177,9 @@ export type Config = { google: { fontsApiKey?: string; }; + guozaoke: { + cookies?: string; + }; hefeng: { key?: string; }; @@ -154,7 +190,6 @@ export type Config = { username?: string; password?: string; bearertoken?: string; - iap_receipt?: string; }; instagram: { username?: string; @@ -169,12 +204,25 @@ export type Config = { javdb: { session?: string; }; + keylol: { + cookie?: string; + }; lastfm: { api_key?: string; }; lightnovel: { cookie?: string; }; + lorientlejour: { + token?: string; + username?: string; + password?: string; + }; + malaysiakini: { + email?: string; + password?: string; + refreshToken?: string; + }; manhuagui: { cookie?: string; }; @@ -194,6 +242,9 @@ export type Config = { instance?: string; token?: string; }; + misskey: { + accessToken?: string; + }; mox: { cookie: string; }; @@ -214,6 +265,9 @@ export type Config = { notion: { key?: string; }; + patreon: { + sessionId?: string; + }; pianyuan: { cookie?: string; }; @@ -233,6 +287,9 @@ export type Config = { qingting: { id?: string; }; + readwise: { + accessToken?: string; + }; saraba1st: { cookie?: string; }; @@ -245,24 +302,52 @@ export type Config = { scihub: { host?: string; }; + sis001: { + baseUrl?: string; + }; + skeb: { + bearerToken?: string; + }; + sorrycc: { + cookie?: string; + }; spotify: { clientId?: string; clientSecret?: string; refreshToken?: string; }; + sspai: { + bearertoken?: string; + }; telegram: { token?: string; + session?: string; + apiId?: number; + apiHash?: string; + maxConcurrentDownloads?: number; + proxy?: { + host?: string; + port?: number; + secret?: string; + }; }; tophub: { cookie?: string; }; + tsdm39: { + cookie: string; + }; twitter: { - oauthTokens?: string[]; - oauthTokenSecrets?: string[]; - username?: string; - password?: string; - authenticationSecret?: string; - cookie?: string; + username?: string[]; + password?: string[]; + authenticationSecret?: string[]; + phoneOrEmail?: string[]; + authToken?: string[]; + thirdPartyApi?: string; + }; + uestc: { + bbsCookie?: string; + bbsAuthStr?: string; }; weibo: { app_key?: string; @@ -280,12 +365,23 @@ export type Config = { device_id?: string; refresh_token?: string; }; + xiaohongshu: { + cookie?: string; + }; ximalaya: { token?: string; }; + xsijishe: { + cookie?: string; + userAgent?: string; + }; xueqiu: { cookies?: string; }; + yamibo: { + salt?: string; + auth?: string; + }; youtube: { key?: string; clientId?: string; @@ -315,7 +411,7 @@ const toBoolean = (value: string | undefined, defaultValue: boolean) => { } }; -const toInt = (value: string | undefined, defaultValue: number) => (value === undefined ? defaultValue : Number.parseInt(value)); +const toInt = (value: string | undefined, defaultValue?: number) => (value === undefined ? defaultValue : Number.parseInt(value)); const calculateValue = () => { const bilibili_cookies: Record = {}; @@ -360,7 +456,6 @@ const calculateValue = () => { requestTimeout: toInt(envs.REQUEST_TIMEOUT, 30000), // Milliseconds to wait for the server to end the response before aborting the request ua: envs.UA ?? (toBoolean(envs.NO_RANDOM_UA, false) ? TRUE_UA : randUserAgent({ browser: 'chrome', os: 'mac os', device: 'desktop' })), trueUA: TRUE_UA, - // cors request allowOrigin: envs.ALLOW_ORIGIN, // cache cache: { @@ -384,8 +479,8 @@ const calculateValue = () => { port: envs.PROXY_PORT, auth: envs.PROXY_AUTH, url_regex: envs.PROXY_URL_REGEX || '.*', + strategy: envs.PROXY_STRATEGY || 'all', // all / on_retry }, - proxyStrategy: envs.PROXY_STRATEGY || 'all', // all / on_retry pacUri: envs.PAC_URI, pacScript: envs.PAC_SCRIPT, // access control @@ -395,11 +490,16 @@ const calculateValue = () => { debugInfo: envs.DEBUG_INFO || 'true', loggerLevel: envs.LOGGER_LEVEL || 'info', noLogfiles: toBoolean(envs.NO_LOGFILES, false), + otel: { + seconds_bucket: envs.OTEL_SECONDS_BUCKET || '0.01,0.1,1,2,5,15,30,60', + milliseconds_bucket: envs.OTEL_MILLISECONDS_BUCKET || '10,20,50,100,250,500,1000,5000,15000', + }, showLoggerTimestamp: toBoolean(envs.SHOW_LOGGER_TIMESTAMP, false), sentry: { dsn: envs.SENTRY, routeTimeout: toInt(envs.SENTRY_ROUTE_TIMEOUT, 30000), }, + enableRemoteDebugging: toBoolean(envs.ENABLE_REMOTE_DEBUGGING, false), // feed config hotlink: { template: envs.HOTLINK_TEMPLATE, @@ -419,13 +519,22 @@ const calculateValue = () => { temperature: toInt(envs.OPENAI_TEMPERATURE, 0.2), maxTokens: toInt(envs.OPENAI_MAX_TOKENS, 0) || undefined, endpoint: envs.OPENAI_API_ENDPOINT || 'https://api.openai.com/v1', - prompt: envs.OPENAI_PROMPT || 'Please summarize the following article and reply with markdown format.', + inputOption: envs.OPENAI_INPUT_OPTION || 'description', + promptDescription: envs.OPENAI_PROMPT || 'Please summarize the following article and reply with markdown format.', + promptTitle: envs.OPENAI_PROMPT_TITLE || 'Please translate the following title into Simplified Chinese and reply only translated text.', + }, + follow: { + ownerUserId: envs.FOLLOW_OWNER_USER_ID, + description: envs.FOLLOW_DESCRIPTION, + price: toInt(envs.FOLLOW_PRICE), + userLimit: toInt(envs.FOLLOW_USER_LIMIT), }, // Route-specific Configurations bilibili: { cookies: bilibili_cookies, dmImgList: envs.BILIBILI_DM_IMG_LIST, + dmImgInter: envs.BILIBILI_DM_IMG_INTER, }, bitbucket: { username: envs.BITBUCKET_USERNAME, @@ -438,9 +547,15 @@ const calculateValue = () => { bupt: { portal_cookie: envs.BUPT_PORTAL_COOKIE, }, + caixin: { + cookie: envs.CAIXIN_COOKIE, + }, civitai: { cookie: envs.CIVITAI_COOKIE, }, + dianping: { + cookie: envs.DIANPING_COOKIE, + }, dida365: { username: envs.DIDA365_USERNAME, password: envs.DIDA365_PASSWORD, @@ -486,6 +601,10 @@ const calculateValue = () => { game4399: { cookie: envs.GAME_4399, }, + gelbooru: { + apiKey: envs.GELBOORU_API_KEY, + userId: envs.GELBOORU_USER_ID, + }, github: { access_token: envs.GITHUB_ACCESS_TOKEN, }, @@ -495,8 +614,10 @@ const calculateValue = () => { google: { fontsApiKey: envs.GOOGLE_FONTS_API_KEY, }, + guozaoke: { + cookies: envs.GUOZAOKE_COOKIES, + }, hefeng: { - // weather key: envs.HEFENG_KEY, }, infzm: { @@ -506,7 +627,6 @@ const calculateValue = () => { username: envs.INITIUM_USERNAME, password: envs.INITIUM_PASSWORD, bearertoken: envs.INITIUM_BEARER_TOKEN, - iap_receipt: envs.INITIUM_IAP_RECEIPT, }, instagram: { username: envs.IG_USERNAME, @@ -521,12 +641,25 @@ const calculateValue = () => { javdb: { session: envs.JAVDB_SESSION, }, + keylol: { + cookie: envs.KEYLOL_COOKIE, + }, lastfm: { api_key: envs.LASTFM_API_KEY, }, lightnovel: { cookie: envs.SECURITY_KEY, }, + lorientlejour: { + token: envs.LORIENTLEJOUR_TOKEN, + username: envs.LORIENTLEJOUR_USERNAME, + password: envs.LORIENTLEJOUR_PASSWORD, + }, + malaysiakini: { + email: envs.MALAYSIAKINI_EMAIL, + password: envs.MALAYSIAKINI_PASSWORD, + refreshToken: envs.MALAYSIAKINI_REFRESHTOKEN, + }, manhuagui: { cookie: envs.MHGUI_COOKIE, }, @@ -546,6 +679,9 @@ const calculateValue = () => { instance: envs.MINIFLUX_INSTANCE || 'https://reader.miniflux.app', token: envs.MINIFLUX_TOKEN || '', }, + misskey: { + accessToken: envs.MISSKEY_ACCESS_TOKEN, + }, mox: { cookie: envs.MOX_COOKIE, }, @@ -566,6 +702,9 @@ const calculateValue = () => { notion: { key: envs.NOTION_TOKEN, }, + patreon: { + sessionId: envs.PATREON_SESSION_ID, + }, pianyuan: { cookie: envs.PIANYUAN_COOKIE, }, @@ -585,6 +724,9 @@ const calculateValue = () => { qingting: { id: envs.QINGTING_ID, }, + readwise: { + accessToken: envs.READWISE_ACCESS_TOKEN, + }, saraba1st: { cookie: envs.SARABA1ST_COOKIE, }, @@ -597,28 +739,52 @@ const calculateValue = () => { scihub: { host: envs.SCIHUB_HOST || 'https://sci-hub.se/', }, + sis001: { + baseUrl: envs.SIS001_BASE_URL || 'https://sis001.com', + }, + skeb: { + bearerToken: envs.SKEB_BEARER_TOKEN, + }, + sorrycc: { + cookie: envs.SORRYCC_COOKIES, + }, spotify: { clientId: envs.SPOTIFY_CLIENT_ID, clientSecret: envs.SPOTIFY_CLIENT_SECRET, refreshToken: envs.SPOTIFY_REFRESHTOKEN, }, + sspai: { + bearertoken: envs.SSPAI_BEARERTOKEN, + }, telegram: { token: envs.TELEGRAM_TOKEN, session: envs.TELEGRAM_SESSION, apiId: envs.TELEGRAM_API_ID, apiHash: envs.TELEGRAM_API_HASH, maxConcurrentDownloads: envs.TELEGRAM_MAX_CONCURRENT_DOWNLOADS, + proxy: { + host: envs.TELEGRAM_PROXY_HOST, + port: envs.TELEGRAM_PROXY_PORT, + secret: envs.TELEGRAM_PROXY_SECRET, + }, }, tophub: { cookie: envs.TOPHUB_COOKIE, }, + tsdm39: { + cookie: envs.TSDM39_COOKIES, + }, twitter: { - oauthTokens: envs.TWITTER_OAUTH_TOKEN?.split(','), - oauthTokenSecrets: envs.TWITTER_OAUTH_TOKEN_SECRET?.split(','), - username: envs.TWITTER_USERNAME, - password: envs.TWITTER_PASSWORD, - authenticationSecret: envs.TWITTER_AUTHENTICATION_SECRET, - cookie: envs.TWITTER_COOKIE, + username: envs.TWITTER_USERNAME?.split(','), + password: envs.TWITTER_PASSWORD?.split(','), + authenticationSecret: envs.TWITTER_AUTHENTICATION_SECRET?.split(','), + phoneOrEmail: envs.TWITTER_PHONE_OR_EMAIL?.split(','), + authToken: envs.TWITTER_AUTH_TOKEN?.split(','), + thirdPartyApi: envs.TWITTER_THIRD_PARTY_API, + }, + uestc: { + bbsCookie: envs.UESTC_BBS_COOKIE, + bbsAuthStr: envs.UESTC_BBS_AUTH_STR, }, weibo: { app_key: envs.WEIBO_APP_KEY, @@ -636,12 +802,23 @@ const calculateValue = () => { device_id: envs.XIAOYUZHOU_ID, refresh_token: envs.XIAOYUZHOU_TOKEN, }, + xiaohongshu: { + cookie: envs.XIAOHONGSHU_COOKIE, + }, ximalaya: { token: envs.XIMALAYA_TOKEN, }, + xsijishe: { + cookie: envs.XSIJISHE_COOKIE, + user_agent: envs.XSIJISHE_USER_AGENT, + }, xueqiu: { cookies: envs.XUEQIU_COOKIES, }, + yamibo: { + salt: envs.YAMIBO_SALT, + auth: envs.YAMIBO_AUTH, + }, youtube: { key: envs.YOUTUBE_KEY, clientId: envs.YOUTUBE_CLIENT_ID, diff --git a/lib/errors/index.test.ts b/lib/errors/index.test.ts index 2538b7a8876b09..e60fa114ac8328 100644 --- a/lib/errors/index.test.ts +++ b/lib/errors/index.test.ts @@ -22,13 +22,17 @@ describe('httperror', () => { it(`httperror`, async () => { const response = await request.get('/test/httperror'); expect(response.status).toBe(503); - expect(response.text).toMatch('FetchError: [GET] "https://httpbingo.org/status/404": 404 Not Found'); + expect(response.text).toContain('FetchError: [GET] "https://httpbingo.org/status/404": 404 Not Found'); }, 20000); }); describe('RequestInProgressError', () => { - it(`RequestInProgressError`, async () => { + it(`RequestInProgressError with retry`, async () => { const responses = await Promise.all([request.get('/test/slow'), request.get('/test/slow')]); + expect(new Set(responses.map((r) => r.status))).toEqual(new Set([200, 200])); + }); + it(`RequestInProgressError`, async () => { + const responses = await Promise.all([request.get('/test/slow4'), request.get('/test/slow4')]); expect(new Set(responses.map((r) => r.status))).toEqual(new Set([200, 503])); expect(new Set(responses.map((r) => r.headers['cache-control']))).toEqual(new Set([`public, max-age=${config.cache.routeExpire}`, `public, max-age=${config.requestTimeout / 1000}`])); expect(responses.filter((r) => r.text.includes('RequestInProgressError: This path is currently fetching, please come back later!'))).toHaveLength(1); @@ -63,19 +67,19 @@ describe('route throws an error', () => { const value = $(item).find('.debug-value').html()?.trim(); switch (key) { case 'Request Amount:': - expect(value).toBe('9'); + expect(value).toBe('11'); break; case 'Hot Routes:': - expect(value).toBe('6 /test/:id/:params?
'); + expect(value).toBe('8 /test/:id/:params?
'); break; case 'Hot Paths:': - expect(value).toBe('2 /test/error
2 /test/slow
1 /test/httperror
1 /test/config-not-found-error
1 /test/invalid-parameter-error
1 /thisDoesNotExist
1 /
'); + expect(value).toBe('2 /test/error
2 /test/slow
2 /test/slow4
1 /test/httperror
1 /test/config-not-found-error
1 /test/invalid-parameter-error
1 /thisDoesNotExist
1 /
'); break; case 'Hot Error Routes:': expect(value).toBe('5 /test/:id/:params?
'); break; case 'Hot Error Paths:': - expect(value).toBe('2 /test/error
1 /test/httperror
1 /test/slow
1 /test/config-not-found-error
1 /test/invalid-parameter-error
1 /thisDoesNotExist
'); + expect(value).toBe('2 /test/error
1 /test/httperror
1 /test/slow4
1 /test/config-not-found-error
1 /test/invalid-parameter-error
1 /thisDoesNotExist
'); break; default: } diff --git a/lib/errors/index.tsx b/lib/errors/index.tsx index 56ba079e455fc3..0ac4027939b322 100644 --- a/lib/errors/index.tsx +++ b/lib/errors/index.tsx @@ -1,20 +1,26 @@ import { type NotFoundHandler, type ErrorHandler } from 'hono'; import { getDebugInfo, setDebugInfo } from '@/utils/debug-info'; import { config } from '@/config'; -import Sentry from '@sentry/node'; +import * as Sentry from '@sentry/node'; import logger from '@/utils/logger'; import Error from '@/views/error'; import NotFoundError from './types/not-found'; +import { requestMetric } from '@/utils/otel'; + export const errorHandler: ErrorHandler = (error, ctx) => { const requestPath = ctx.req.path; const matchedRoute = ctx.req.routePath; const hasMatchedRoute = matchedRoute !== '/*'; const debug = getDebugInfo(); - if (ctx.res.headers.get('RSSHub-Cache-Status')) { - debug.hitCache++; + try { + if (ctx.res.headers.get('RSSHub-Cache-Status')) { + debug.hitCache++; + } + } catch { + // ignore } debug.error++; @@ -61,8 +67,9 @@ export const errorHandler: ErrorHandler = (error, ctx) => { const message = `${error.name}: ${errorMessage}`; logger.error(`Error in ${requestPath}: ${message}`); + requestMetric.error({ path: matchedRoute, method: ctx.req.method, status: ctx.res.status }); - return config.isPackage + return config.isPackage || ctx.req.query('format') === 'json' ? ctx.json({ error: { message: error.message ?? error, diff --git a/lib/index.ts b/lib/index.ts index 24e7a247c3d3ac..61fbec2f9311f2 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -10,13 +10,19 @@ const hostIPList = getLocalhostAddress(); logger.info(`🎉 RSSHub is running on port ${port}! Cheers!`); logger.info('💖 Can you help keep this open source project alive? Please sponsor 👉 https://docs.rsshub.app/sponsor'); logger.info(`🔗 Local: 👉 http://localhost:${port}`); -for (const ip of hostIPList) { - logger.info(`🔗 Network: 👉 http://${ip}:${port}`); +if (config.listenInaddrAny) { + for (const ip of hostIPList) { + logger.info(`🔗 Network: 👉 http://${ip}:${port}`); + } } const server = serve({ fetch: app.fetch, + hostname: config.listenInaddrAny ? '::' : '127.0.0.1', port, + serverOptions: { + maxHeaderSize: 1024 * 32, + }, }); export default server; diff --git a/lib/middleware/access-control.ts b/lib/middleware/access-control.ts index 2ba1b237c764fd..41123b1f84527a 100644 --- a/lib/middleware/access-control.ts +++ b/lib/middleware/access-control.ts @@ -3,12 +3,12 @@ import { config } from '@/config'; import md5 from '@/utils/md5'; import RejectError from '@/errors/types/reject'; -const reject = () => { - throw new RejectError('Authentication failed. Access denied.'); +const reject = (requestPath) => { + throw new RejectError(`Authentication failed. Access denied.\n${requestPath}`); }; const middleware: MiddlewareHandler = async (ctx, next) => { - const requestPath = ctx.req.path; + const requestPath = new URL(ctx.req.url).pathname; const accessKey = ctx.req.query('key'); const accessCode = ctx.req.query('code'); @@ -16,7 +16,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => { await next(); } else { if (config.accessKey && !(config.accessKey === accessKey || accessCode === md5(requestPath + config.accessKey))) { - return reject(); + return reject(requestPath); } await next(); } diff --git a/lib/middleware/anti-hotlink.test.ts b/lib/middleware/anti-hotlink.test.ts index 4987ceb3f974b5..70c30f1343b2ea 100644 --- a/lib/middleware/anti-hotlink.test.ts +++ b/lib/middleware/anti-hotlink.test.ts @@ -36,7 +36,7 @@ const expects = { ` `, ], - desc: ' - Made with love by RSSHub(https://github.com/DIYgod/RSSHub)', + desc: ' - Powered by RSSHub', }, processed: { items: [ @@ -54,7 +54,7 @@ const expects = { ` `, ], - desc: ' - Made with love by RSSHub(https://github.com/DIYgod/RSSHub)', + desc: ' - Powered by RSSHub', }, urlencoded: { items: [ @@ -72,7 +72,7 @@ const expects = { ` `, ], - desc: ' - Made with love by RSSHub(https://github.com/DIYgod/RSSHub)', + desc: ' - Powered by RSSHub', }, }, multimedia: { @@ -87,7 +87,7 @@ const expects = { `, ], - desc: ' - Made with love by RSSHub(https://github.com/DIYgod/RSSHub)', + desc: ' - Powered by RSSHub', }, relayed: { items: [ @@ -100,7 +100,7 @@ const expects = { `, ], - desc: ' - Made with love by RSSHub(https://github.com/DIYgod/RSSHub)', + desc: ' - Powered by RSSHub', }, partlyRelayed: { items: [ @@ -113,7 +113,149 @@ const expects = { `, ], - desc: ' - Made with love by RSSHub(https://github.com/DIYgod/RSSHub)', + desc: ' - Powered by RSSHub', + }, + }, + extraComplicated: { + origin: { + items: [ + { + content: + '\n\n\n\n\n\n\n\n\n\n', + itunes: {}, + }, + { + content: '\n', + itunes: {}, + }, + { + content: + '\n\n', + enclosure: { + url: 'https://mock.com/DIYgod/RSSHub.png', + type: 'image/png', + }, + itunes: { + image: 'https://mock.com/DIYgod/RSSHub.gif', + }, + }, + ], + image: { + link: 'https://github.com/DIYgod/RSSHub', + url: 'https://mock.com/DIYgod/RSSHub.png', + title: 'Test complicated', + }, + description: ' - Powered by RSSHub', + }, + processed: { + items: [ + { + content: + '\n\n\n\n\n\n\n\n\n\n', + itunes: {}, + }, + { + content: '\n', + itunes: {}, + }, + { + content: + '\n\n', + enclosure: { + url: 'https://i3.wp.com/mock.com/DIYgod/RSSHub.png', + type: 'image/png', + }, + itunes: { + image: 'https://i3.wp.com/mock.com/DIYgod/RSSHub.gif', + }, + }, + ], + image: { + link: 'https://github.com/DIYgod/RSSHub', + url: 'https://i3.wp.com/mock.com/DIYgod/RSSHub.png', + title: 'Test complicated', + }, + description: ' - Powered by RSSHub', + }, + urlencoded: { + items: [ + { + content: + '\n\n\n\n\n\n\n\n\n\n', + itunes: {}, + }, + { + content: '\n', + itunes: {}, + }, + { + content: + '\n\n', + enclosure: { + url: 'https://images.weserv.nl?url=https%3A%2F%2Fmock.com%2FDIYgod%2FRSSHub.png', + type: 'image/png', + }, + itunes: { + image: 'https://images.weserv.nl?url=https%3A%2F%2Fmock.com%2FDIYgod%2FRSSHub.gif', + }, + }, + ], + image: { + link: 'https://github.com/DIYgod/RSSHub', + url: 'https://images.weserv.nl?url=https%3A%2F%2Fmock.com%2FDIYgod%2FRSSHub.png', + title: 'Test complicated', + }, + description: ' - Powered by RSSHub', + }, + }, + extraMultimedia: { + origin: { + items: [ + { + content: + '\n\n\n\n', + }, + { + content: '\n', + enclosure: { + url: 'https://mock.com/DIYgod/RSSHub.mp4', + type: 'video/mp4', + }, + }, + ], + description: ' - Powered by RSSHub', + }, + relayed: { + items: [ + { + content: + '\n\n\n\n', + }, + { + content: '\n', + enclosure: { + url: 'https://i3.wp.com/mock.com/DIYgod/RSSHub.mp4', + type: 'video/mp4', + }, + }, + ], + description: ' - Powered by RSSHub', + }, + partlyRelayed: { + items: [ + { + content: + '\n\n\n\n', + }, + { + content: '\n', + enclosure: { + url: 'https://i3.wp.com/mock.com/DIYgod/RSSHub.mp4', + type: 'video/mp4', + }, + }, + ], + description: ' - Powered by RSSHub', }, }, }; @@ -142,12 +284,55 @@ const testAntiHotlink = async (path, expectObj, query?: string | Record) => testAntiHotlink('/test/complicated', expects.complicated.origin, query); -const expectImgProcessed = (query?: string | Record) => testAntiHotlink('/test/complicated', expects.complicated.processed, query); -const expectImgUrlencoded = (query?: string | Record) => testAntiHotlink('/test/complicated', expects.complicated.urlencoded, query); -const expectMultimediaOrigin = (query?: string | Record) => testAntiHotlink('/test/multimedia', expects.multimedia.origin, query); -const expectMultimediaRelayed = (query?: string | Record) => testAntiHotlink('/test/multimedia', expects.multimedia.relayed, query); -const expectMultimediaPartlyRelayed = (query?: string | Record) => testAntiHotlink('/test/multimedia', expects.multimedia.partlyRelayed, query); +const testAntiHotlinkExtra = async (path, expectObj, query?: string | Record) => { + const app = (await import('@/app')).default; + + path += query ? `?${new URLSearchParams(query).toString()}` : ''; + + const response = await app.request(path); + const parsed = await parser.parseString(await response.text()); + const obj = { + description: parsed.description, + image: parsed.image, + items: parsed.items.slice(0, expectObj.items.length).map((e) => ({ + content: e.content, + enclosure: e.enclosure, + itunes: e.itunes, + })), + }; + expect(obj).toEqual(expectObj); + + return parsed; +}; + +const expectImgOrigin = async (query?: string | Record) => { + await testAntiHotlink('/test/complicated', expects.complicated.origin, query); + await testAntiHotlinkExtra('/test/complicated', expects.extraComplicated.origin, query); +}; +const expectImgProcessed = async (query?: string | Record) => { + await testAntiHotlink('/test/complicated', expects.complicated.processed, query); + await testAntiHotlinkExtra('/test/complicated', expects.extraComplicated.processed, query); +}; + +const expectImgUrlencoded = async (query?: string | Record) => { + await testAntiHotlink('/test/complicated', expects.complicated.urlencoded, query); + await testAntiHotlinkExtra('/test/complicated', expects.extraComplicated.urlencoded, query); +}; + +const expectMultimediaOrigin = async (query?: string | Record) => { + await testAntiHotlink('/test/multimedia', expects.multimedia.origin, query); + await testAntiHotlinkExtra('/test/multimedia', expects.extraMultimedia.origin, query); +}; + +const expectMultimediaRelayed = async (query?: string | Record) => { + await testAntiHotlink('/test/multimedia', expects.multimedia.relayed, query); + await testAntiHotlinkExtra('/test/multimedia', expects.extraMultimedia.relayed, query); +}; + +const expectMultimediaPartlyRelayed = async (query?: string | Record) => { + await testAntiHotlink('/test/multimedia', expects.multimedia.partlyRelayed, query); + await testAntiHotlinkExtra('/test/multimedia', expects.extraMultimedia.partlyRelayed, query); +}; describe('anti-hotlink', () => { it('template-legacy', async () => { diff --git a/lib/middleware/anti-hotlink.ts b/lib/middleware/anti-hotlink.ts index dd8b289d5564f0..6b04cda304c769 100644 --- a/lib/middleware/anti-hotlink.ts +++ b/lib/middleware/anti-hotlink.ts @@ -17,7 +17,7 @@ const matchPath = (path: string, paths: string[]) => { return false; }; -// return ture if the path needs to be processed +// return true if the path needs to be processed const filterPath = (path: string) => { const include = config.hotlink.includePaths; const exclude = config.hotlink.excludePaths; @@ -44,6 +44,18 @@ const parseUrl = (str: string) => { return url; }; + +const replaceUrl = (template?: string, url?: string) => { + if (!template || !url) { + return url; + } + const oldUrl = parseUrl(url); + if (oldUrl && oldUrl.protocol !== 'data:') { + return interpolate(template, oldUrl); + } + return url; +}; + const replaceUrls = ($: CheerioAPI, selector: string, template: string, attribute = 'src') => { $(selector).each(function () { const oldSrc = $(this).attr(attribute); @@ -105,6 +117,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => { // Force config hotlink template on conflict if (config.hotlink.template) { imageHotlinkTemplate = filterPath(ctx.req.path) ? config.hotlink.template : undefined; + multimediaHotlinkTemplate = filterPath(ctx.req.path) ? config.hotlink.template : undefined; } if (!imageHotlinkTemplate && !multimediaHotlinkTemplate) { @@ -120,6 +133,9 @@ const middleware: MiddlewareHandler = async (ctx, next) => { // image link const data: Data = ctx.get('data'); if (data) { + if (data.image) { + data.image = replaceUrl(imageHotlinkTemplate, data.image); + } if (data.description) { data.description = process(data.description, imageHotlinkTemplate, multimediaHotlinkTemplate); } @@ -129,6 +145,19 @@ const middleware: MiddlewareHandler = async (ctx, next) => { if (item.description) { item.description = process(item.description, imageHotlinkTemplate, multimediaHotlinkTemplate); } + if (item.enclosure_url && item.enclosure_type) { + if (item.enclosure_type.startsWith('image/')) { + item.enclosure_url = replaceUrl(imageHotlinkTemplate, item.enclosure_url); + } else if (/^(video|audio)\//.test(item.enclosure_type)) { + item.enclosure_url = replaceUrl(multimediaHotlinkTemplate, item.enclosure_url); + } + } + if (item.image) { + item.image = replaceUrl(imageHotlinkTemplate, item.image); + } + if (item.itunes_item_image) { + item.itunes_item_image = replaceUrl(imageHotlinkTemplate, item.itunes_item_image); + } } } diff --git a/lib/middleware/cache.ts b/lib/middleware/cache.ts index 5ceee99eadfd54..d31a26a5b0b13b 100644 --- a/lib/middleware/cache.ts +++ b/lib/middleware/cache.ts @@ -6,23 +6,40 @@ import RequestInProgressError from '@/errors/types/request-in-progress'; import cacheModule from '@/utils/cache/index'; import { Data } from '@/types'; +const bypassList = new Set(['/', '/robots.txt', '/logo.png', '/favicon.ico']); // only give cache string, as the `!` condition tricky -// md5 is used to shrink key size +// XXH64 is used to shrink key size // plz, write these tips in comments! const middleware: MiddlewareHandler = async (ctx, next) => { - const { h64ToString } = await xxhash(); - const key = 'rsshub:koa-redis-cache:' + h64ToString(ctx.req.path); - const controlKey = 'rsshub:path-requested:' + h64ToString(ctx.req.path); - - if (!cacheModule.status.available) { + if (!cacheModule.status.available || bypassList.has(ctx.req.path)) { await next(); return; } + const requestPath = ctx.req.path; + const limit = ctx.req.query('limit') ? `:${ctx.req.query('limit')}` : ''; + const { h64ToString } = await xxhash(); + const key = 'rsshub:koa-redis-cache:' + h64ToString(requestPath + limit); + const controlKey = 'rsshub:path-requested:' + h64ToString(requestPath + limit); + const isRequesting = await cacheModule.globalCache.get(controlKey); if (isRequesting === '1') { - throw new RequestInProgressError('This path is currently fetching, please come back later!'); + let retryTimes = process.env.NODE_ENV === 'test' ? 1 : 10; + let bypass = false; + while (retryTimes > 0) { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => setTimeout(resolve, process.env.NODE_ENV === 'test' ? 3000 : 6000)); + // eslint-disable-next-line no-await-in-loop + if ((await cacheModule.globalCache.get(controlKey)) !== '1') { + bypass = true; + break; + } + retryTimes--; + } + if (!bypass) { + throw new RequestInProgressError('This path is currently fetching, please come back later!'); + } } const value = await cacheModule.globalCache.get(key); diff --git a/lib/middleware/logger.ts b/lib/middleware/logger.ts index f806e7ee06170e..9d2ca59cec6b8f 100644 --- a/lib/middleware/logger.ts +++ b/lib/middleware/logger.ts @@ -1,3 +1,4 @@ +import { requestMetric } from '@/utils/otel'; import { MiddlewareHandler } from 'hono'; import logger from '@/utils/logger'; import { getPath, time } from '@/utils/helpers'; @@ -25,7 +26,7 @@ const colorStatus = (status: number) => { }; const middleware: MiddlewareHandler = async (ctx, next) => { - const { method, raw } = ctx.req; + const { method, raw, routePath } = ctx.req; const path = getPath(raw); logger.info(`${LogPrefix.Incoming} ${method} ${path}`); @@ -34,7 +35,10 @@ const middleware: MiddlewareHandler = async (ctx, next) => { await next(); - logger.info(`${LogPrefix.Outgoing} ${method} ${path} ${colorStatus(ctx.res.status)} ${time(start)}`); + const status = ctx.res.status; + + logger.info(`${LogPrefix.Outgoing} ${method} ${path} ${colorStatus(status)} ${time(start)}`); + requestMetric.success(Date.now() - start, { path: routePath, method, status }); }; export default middleware; diff --git a/lib/middleware/parameter.test.ts b/lib/middleware/parameter.test.ts index 4166157238ef63..57debada33ca16 100644 --- a/lib/middleware/parameter.test.ts +++ b/lib/middleware/parameter.test.ts @@ -427,7 +427,8 @@ describe('multi parameter', () => { }); describe('openai', () => { - it(`chatgpt`, async () => { + it('processes both title and description', async () => { + config.openai.inputOption = 'both'; const responseWithGpt = await app.request('/test/gpt?chatgpt=true'); const responseNormal = await app.request('/test/gpt'); @@ -437,9 +438,25 @@ describe('openai', () => { const parsedGpt = await parser.parseString(await responseWithGpt.text()); const parsedNormal = await parser.parseString(await responseNormal.text()); - expect(parsedGpt.items[0].content).not.toBe(undefined); - expect(parsedGpt.items[0].content).toBe(parsedNormal.items[0].content); - expect(parsedGpt.items[1].content).not.toBe(undefined); - expect(parsedGpt.items[1].content).not.toBe(parsedNormal.items[1].content); + expect(parsedGpt.items[0].title).not.toBe(parsedNormal.items[0].title); + expect(parsedGpt.items[0].title).toContain('AI processed content.'); + expect(parsedGpt.items[0].content).not.toBe(parsedNormal.items[0].content); + expect(parsedGpt.items[0].content).toContain('AI processed content.'); + }); + + it('processes title or description', async () => { + // test title + config.openai.inputOption = 'title'; + const responseTitleOnly = await app.request('/test/gpt?chatgpt=true'); + const parsedTitleOnly = await parser.parseString(await responseTitleOnly.text()); + expect(parsedTitleOnly.items[0].title).toContain('AI processed content.'); + expect(parsedTitleOnly.items[0].content).not.toContain('AI processed content.'); + + // test description + config.openai.inputOption = 'description'; + const responseDescriptionOnly = await app.request('/test/gpt?chatgpt=true'); + const parsedDescriptionOnly = await parser.parseString(await responseDescriptionOnly.text()); + expect(parsedDescriptionOnly.items[0].title).not.toContain('AI processed content.'); + expect(parsedDescriptionOnly.items[0].content).toContain('AI processed content.'); }); }); diff --git a/lib/middleware/parameter.ts b/lib/middleware/parameter.ts index 843f9bc1f931b9..2b84f3e32191d1 100644 --- a/lib/middleware/parameter.ts +++ b/lib/middleware/parameter.ts @@ -32,7 +32,7 @@ const resolveRelativeLink = ($: CheerioAPI, elem: Element, attr: string, baseUrl } }; -const summarizeArticle = async (articleText: string) => { +const getAiCompletion = async (prompt: string, text: string) => { const apiUrl = `${config.openai.endpoint}/chat/completions`; const response = await ofetch(apiUrl, { method: 'POST', @@ -40,8 +40,8 @@ const summarizeArticle = async (articleText: string) => { model: config.openai.model, max_tokens: config.openai.maxTokens, messages: [ - { role: 'system', content: config.openai.prompt }, - { role: 'user', content: articleText }, + { role: 'system', content: prompt }, + { role: 'user', content: text }, ], temperature: config.openai.temperature, }, @@ -327,23 +327,53 @@ const middleware: MiddlewareHandler = async (ctx, next) => { if (ctx.req.query('chatgpt') && config.openai.apiKey) { data.item = await Promise.all( data.item.map(async (item) => { - if (item.description) { - try { - const summary = await cache.tryGet(`openai:${item.link}`, async () => { - const text = convert(item.description!); - if (text.length < 300) { - return ''; - } - const summary_md = await summarizeArticle(text); - return md.render(summary_md); + try { + // handle description + if (config.openai.inputOption === 'description' && item.description) { + const description = await cache.tryGet(`openai:description:${item.link}`, async () => { + const description = convert(item.description!); + const descriptionMd = await getAiCompletion(config.openai.promptDescription, description); + return md.render(descriptionMd); }); - // 将总结结果添加到文章数据中 - if (summary !== '') { - item.description = summary + '

' + item.description; + // add it to the description + if (description !== '') { + item.description = description + '

' + item.description; + } + } + // handle title + else if (config.openai.inputOption === 'title' && item.title) { + const title = await cache.tryGet(`openai:title:${item.link}`, async () => { + const title = convert(item.title!); + return await getAiCompletion(config.openai.promptTitle, title); + }); + // replace the title + if (title !== '') { + item.title = title + ''; + } + } + // handle both + else if (config.openai.inputOption === 'both' && item.title && item.description) { + const title = await cache.tryGet(`openai:title:${item.link}`, async () => { + const title = convert(item.title!); + return await getAiCompletion(config.openai.promptTitle, title); + }); + // replace the title + if (title !== '') { + item.title = title + ''; + } + + const description = await cache.tryGet(`openai:description:${item.link}`, async () => { + const description = convert(item.description!); + const descriptionMd = await getAiCompletion(config.openai.promptDescription, description); + return md.render(descriptionMd); + }); + // add it to the description + if (description !== '') { + item.description = description + '

' + item.description; } - } catch { - // when openai failed, return default description and not write cache } + } catch { + // when openai failed, return default content and not write cache } return item; }) diff --git a/lib/middleware/template.test.ts b/lib/middleware/template.test.ts index 2a03c5ddd77a12..da61f0ffec20e4 100644 --- a/lib/middleware/template.test.ts +++ b/lib/middleware/template.test.ts @@ -108,4 +108,10 @@ describe('template', () => { expect(parsed.items[0].enclosure?.length).toBe('3661'); expect(parsed.items[0].itunes.duration).toBe('10:10:10'); }); + + it(`redirect`, async () => { + const response = await app.request('/test/redirect'); + expect(response.status).toBe(301); + expect(response.headers.get('location')).toBe('/test/1'); + }); }); diff --git a/lib/middleware/template.tsx b/lib/middleware/template.tsx index 4262b9716e4bb4..6bcf3fd639502b 100644 --- a/lib/middleware/template.tsx +++ b/lib/middleware/template.tsx @@ -4,14 +4,14 @@ import { collapseWhitespace, convertDateToISO8601 } from '@/utils/common-utils'; import type { MiddlewareHandler } from 'hono'; import { Data } from '@/types'; -// Set RSS (minute) according to the availability of cache -// * available: max(config.cache.routeExpire / 60, 1) -// * unavailable: 1 -// The minimum is limited to 1 minute to prevent potential misuse import cacheModule from '@/utils/cache/index'; -const ttl = (cacheModule.status.available && Math.trunc(config.cache.routeExpire / 60)) || 1; const middleware: MiddlewareHandler = async (ctx, next) => { + // Set RSS (minute) according to the availability of cache + // * available: max(config.cache.routeExpire / 60, 1) + // * unavailable: 1 + // The minimum is limited to 1 minute to prevent potential misuse + const ttl = (cacheModule.status.available && Math.trunc(config.cache.routeExpire / 60)) || 1; await next(); const data: Data = ctx.get('data'); @@ -53,7 +53,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => { // https://stackoverflow.com/questions/1497885/remove-control-characters-from-php-string/1497928#1497928 // remove unicode control characters // see #14940 #14943 #15262 - item.description = item.description.replaceAll(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F]/g, ''); + item.description = item.description.replaceAll(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F\u200B\uFFFF]/g, ''); } if (typeof item.author === 'string') { @@ -74,8 +74,16 @@ const middleware: MiddlewareHandler = async (ctx, next) => { } if (outputType !== 'rss') { - item.pubDate && (item.pubDate = convertDateToISO8601(item.pubDate) || ''); - item.updated && (item.updated = convertDateToISO8601(item.updated) || ''); + try { + item.pubDate && (item.pubDate = convertDateToISO8601(item.pubDate) || ''); + } catch { + item.pubDate = ''; + } + try { + item.updated && (item.updated = convertDateToISO8601(item.updated) || ''); + } catch { + item.updated = ''; + } } } } @@ -94,18 +102,24 @@ const middleware: MiddlewareHandler = async (ctx, next) => { return ctx.json(result); } - // retain .ums for backward compatibility - if (outputType === 'ums' || outputType === 'rss3') { - return ctx.json(rss3(result)); - } else if (outputType === 'json') { - ctx.header('Content-Type', 'application/feed+json; charset=UTF-8'); - return ctx.body(json(result)); + if (ctx.get('redirect')) { + return ctx.redirect(ctx.get('redirect'), 301); } else if (ctx.get('no-content')) { return ctx.body(null); - } else if (outputType === 'atom') { - return ctx.render(); } else { - return ctx.render(); + // retain .ums for backward compatibility + switch (outputType) { + case 'ums': + case 'rss3': + return ctx.json(rss3(result)); + case 'json': + ctx.header('Content-Type', 'application/feed+json; charset=UTF-8'); + return ctx.body(json(result)); + case 'atom': + return ctx.render(); + default: + return ctx.render(); + } } }; diff --git a/lib/middleware/trace.ts b/lib/middleware/trace.ts new file mode 100644 index 00000000000000..78fa81e4491f14 --- /dev/null +++ b/lib/middleware/trace.ts @@ -0,0 +1,25 @@ +import { MiddlewareHandler } from 'hono'; +import { getPath } from '@/utils/helpers'; +import { config } from '@/config'; +import { tracer } from '@/utils/otel'; + +const middleware: MiddlewareHandler = async (ctx, next) => { + if (config.debugInfo) { + // Only enable tracing in debug mode + const { method, raw } = ctx.req; + const path = getPath(raw); + + const span = tracer.startSpan(`${method} ${path}`, { + kind: 1, // server + attributes: {}, + }); + span.addEvent('invoking handleRequest'); + await next(); + span.end(); + } else { + // Skip + await next(); + } +}; + +export default middleware; diff --git a/lib/registry.test.ts b/lib/registry.test.ts index d168eb59906f99..a2a2eb8fcdd96a 100644 --- a/lib/registry.test.ts +++ b/lib/registry.test.ts @@ -29,4 +29,12 @@ describe('registry', () => { const response = await app.request('/favicon.ico'); expect(response.status).toBe(200); }); + + // healthz + it('/healthz', async () => { + const response = await app.request('/healthz'); + expect(response.status).toBe(200); + expect(response.headers.get('cache-control')).toBe('no-cache'); + expect(await response.text()).toBe('ok'); + }); }); diff --git a/lib/registry.ts b/lib/registry.ts index e74012efbf8bad..3a8724d2add379 100644 --- a/lib/registry.ts +++ b/lib/registry.ts @@ -4,9 +4,12 @@ import { Hono, type Handler } from 'hono'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { serveStatic } from '@hono/node-server/serve-static'; +import { config } from '@/config'; import index from '@/routes/index'; +import healthz from '@/routes/healthz'; import robotstxt from '@/routes/robots.txt'; +import metrics from '@/routes/metrics'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -81,24 +84,65 @@ if (Object.keys(modules).length) { export { namespaces }; const app = new Hono(); +const sortRoutes = ( + routes: Record< + string, + Route & { + location: string; + } + > +) => + Object.entries(routes).sort(([pathA], [pathB]) => { + const segmentsA = pathA.split('/'); + const segmentsB = pathB.split('/'); + const lenA = segmentsA.length; + const lenB = segmentsB.length; + const minLen = Math.min(lenA, lenB); + + for (let i = 0; i < minLen; i++) { + const segmentA = segmentsA[i]; + const segmentB = segmentsB[i]; + + // Literal segments have priority over parameter segments + if (segmentA.startsWith(':') !== segmentB.startsWith(':')) { + return segmentA.startsWith(':') ? 1 : -1; + } + } + + return 0; + }); + for (const namespace in namespaces) { const subApp = app.basePath(`/${namespace}`); - for (const path in namespaces[namespace].routes) { - const wrapedHandler: Handler = async (ctx) => { + + const namespaceData = namespaces[namespace]; + if (!namespaceData || !namespaceData.routes) { + continue; + } + + const sortedRoutes = sortRoutes(namespaceData.routes); + + for (const [path, routeData] of sortedRoutes) { + const wrappedHandler: Handler = async (ctx) => { if (!ctx.get('data')) { - if (typeof namespaces[namespace].routes[path].handler !== 'function') { - const { route } = await import(`./routes/${namespace}/${namespaces[namespace].routes[path].location}`); - namespaces[namespace].routes[path].handler = route.handler; + if (typeof routeData.handler !== 'function') { + const { route } = await import(`./routes/${namespace}/${routeData.location}`); + routeData.handler = route.handler; } - ctx.set('data', await namespaces[namespace].routes[path].handler(ctx)); + ctx.set('data', await routeData.handler(ctx)); } }; - subApp.get(path, wrapedHandler); + subApp.get(path, wrappedHandler); } } app.get('/', index); +app.get('/healthz', healthz); app.get('/robots.txt', robotstxt); +if (config.debugInfo) { + // Only enable tracing in debug mode + app.get('/metrics', metrics); +} app.use( '/*', serveStatic({ diff --git a/lib/router.js b/lib/router.js index 36c0aa9fbe5954..9ab78428ede701 100644 --- a/lib/router.js +++ b/lib/router.js @@ -47,14 +47,6 @@ router.get('/huya/live/:id', lazyloadRouteHandler('./routes/huya/live')); // f-droid // router.get('/fdroid/apprelease/:app', lazyloadRouteHandler('./routes/fdroid/apprelease')); -// konachan -router.get('/konachan/post/popular_recent', lazyloadRouteHandler('./routes/konachan/post-popular-recent')); -router.get('/konachan.com/post/popular_recent', lazyloadRouteHandler('./routes/konachan/post-popular-recent')); -router.get('/konachan.net/post/popular_recent', lazyloadRouteHandler('./routes/konachan/post-popular-recent')); -router.get('/konachan/post/popular_recent/:period', lazyloadRouteHandler('./routes/konachan/post-popular-recent')); -router.get('/konachan.com/post/popular_recent/:period', lazyloadRouteHandler('./routes/konachan/post-popular-recent')); -router.get('/konachan.net/post/popular_recent/:period', lazyloadRouteHandler('./routes/konachan/post-popular-recent')); - // PornHub // router.get('/pornhub/category/:caty', lazyloadRouteHandler('./routes/pornhub/category')); // router.get('/pornhub/search/:keyword', lazyloadRouteHandler('./routes/pornhub/search')); @@ -66,9 +58,6 @@ router.get('/konachan.net/post/popular_recent/:period', lazyloadRouteHandler('./ // EZTV router.get('/eztv/torrents/:imdb_id', lazyloadRouteHandler('./routes/eztv/imdb')); -// 新京报 -router.get('/bjnews/:cat', lazyloadRouteHandler('./routes/bjnews/news')); - // 米哈游 router.get('/mihoyo/bh3/:type', lazyloadRouteHandler('./routes/mihoyo/bh3')); router.get('/mihoyo/bh2/:type', lazyloadRouteHandler('./routes/mihoyo/bh2')); @@ -305,7 +294,7 @@ router.get('/fzu_min/:type', lazyloadRouteHandler('./routes/universities/fzu/new router.get('/xmu/aero/:type', lazyloadRouteHandler('./routes/universities/xmu/aero')); // ifanr -router.get('/ifanr/:channel?', lazyloadRouteHandler('./routes/ifanr/index')); +// router.get('/ifanr/:channel?', lazyloadRouteHandler('./routes/ifanr/index')); // IPSW.me // router.get('/ipsw/index/:ptype/:pname', lazyloadRouteHandler('./routes/ipsw/index')); @@ -364,7 +353,7 @@ router.get('/ltaaa/:category?', lazyloadRouteHandler('./routes/ltaaa/index')); router.get('/autotrader/:query', lazyloadRouteHandler('./routes/autotrader')); // 极客公园 -router.get('/geekpark/breakingnews', lazyloadRouteHandler('./routes/geekpark/breakingnews')); +// router.get('/geekpark/breakingnews', lazyloadRouteHandler('./routes/geekpark/breakingnews')); // 搜狗 // router.get('/sogou/doodles', lazyloadRouteHandler('./routes/sogou/doodles')); @@ -594,17 +583,11 @@ router.get('/zucc/cssearch/latest/:webVpn/:key', lazyloadRouteHandler('./routes/ // checkee router.get('/checkee/:dispdate', lazyloadRouteHandler('./routes/checkee/index')); -// Matters -router.get('/matters/latest/:type?', lazyloadRouteHandler('./routes/matters/latest')); -router.redirect('/matters/hot', '/matters/latest/heat'); // Deprecated -router.get('/matters/tags/:tid', lazyloadRouteHandler('./routes/matters/tags')); -router.get('/matters/author/:uid', lazyloadRouteHandler('./routes/matters/author')); - // 古诗文网 router.get('/gushiwen/recommend/:annotation?', lazyloadRouteHandler('./routes/gushiwen/recommend')); // 21财经 -router.get('/21caijing/channel/:name', lazyloadRouteHandler('./routes/21caijing/channel')); +// router.get('/21caijing/channel/:name', lazyloadRouteHandler('./routes/21caijing/channel')); // 北京邮电大学 router.get('/bupt/yz/:type', lazyloadRouteHandler('./routes/universities/bupt/yz')); @@ -710,9 +693,6 @@ router.get('/huodongxing/explore', lazyloadRouteHandler('./routes/hdx/explore')) // LWN.net Alerts router.get('/lwn/alerts/:distributor', lazyloadRouteHandler('./routes/lwn/alerts')); -// 英雄联盟 -router.get('/lol/newsindex/:type', lazyloadRouteHandler('./routes/lol/newsindex')); - // 掌上英雄联盟 router.get('/lolapp/recommend', lazyloadRouteHandler('./routes/lolapp/recommend')); router.get('/lolapp/article/:uuid', lazyloadRouteHandler('./routes/lolapp/article')); @@ -891,9 +871,6 @@ router.get('/guet/xwzx/:type?', lazyloadRouteHandler('./routes/guet/news')); // はてな匿名ダイアリー router.get('/hatena/anonymous_diary/archive', lazyloadRouteHandler('./routes/hatena/anonymous_diary/archive')); -// IEEE Xplore [Sci Journal] -router.get('/ieee/author/:aid/:sortType/:count?', lazyloadRouteHandler('./routes/ieee/author')); - // PNAS [Sci Journal] // router.get('/pnas/:topic?', lazyloadRouteHandler('./routes/pnas/index')); @@ -1472,9 +1449,6 @@ router.get('/deepl/blog/:lang?', lazyloadRouteHandler('./routes/deepl/blog')); router.get('/muchong/journal/:type?', lazyloadRouteHandler('./routes/muchong/journal')); router.get('/muchong/:id/:type?/:sort?', lazyloadRouteHandler('./routes/muchong/index')); -// 求是网 -router.get('/qstheory/:category?', lazyloadRouteHandler('./routes/qstheory/index')); - // 生命时报 router.get('/lifetimes/:category?', lazyloadRouteHandler('./routes/lifetimes/index')); diff --git a/lib/routes-deprecated/21caijing/channel.js b/lib/routes-deprecated/21caijing/channel.js deleted file mode 100644 index 2d8694f36e35bc..00000000000000 --- a/lib/routes-deprecated/21caijing/channel.js +++ /dev/null @@ -1,76 +0,0 @@ -const got = require('@/utils/got'); -const { parseRelativeDate } = require('@/utils/parse-date'); -const timezone = require('@/utils/timezone'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - const channel = ctx.params.name; - const pageUrl = `https://m.21jingji.com/channel/${channel}`; - - const response = await got({ - method: 'get', - url: pageUrl, - }); - - const $ = cheerio.load(response.data); - const channelName = $(`div.nav a[href='/channel/${channel}']`).text(); - const list = $('div.news_list>a') - .slice(0, 5) - .map((i, e) => $(e).attr('href')) - .get(); - - const out = await Promise.all( - list.map(async (link) => { - const cache = await ctx.cache.get(link); - if (cache) { - return JSON.parse(cache); - } - - const response = await got({ - method: 'get', - url: link, - headers: { - Referer: pageUrl, - }, - }); - - const $ = cheerio.load(response.data); - - const div = $('div.editor'); - if (div) { - div.remove(); - } - - const title = $('h1').text(); - const dateStr = $('div.newsDate').text(); - - $('div.txtContent img').each((index, element) => { - const $el = $(element); - const img_original_data = $el.attr('data-original'); - const img_src = $el.attr('src'); - if (img_src === undefined && img_original_data) { - $el.attr('src', img_original_data); - } - }); - - const content = $('div.txtContent').html(); - - const single = { - pubDate: timezone(parseRelativeDate(dateStr), +8), - link, - title, - description: content, - }; - - ctx.cache.set(link, JSON.stringify(single)); - return single; - }) - ); - - ctx.state.data = { - title: '21财经-' + channelName, - link: pageUrl, - description: '21财经-' + channelName, - item: out, - }; -}; diff --git a/lib/routes-deprecated/anime1/anime.js b/lib/routes-deprecated/anime1/anime.js deleted file mode 100644 index 214f10ca2dd2ee..00000000000000 --- a/lib/routes-deprecated/anime1/anime.js +++ /dev/null @@ -1,25 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - const { time, name } = ctx.params; - const $ = await got.get(`https://anime1.me/category/${encodeURIComponent(time)}/${encodeURIComponent(name)}`).then((r) => cheerio.load(r.data)); - const title = $('.page-title').text().trim(); - ctx.state.data = { - title, - link: `https://anime1.me/category/${time}/${name}`, - description: title, - item: $('article') - .toArray() - .map((art) => { - const $el = $(art); - const title = $el.find('.entry-title a').text(); - return { - title: $el.find('.entry-title a').text(), - link: $el.find('.entry-title a').attr('href'), - description: title, - pubDate: new Date($el.find('time').attr('datetime')).toUTCString(), - }; - }), - }; -}; diff --git a/lib/routes-deprecated/anime1/search.js b/lib/routes-deprecated/anime1/search.js deleted file mode 100644 index 6b88f8b9db5429..00000000000000 --- a/lib/routes-deprecated/anime1/search.js +++ /dev/null @@ -1,25 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - const { keyword } = ctx.params; - const $ = await got.get(`https://anime1.me/?s=${encodeURIComponent(keyword)}`).then((r) => cheerio.load(r.data)); - const title = $('.page-title').text().trim(); - ctx.state.data = { - title, - link: `https://anime1.me/?s=${keyword}`, - description: title, - item: $('article:has(.cat-links)') - .toArray() - .map((art) => { - const $el = $(art); - const title = $el.find('.entry-title a').text(); - return { - title: $el.find('.entry-title a').text(), - link: $el.find('.entry-title a').attr('href'), - description: title, - pubDate: new Date($el.find('time').attr('datetime')).toUTCString(), - }; - }), - }; -}; diff --git a/lib/routes-deprecated/bjnews/news.js b/lib/routes-deprecated/bjnews/news.js deleted file mode 100644 index 46035014d11d7d..00000000000000 --- a/lib/routes-deprecated/bjnews/news.js +++ /dev/null @@ -1,44 +0,0 @@ -const cheerio = require('cheerio'); -const { parseRelativeDate } = require('@/utils/parse-date'); -const timezone = require('@/utils/timezone'); -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - const url = `http://www.bjnews.com.cn/${ctx.params.cat}`; - const res = await got.get(url); - const $ = cheerio.load(res.data); - const list = $('#waterfall-container .pin_demo > a').get(); - - const out = await Promise.all( - list.map(async (item) => { - const $ = cheerio.load(item); - - const title = $('.pin_tit').text(); - const itemUrl = $('a').attr('href'); - const cache = await ctx.cache.get(itemUrl); - if (cache) { - return JSON.parse(cache); - } - - const responses = await got.get(itemUrl); - const $d = cheerio.load(responses.data); - $d('img').each((i, e) => $(e).attr('referrerpolicy', 'no-referrer')); - - const single = { - title, - pubDate: timezone(parseRelativeDate($d('.left-info .timer').text()), +8), - author: $d('.left-info .reporter').text(), - link: itemUrl, - guid: itemUrl, - description: $d('#contentStr').html(), - }; - ctx.cache.set(itemUrl, JSON.stringify(single)); - return single; - }) - ); - ctx.state.data = { - title: $('title').text(), - link: url, - item: out, - }; -}; diff --git a/lib/routes-deprecated/chuapp/index.js b/lib/routes-deprecated/chuapp/index.js deleted file mode 100644 index a7871ffe64c299..00000000000000 --- a/lib/routes-deprecated/chuapp/index.js +++ /dev/null @@ -1,54 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - const category = ctx.params.category || 'night'; - - const options = { - daily: { - title: '每日聚焦', - suffix: '/category/daily', - }, - pcz: { - title: '最好玩', - suffix: '/category/pcz', - }, - night: { - title: '触乐夜话', - suffix: '/tag/index/id/20369.html', - }, - news: { - title: '动态资讯', - suffix: '/category/zsyx', - }, - }; - - const response = await got.get(`https://www.chuapp.com${options[category].suffix}`); - const $ = cheerio.load(response.data); - - const articles = $('a.fn-clear') - .map((index, ele) => ({ - title: $(ele).attr('title'), - link: $(ele).attr('href'), - })) - .get(); - - const item = await Promise.all( - articles.map((item) => - ctx.cache.tryGet(item.link, async () => { - const res = await got.get(`http://www.chuapp.com${item.link}`); - const s = cheerio.load(res.data); - item.description = s('.content .the-content').html(); - item.pubDate = new Date(s('.friendly_time').attr('data-time')); - item.author = s('.author-time .fn-left').text(); - return item; - }) - ) - ); - - ctx.state.data = { - title: `触乐 - ${options[category].title}`, - link: `https://www.chuapp.com${options[category].suffix}`, - item, - }; -}; diff --git a/lib/routes-deprecated/dykszx/news.js b/lib/routes-deprecated/dykszx/news.js deleted file mode 100644 index 1d7feedd18a06c..00000000000000 --- a/lib/routes-deprecated/dykszx/news.js +++ /dev/null @@ -1,60 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); -const { parseDate } = require('@/utils/parse-date'); -const timezone = require('@/utils/timezone'); -const host = 'https://www.dykszx.com'; - -// disable ssl check -const options = { https: { rejectUnauthorized: false } }; - -const getContent = (href, caches) => { - const newsPage = `${host}${href}`; - return caches.tryGet(newsPage, async () => { - const response = await got.get(newsPage, options); - const data = response.data; - const $ = cheerio.load(data); - const newsTime = $('body > div:nth-child(3) > div.page.w > div.shuxing.w') - .text() - .trim() - .match(/时间:(.*?)点击/g)[0]; - // 移除二维码 - $('.sjlook').remove(); - const content = $('#show-body').html(); - return { newsTime, content, newsPage }; - }); -}; - -const newsTypeObj = { - all: { selector: '#nrs > li > b', name: '新闻中心' }, - gwy: { selector: 'body > div:nth-child(3) > div:nth-child(8) > ul > li', name: '公务员考试' }, - sydw: { selector: 'body > div:nth-child(3) > div:nth-child(9) > ul > li', name: '事业单位考试' }, - zyzc: { selector: 'body > div:nth-child(3) > div:nth-child(10) > ul > li', name: '执(职)业资格、职称考试' }, - other: { selector: 'body > div:nth-child(3) > div:nth-child(11) > ul > li', name: '其他考试' }, -}; - -module.exports = async (ctx) => { - const newsType = ctx.params.type || 'all'; - const response = await got(host, options); - const data = response.data; - const $ = cheerio.load(data); - const newsList = $(newsTypeObj[newsType].selector).toArray(); - - const newsDetail = await Promise.all( - newsList.map(async (item) => { - const href = item.children[0].attribs.href; - const newsContent = await getContent(href, ctx.cache); - return { - title: item.children[0].children[0].data, - description: newsContent.content, - link: newsContent.newsPage, - pubDate: timezone(parseDate(newsContent.newsTime, '时间:YYYY-MM-DD HH:mm:ss'), +8), - }; - }) - ); - ctx.state.data = { - title: `德阳人事考试网 - ${newsTypeObj[newsType].name}`, - link: host, - description: '德阳人事考试网 考试新闻发布', - item: newsDetail, - }; -}; diff --git a/lib/routes-deprecated/dytt/index.js b/lib/routes-deprecated/dytt/index.js deleted file mode 100644 index 8266c061942d9a..00000000000000 --- a/lib/routes-deprecated/dytt/index.js +++ /dev/null @@ -1,60 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); -const iconv = require('iconv-lite'); -const baseURL = 'https://www.ygdy8.net/index.html'; -async function load(link, ctx) { - const cache = await ctx.cache.get(link); - if (cache) { - return cache; - } - const response = await got.get(link, { - responseType: 'buffer', - }); - response.data = iconv.decode(response.data, 'gb2312'); - - const $ = cheerio.load(response.data); - - const description = $('div#Zoom').html(); - ctx.cache.set(link, description); - return description; -} - -module.exports = async (ctx) => { - const response = await got.get(baseURL, { - responseType: 'buffer', - }); - response.data = iconv.decode(response.data, 'gb2312'); - - const $ = cheerio.load(response.data); - const list = $('.co_content8 table tr').get(); - // 页面含有2个.co_content8 table - // 仅第一个table内第一个tr元素是广告连接 - // 去除该广告连接 - list.splice(0, 1); - // const list = $('.co_content8 table tr:not(:first-child)').get(); - const process = await Promise.all( - list.slice(0, 20).map(async (item) => { - const link = $(item).find('a:nth-of-type(2)'); - const itemUrl = 'https://www.ygdy8.net' + link.attr('href'); - const other = await load(itemUrl, ctx); - - return { - enclosure_url: String(other.match(/magnet:.*?(?=">)/)), - enclosure_type: 'application/x-bittorrent', - title: link.text(), - description: other, - pubDate: new Date($(item).find('font').text()).toUTCString(), - link: itemUrl, - }; - }) - ); - - const data = { - title: '电影天堂/阳光电影', - link: baseURL, - description: '电影天堂RSS', - item: process, - }; - - ctx.state.data = data; -}; diff --git a/lib/routes-deprecated/furaffinity/browse.js b/lib/routes-deprecated/furaffinity/browse.js deleted file mode 100644 index 446498b8cdc406..00000000000000 --- a/lib/routes-deprecated/furaffinity/browse.js +++ /dev/null @@ -1,44 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - // 传入参数 - const nsfw = String(ctx.params.nsfw); - - // 判断传入的参数nsfw - let url = 'https://faexport.spangle.org.uk/browse.json?sfw=1'; - if (nsfw === '1') { - url = 'https://faexport.spangle.org.uk/browse.json'; - } - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - const data = response.data; - - ctx.state.data = { - // 源标题 - title: `Fur Affinity Browse`, - // 源链接 - link: `https://www.furaffinity.net/browse/`, - // 源说明 - description: `Fur Affinity Browse`, - - // 遍历此前获取的数据 - item: data.map((item) => ({ - // 标题 - title: item.title, - // 正文 - description: ``, - // 链接 - link: item.link, - // 作者 - author: item.name, - // 由于源API未提供日期,故无pubDate - })), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/commissions.js b/lib/routes-deprecated/furaffinity/commissions.js deleted file mode 100644 index c2a21eecc88029..00000000000000 --- a/lib/routes-deprecated/furaffinity/commissions.js +++ /dev/null @@ -1,39 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - // 传入参数 - const username = String(ctx.params.username); - - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/user/${username}/commissions.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - const data = response.data; - - ctx.state.data = { - // 源标题 - title: `${username}'s Commissions`, - // 源链接 - link: `https://www.furaffinity.net/commissions/${username}/`, - // 源说明 - description: `Fur Affinity ${username}'s Commissions`, - - // 遍历此前获取的数据 - item: data.map((item) => ({ - // 标题 - title: item.title, - // 正文 - description: `${item.description}
`, - // 链接 - link: item.submission.link, - // 作者 - author: username, - // 由于源API未提供日期,故无pubDate - })), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/favorites.js b/lib/routes-deprecated/furaffinity/favorites.js deleted file mode 100644 index 0219886e5e7fb2..00000000000000 --- a/lib/routes-deprecated/furaffinity/favorites.js +++ /dev/null @@ -1,56 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - // 传入参数 - const nsfw = String(ctx.params.nsfw); - const username = String(ctx.params.username); - - // 添加参数username以及判断传入的参数nsfw - let url = `https://faexport.spangle.org.uk/user/${username}/favorites.rss?sfw=1`; - if (nsfw === '1') { - url = `https://faexport.spangle.org.uk/user/${username}/favorites.rss`; - } - - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - // 使用 cheerio 加载返回的 HTML - - const data = response.data; - const $ = cheerio.load(data, { - xmlMode: true, - }); - - const list = $('item'); - - ctx.state.data = { - // 源标题 - title: `${username}'s Favorites`, - // 源链接 - link: `https://www.furaffinity.net/favorites/${username}/`, - // 源说明 - description: `Fur Affinity ${username}'s Favorites`, - - // 遍历此前获取的数据 - item: - list && - list - .map((index, item) => { - item = $(item); - return { - title: item.find('title').text(), - description: item.find('description').text(), - link: item.find('link').text(), - pubDate: new Date(item.find('pubDate').text()).toUTCString(), - }; - }) - .get(), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/gallery.js b/lib/routes-deprecated/furaffinity/gallery.js deleted file mode 100644 index 57cc86f2ddb980..00000000000000 --- a/lib/routes-deprecated/furaffinity/gallery.js +++ /dev/null @@ -1,56 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - // 传入参数 - const nsfw = String(ctx.params.nsfw); - const username = String(ctx.params.username); - - // 添加参数username以及判断传入的参数nsfw - let url = `https://faexport.spangle.org.uk/user/${username}/gallery.rss?sfw=1`; - if (nsfw === '1') { - url = `https://faexport.spangle.org.uk/user/${username}/gallery.rss`; - } - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - // 使用 cheerio 加载返回的 HTML - - const data = response.data; - const $ = cheerio.load(data, { - xmlMode: true, - }); - - const list = $('item'); - - ctx.state.data = { - // 源标题 - title: `${username}'s Gallery`, - // 源链接 - link: `https://www.furaffinity.net/gallery/${username}/`, - // 源说明 - description: `Fur Affinity ${username}'s Gallery`, - - // 遍历此前获取的数据 - item: - list && - list - .map((index, item) => { - item = $(item); - return { - title: item.find('title').text(), - description: item.find('description').text(), - link: item.find('link').text(), - pubDate: new Date(item.find('pubDate').text()).toUTCString(), - author: username, - }; - }) - .get(), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/home.js b/lib/routes-deprecated/furaffinity/home.js deleted file mode 100644 index 86745240bd9897..00000000000000 --- a/lib/routes-deprecated/furaffinity/home.js +++ /dev/null @@ -1,72 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - // 传入参数 - const type = String(ctx.params.type); - const nsfw = String(ctx.params.nsfw); - - // 判断传入的参数nsfw - let url = 'https://faexport.spangle.org.uk/home.json?sfw=1'; - if (nsfw === '1' || type === '1') { - url = 'https://faexport.spangle.org.uk/home.json'; - } - - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - let data = response.data; - - // 判断传入的参数type,分别为:artwork、crafts、music、writing - switch (type) { - case 'artwork': - data = data.artwork; - - break; - - case 'crafts': - data = data.crafts; - - break; - - case 'music': - data = data.music; - - break; - - case 'writing': - data = data.writing; - - break; - - default: - data = data.artwork; - } - - ctx.state.data = { - // 源标题 - title: `Fur Affinity Home`, - // 源链接 - link: `https://www.furaffinity.net/`, - // 源说明 - description: `Fur Affinity Home`, - - // 遍历此前获取的数据 - item: data.map((item) => ({ - // 标题 - title: item.title, - // 正文 - description: ``, - // 链接 - link: item.link, - // 作者 - author: item.name, - // 由于源API未提供日期,故无pubDate - })), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/journal-comments.js b/lib/routes-deprecated/furaffinity/journal-comments.js deleted file mode 100644 index 3a86d239e42e14..00000000000000 --- a/lib/routes-deprecated/furaffinity/journal-comments.js +++ /dev/null @@ -1,50 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - // 传入参数 - const id = String(ctx.params.id); - - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/journal/${id}/comments.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - // 发起第二个 HTTP GET 请求,用于获取该日记的标题 - const response2 = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/journal/${id}.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - const data = response.data; - const data2 = response2.data; - - ctx.state.data = { - // 源标题 - title: `${data2.title} - Journal Comments`, - // 源链接 - link: `https://www.furaffinity.net/journal/${id}/`, - // 源说明 - description: `Fur Affinity ${data2.title} - Journal Comments`, - - // 遍历此前获取的数据 - item: data.map((item) => ({ - // 标题 - title: item.text, - // 正文 - description: `
${item.name}: ${item.text}`, - // 链接 - link: `https://www.furaffinity.net/journal/${id}/`, - // 作者 - author: item.name, - // 日期 - pubDate: new Date(item.posted_at).toUTCString(), - })), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/journals.js b/lib/routes-deprecated/furaffinity/journals.js deleted file mode 100644 index 74862de0f87b1b..00000000000000 --- a/lib/routes-deprecated/furaffinity/journals.js +++ /dev/null @@ -1,49 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - // 传入参数 - const username = String(ctx.params.username); - - // 添加参数username 和 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/user/${username}/journals.rss`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - // 使用 cheerio 加载返回的 HTML - const data = response.data; - const $ = cheerio.load(data, { - xmlMode: true, - }); - - const list = $('item'); - - ctx.state.data = { - // 源标题 - title: `${username}'s Journals`, - // 源链接 - link: `https://www.furaffinity.net/journals/${username}/`, - // 源说明 - description: `Fur Affinity ${username}'s Journals`, - - // 遍历此前获取的数据 - item: - list && - list - .map((index, item) => { - item = $(item); - return { - title: item.find('title').text(), - description: item.find('description').text(), - link: item.find('link').text(), - pubDate: new Date(item.find('pubDate').text()).toUTCString(), - author: username, - }; - }) - .get(), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/scraps.js b/lib/routes-deprecated/furaffinity/scraps.js deleted file mode 100644 index 0acb53fcd35875..00000000000000 --- a/lib/routes-deprecated/furaffinity/scraps.js +++ /dev/null @@ -1,56 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - // 传入参数 - const nsfw = String(ctx.params.nsfw); - const username = String(ctx.params.username); - - // 添加参数username以及判断传入的参数nsfw - let url = `https://faexport.spangle.org.uk/user/${username}/scraps.rss?sfw=1`; - if (nsfw === '1') { - url = `https://faexport.spangle.org.uk/user/${username}/scraps.rss`; - } - - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - // 使用 cheerio 加载返回的 HTML - const data = response.data; - const $ = cheerio.load(data, { - xmlMode: true, - }); - - const list = $('item'); - - ctx.state.data = { - // 源标题 - title: `${username}'s Scraps`, - // 源链接 - link: `https://www.furaffinity.net/scraps/${username}/`, - // 源说明 - description: `Fur Affinity ${username}'s Scraps`, - - // 遍历此前获取的数据 - item: - list && - list - .map((index, item) => { - item = $(item); - return { - title: item.find('title').text(), - description: item.find('description').text(), - link: item.find('link').text(), - pubDate: new Date(item.find('pubDate').text()).toUTCString(), - author: username, - }; - }) - .get(), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/search.js b/lib/routes-deprecated/furaffinity/search.js deleted file mode 100644 index 90066bd324a6a0..00000000000000 --- a/lib/routes-deprecated/furaffinity/search.js +++ /dev/null @@ -1,56 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - // 传入参数 - const nsfw = String(ctx.params.nsfw); - const keyword = String(ctx.params.keyword); - - // 添加参数keyword以及判断传入的参数nsfw - let url = `https://faexport.spangle.org.uk/search.rss?q=${keyword}&sfw=1`; - if (nsfw === '1') { - url = `https://faexport.spangle.org.uk/search.rss?q=${keyword}`; - } - - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - // 使用 cheerio 加载返回的 HTML - const data = response.data; - const $ = cheerio.load(data, { - xmlMode: true, - }); - - const list = $('item'); - - ctx.state.data = { - // 源标题 - title: `FA Search for ${keyword}`, - // 源链接 - link: `https://www.furaffinity.net/search/?q=${keyword}`, - // 源说明 - description: `Fur Affinity Search for ${keyword}`, - - // 遍历此前获取的数据 - item: - list && - list - .map((index, item) => { - item = $(item); - return { - title: item.find('title').text(), - description: item.find('description').text(), - link: item.find('link').text(), - pubDate: new Date(item.find('pubDate').text()).toUTCString(), - // 由于源API未提供作者信息,故无author - }; - }) - .get(), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/shouts.js b/lib/routes-deprecated/furaffinity/shouts.js deleted file mode 100644 index ca3685c029ebc7..00000000000000 --- a/lib/routes-deprecated/furaffinity/shouts.js +++ /dev/null @@ -1,40 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - // 传入参数 - const username = String(ctx.params.username); - - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/user/${username}/shouts.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - const data = response.data; - - ctx.state.data = { - // 源标题 - title: `${username}'s Shouts`, - // 源链接 - link: `https://www.furaffinity.net/user/${username}/`, - // 源说明 - description: `Fur Affinity ${username}'s Shouts`, - - // 遍历此前获取的数据 - item: data.map((item) => ({ - // 标题 - title: item.text, - // 正文 - description: `
${item.name}: ${item.text} `, - // 链接 - link: `https://www.furaffinity.net/user/${username}/`, - // 作者 - author: item.name, - // 日期 - pubDate: new Date(item.posted_at).toUTCString(), - })), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/status.js b/lib/routes-deprecated/furaffinity/status.js deleted file mode 100644 index 6b0d772e2ace6f..00000000000000 --- a/lib/routes-deprecated/furaffinity/status.js +++ /dev/null @@ -1,38 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/status.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - const data = response.data; - const status = data.online; - - let description = ''; - description = - Object.keys(data)[0] === 'online' - ? `Status: ${Object.keys(data)[0]}
Guests: ${status.guests}
Registered: ${status.registered}
Other: ${status.other}
Total: ${status.total}
Fa Server Time: ${data.fa_server_time}` - : 'offline'; - const item = []; - item.push({ - title: `Status:${Object.keys(data)[0]}`, - description, - link: `https://www.furaffinity.net/`, - }); - - ctx.state.data = { - // 源标题 - title: `Fur Affinity Status`, - // 源链接 - link: `https://www.furaffinity.net/`, - // 源说明 - description: `Fur Affinity Status`, - - item, - }; -}; diff --git a/lib/routes-deprecated/furaffinity/submission-comments.js b/lib/routes-deprecated/furaffinity/submission-comments.js deleted file mode 100644 index e08c5ba26ec018..00000000000000 --- a/lib/routes-deprecated/furaffinity/submission-comments.js +++ /dev/null @@ -1,50 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - // 传入参数 - const id = String(ctx.params.id); - - // 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/submission/${id}/comments.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - // 发起第二个 HTTP GET 请求,用于获取该作品的标题 - const response2 = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/submission/${id}.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - const data = response.data; - const data2 = response2.data; - - ctx.state.data = { - // 源标题 - title: `${data2.title} - Submission Comments`, - // 源链接 - link: `https://www.furaffinity.net/view/${id}/`, - // 源说明 - description: `Fur Affinity ${data2.title} - Submission Comments`, - - // 遍历此前获取的数据 - item: data.map((item) => ({ - // 标题 - title: item.text, - // 正文 - description: `
${item.name}: ${item.text}`, - // 链接 - link: `https://www.furaffinity.net/view/${id}/`, - // 作者 - author: item.name, - // 日期 - pubDate: new Date(item.posted_at).toUTCString(), - })), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/watchers.js b/lib/routes-deprecated/furaffinity/watchers.js deleted file mode 100644 index a02a2befeebabf..00000000000000 --- a/lib/routes-deprecated/furaffinity/watchers.js +++ /dev/null @@ -1,47 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - // 传入参数 - const username = String(ctx.params.username); - - // 添加参数username 和 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/user/${username}/watchers.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - // 发起第二个HTTP GET请求,用于获取该用户被关注总数 - const response2 = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/user/${username}.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - const data = response.data; - const data2 = response2.data; - - ctx.state.data = { - // 源标题 - title: `${username}'s Watcher List`, - // 源链接 - link: `https://www.furaffinity.net/watchlist/to/${username}/`, - // 源说明 - description: `Fur Affinity ${username}'s Watcher List`, - - // 遍历此前获取的数据 - item: data.map((item) => ({ - // 标题 - title: item, - // 正文 - description: `${username} was watched by ${item}
Totall: ${data2.watchers.count} `, - // 链接 - link: `https://www.furaffinity.net/user/${item}/`, - // 由于源API未提供日期,故无pubDate - })), - }; -}; diff --git a/lib/routes-deprecated/furaffinity/watching.js b/lib/routes-deprecated/furaffinity/watching.js deleted file mode 100644 index 9532320b4fd29e..00000000000000 --- a/lib/routes-deprecated/furaffinity/watching.js +++ /dev/null @@ -1,47 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - // 传入参数 - const username = String(ctx.params.username); - - // 添加参数username 和 发起 HTTP GET 请求 - const response = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/user/${username}/watching.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - // 发起第二个HTTP GET请求,用于获取该用户关注总数 - const response2 = await got({ - method: 'get', - url: `https://faexport.spangle.org.uk/user/${username}.json`, - headers: { - Referer: `https://faexport.spangle.org.uk/`, - }, - }); - - const data = response.data; - const data2 = response2.data; - - ctx.state.data = { - // 源标题 - title: `${username}'s Watching List`, - // 源链接 - link: `https://www.furaffinity.net/watchlist/by/${username}/`, - // 源说明 - description: `Fur Affinity ${username}}'s Search List`, - - // 遍历此前获取的数据 - item: data.map((item) => ({ - // 标题 - title: item, - // 正文 - description: `${username} watched ${item}
Totall: ${data2.watching.count}`, - // 链接 - link: `https://www.furaffinity.net/user/${item}/`, - // 由于源API未提供日期,故无pubDate - })), - }; -}; diff --git a/lib/routes-deprecated/gamersky/ent.js b/lib/routes-deprecated/gamersky/ent.js deleted file mode 100644 index 566423aa281216..00000000000000 --- a/lib/routes-deprecated/gamersky/ent.js +++ /dev/null @@ -1,89 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -const map = new Map([ - ['qysj', { title: '趣囧时间', suffix: 'ent/qw' }], - ['ymyy', { title: '游民影院', suffix: 'wenku/movie' }], - ['ygtx', { title: '游观天下', suffix: 'ent/discovery' }], - ['bztk', { title: '壁纸图库', suffix: 'ent/wp' }], - ['ympd', { title: '游民盘点', suffix: 'wenku' }], - ['ymfl', { title: '游民福利', suffix: 'ent/xz/' }], -]); - -module.exports = async (ctx) => { - const category = ctx.params.category; - const suffix = map.get(category).suffix; - const title = map.get(category).title; - - const url = `https://www.gamersky.com/${suffix}`; - const response = await got({ - method: 'get', - url, - }); - - const data = response.data; - const $ = cheerio.load(data); - - const list = $('ul.pictxt.contentpaging li') - .slice(0, 10) - .map(function () { - const info = { - title: $(this).find('div.tit a').text(), - link: $(this).find('div.tit a').attr('href'), - pubDate: new Date($(this).find('.time').text()).toUTCString(), - }; - return info; - }) - .get(); - - const out = await Promise.all( - list.map(async (info) => { - const title = info.title; - const itemUrl = info.link.startsWith('https://') ? info.link : `https://www.gamersky.com/${info.link}`; - const pubDate = info.pubDate; - - const cache = await ctx.cache.get(itemUrl); - if (cache) { - return JSON.parse(cache); - } - - const response = await got.get(itemUrl); - const $ = cheerio.load(response.data); - - let next_pages = $('div.page_css a') - .map(function () { - return $(this).attr('href'); - }) - .get(); - - next_pages = next_pages.slice(0, -1); - - const des = await Promise.all( - next_pages.map(async (next_page) => { - const response = await got.get(next_page); - const $ = cheerio.load(response.data); - $('div.page_css').remove(); - - return $('.Mid2L_con').html().trim(); - }) - ); - - const description = des.join(''); - - const single = { - title, - link: itemUrl, - description, - pubDate, - }; - ctx.cache.set(itemUrl, JSON.stringify(single)); - return single; - }) - ); - - ctx.state.data = { - title: `游民娱乐-${title}`, - link: url, - item: out, - }; -}; diff --git a/lib/routes-deprecated/gamersky/news.js b/lib/routes-deprecated/gamersky/news.js deleted file mode 100644 index b0d17c330b6780..00000000000000 --- a/lib/routes-deprecated/gamersky/news.js +++ /dev/null @@ -1,34 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - const response = await got({ - method: 'get', - url: 'https://www.gamersky.com/news/', - headers: { - Referer: 'https://www.gamersky.com/news/', - }, - }); - - const data = response.data; - const $ = cheerio.load(data); - - const out = $('.Mid2L_con li') - .slice(0, 10) - .map(function () { - const info = { - title: $(this).find('.tt').text(), - link: $(this).find('.tt').attr('href'), - pubDate: new Date($(this).find('.time').text()).toUTCString(), - description: $(this).find('.txt').text(), - }; - return info; - }) - .get(); - - ctx.state.data = { - title: '游民星空-今日推荐', - link: 'https://www.gamersky.com/news/', - item: out, - }; -}; diff --git a/lib/routes-deprecated/geekpark/breakingnews.js b/lib/routes-deprecated/geekpark/breakingnews.js deleted file mode 100644 index 67f1e4255f7b7e..00000000000000 --- a/lib/routes-deprecated/geekpark/breakingnews.js +++ /dev/null @@ -1,25 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - const url = 'https://mainssl.geekpark.net/api/v1/posts'; - const link = 'https://www.geekpark.net'; - - const response = await got({ - method: 'get', - url, - }); - const data = response.data.posts; - - ctx.state.data = { - title: '极客公园 - 资讯', - description: - '极客公园聚焦互联网领域,跟踪最新的科技新闻动态,关注极具创新精神的科技产品。目前涵盖前沿科技、游戏、手机评测、硬件测评、出行方式、共享经济、人工智能等全方位的科技生活内容。现有前沿社、挖App、深度报道、极客养成指南等多个内容栏目。', - link, - item: data.map(({ title, content, published_at, id }) => ({ - title, - link: `https://www.geekpark.net/news/${id}`, - description: content, - pubDate: new Date(published_at).toUTCString(), - })), - }; -}; diff --git a/lib/routes-deprecated/hko/weather.js b/lib/routes-deprecated/hko/weather.js deleted file mode 100644 index 816b6ece4cfea3..00000000000000 --- a/lib/routes-deprecated/hko/weather.js +++ /dev/null @@ -1,44 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - const url = 'http://rss.weather.gov.hk/rss/CurrentWeather.xml'; - const cache = await ctx.cache.get(url); - if (cache) { - return JSON.parse(cache); - } - - const { data } = await got({ method: 'get', url }); - const $ = cheerio.load(data, { - xmlMode: true, - }); - const weather = $('description').slice(1, 2).text(); - - const $$ = cheerio.load(weather); - const items = []; - $$('table') - .find('td') - .each((index, element) => { - if (index % 2) { - return; - } - const area = $$(element).text(); - const degree = $$(element).next().text().split(' ')[0]; - - const item = { - title: area, - description: degree, - }; - items.push(item); - }); - const result = { - title: 'Current Weather Report', - description: ` provided by the Hong Kong Observatory: ${$('pubDate').text()}`, - link: url, - item: items, - }; - // one hour cache - ctx.cache.set(url, JSON.stringify(result)); - - ctx.state.data = result; -}; diff --git a/lib/routes-deprecated/ieee/author.js b/lib/routes-deprecated/ieee/author.js deleted file mode 100644 index cb265300508340..00000000000000 --- a/lib/routes-deprecated/ieee/author.js +++ /dev/null @@ -1,30 +0,0 @@ -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - const { aid, sortType, count = 10 } = ctx.params; - - const response = await got(`https://ieeexplore.ieee.org/rest/author/${aid}`); - const author = response.data[0]; - - const { data: papers } = await got.post('https://ieeexplore.ieee.org/rest/search', { - json: { - rowsPerPage: count, - searchWithin: [`"Author Ids": ${aid}`], - sortType, - }, - }); - - ctx.state.data = { - title: `${author.preferredName} on IEEE Xplore`, - link: `https://ieeexplore.ieee.org/author/${aid}`, - description: author.bioParagraphs.join('
'), - item: papers.records.map((item) => ({ - title: item.articleTitle, - author: item.authors.map((author) => author.preferredName).join(', '), - category: item.articleContentType, - description: item.abstract, - pubDate: item.publicationDate, - link: `https://ieeexplore.ieee.org${item.documentLink}`, - })), - }; -}; diff --git a/lib/routes-deprecated/ifanr/index.js b/lib/routes-deprecated/ifanr/index.js deleted file mode 100644 index 0e472393138207..00000000000000 --- a/lib/routes-deprecated/ifanr/index.js +++ /dev/null @@ -1,71 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - let host = 'https://www.ifanr.com'; - - let channel; - if (ctx.params.channel) { - channel = ctx.params.channel.toLowerCase(); - channel = channel.split('-').join('/'); - - // 兼容旧版路由 - host = channel === 'appso' ? `${host}/app` : `${host}/${channel}`; - } else { - host = `${host}/app`; - channel = 'app'; - } - - const response = await got.get(host); - - const $ = cheerio.load(response.data); - - let list; - switch (channel) { - case 'dasheng': - list = $('#articles-collection') - .find('.c-bricks__brick a.c-dasheng') - .map((i, e) => $(e).attr('href')) - .get(); - break; - default: - list = $('#articles-collection') - .find('.article-item .article-info h3 a') - .map((i, e) => $(e).attr('href')) - .get(); - break; - } - - const out = await Promise.all( - list.map(async (itemUrl) => { - const cache = await ctx.cache.get(itemUrl); - if (cache) { - return JSON.parse(cache); - } - - const response = await got.get(itemUrl); - const $ = cheerio.load(response.data); - - const item = { - title: $('.c-single-normal__title').text(), - link: itemUrl, - author: $('.c-card-author__name').text(), - description: $('article').html(), - pubDate: new Date($('.c-article-header-meta__time').attr('data-timestamp') * 1000), - }; - // "大声" 标题 - if (/^https?:\/\/www\.ifanr\.com\/dasheng\/.*$/.test(itemUrl)) { - item.title = $('.c-dasheng_header__title').text(); - } - - ctx.cache.set(itemUrl, JSON.stringify(item)); - return item; - }) - ); - - ctx.state.data = { - title: `iFanr - ${$('h1.c-archive-header__title').text()}:${$('div.c-archive-header__desc').text()}`, - link: host, - item: out, - }; -}; diff --git a/lib/routes-deprecated/iplay/home.js b/lib/routes-deprecated/iplay/home.js deleted file mode 100644 index f879546e9b5489..00000000000000 --- a/lib/routes-deprecated/iplay/home.js +++ /dev/null @@ -1,26 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); -const util = require('./utils'); - -module.exports = async (ctx) => { - const url = `https://www.iplaysoft.com/`; - const response = await got({ - method: 'get', - url, - headers: { - Referer: url, - }, - }); - - const $ = cheerio.load(response.data); - const list = $('#postlist .entry').get(); - - const result = await util.ProcessFeed(list, ctx.cache); - - ctx.state.data = { - title: $('title').text().split('-')[0], - link: url, - description: $('meta[name="description"]').attr('content'), - item: result, - }; -}; diff --git a/lib/routes-deprecated/iplay/utils.js b/lib/routes-deprecated/iplay/utils.js deleted file mode 100644 index 01a698d58d556a..00000000000000 --- a/lib/routes-deprecated/iplay/utils.js +++ /dev/null @@ -1,57 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); -const url = require('url'); - -async function load(link) { - const response = await got.get(link); - const $ = cheerio.load(response.data); - // 处理日期 - const datestr = $('.entry-meta li') - .text() - .match(/生产日期:异次纪元 ([\S\s]*?秒)/)[1] - .match(/(\d{1,2})/gm); - for (let i = 1; i < datestr.length; i++) { - datestr[i] = datestr[i].padStart(2, '0'); - } - const date = new Date('20' + datestr[0] + '-' + datestr[1] + '-' + datestr[2] + ' ' + datestr[3] + ':' + datestr[4] + ':' + datestr[5]); - const timeZone = 8; - const serverOffset = date.getTimezoneOffset() / 60; - const pubDate = new Date(date.getTime() - 60 * 60 * 1000 * (timeZone + serverOffset)).toUTCString(); - // 提取详情 - let description = $('.entry-content').html(); - // 去除data-srcset,srcset,将data-src替换成src以正常显示图片 - description = description.replaceAll(/(data-){0,1}srcset="[\S\s]*?"/g, ''); - description = description.replaceAll('data-src', 'src'); - return { description, pubDate }; -} - -const ProcessFeed = (list, caches) => { - const host = 'https://www.iplaysoft.com/'; - return Promise.all( - list.map(async (item) => { - const $ = cheerio.load(item); - const $title = $('.entry-title a'); - // 还原相对链接为绝对链接 - const itemUrl = url.resolve(host, $title.attr('href')); - - // 列表上提取到的信息 - const single = { - title: $title.text(), - link: itemUrl, - // author: $('.nickname').text(), - guid: itemUrl, - }; - - // 使用tryGet方法从缓存获取内容。 - // 当缓存中无法获取到链接内容的时候,则使用load方法加载文章内容。 - const other = await caches.tryGet(itemUrl, () => load(itemUrl)); - - // 合并解析后的结果集作为该篇文章最终的输出结果 - return Object.assign({}, single, other); - }) - ); -}; - -module.exports = { - ProcessFeed, -}; diff --git a/lib/routes-deprecated/konachan/post-popular-recent.js b/lib/routes-deprecated/konachan/post-popular-recent.js deleted file mode 100644 index e500e3ab77779c..00000000000000 --- a/lib/routes-deprecated/konachan/post-popular-recent.js +++ /dev/null @@ -1,67 +0,0 @@ -const got = require('@/utils/got'); -const queryString = require('query-string'); - -module.exports = async (ctx) => { - const { period = '1d' } = ctx.params; - - const baseUrl = ctx.path.startsWith('/konachan.net') ? 'https://konachan.net' : 'https://konachan.com'; - const safemode = ctx.path.startsWith('/konachan.net'); - - const response = await got({ - method: 'get', - url: `${baseUrl}/post/popular_recent.json`, - searchParams: queryString.stringify({ - period, - }), - }); - - const posts = response.data; - - const titles = { - '1d': 'Exploring last 24 hours ', - '1w': 'Exploring last week', - '1m': 'Exploring last month', - '1y': 'Exploring last year', - }; - - const title = titles[period] || titles['1d']; - - ctx.state.data = { - title: `${title} - Konachan Anime Wallpapers`, - link: `${baseUrl}/post/popular_recent`, - item: posts - .filter((post) => !(safemode && post.rating !== 's')) - .map((post) => { - const content = (url) => { - if (url.startsWith('//')) { - url = 'https:' + url; - } - let result = ``; - if (post.source) { - result += `source`; - } - if (post.parent_id) { - result += `parent`; - } - return result; - }; - - const created_at = post.created_at * 1e3; - - return { - title: post.tags, - id: `${ctx.path}#${post.id}`, - guid: `${ctx.path}#${post.id}`, - link: `${baseUrl}/post/show/${post.id}`, - author: post.author, - published: new Date(created_at).toISOString(), - pubDate: new Date(created_at).toUTCString(), - description: content(post.sample_url), - summary: content(post.sample_url), - content: { html: content(post.file_url) }, - image: post.file_url, - category: post.tags.split(/\s+/), - }; - }), - }; -}; diff --git a/lib/routes-deprecated/lol/newsindex.js b/lib/routes-deprecated/lol/newsindex.js deleted file mode 100644 index 61903dea7c8435..00000000000000 --- a/lib/routes-deprecated/lol/newsindex.js +++ /dev/null @@ -1,87 +0,0 @@ -const got = require('@/utils/got'); -const map = new Map([ - ['zh', { name: '综合', channelid: '23' }], - ['gg', { name: '公告', channelid: '24' }], - ['ss', { name: '赛事', channelid: '25' }], - ['gl', { name: '攻略', channelid: '27' }], - ['sq', { name: '社区', channelid: '1934' }], -]); -const refererUrl = 'https://lol.qq.com/news/index.shtml'; -const apiUrl = 'https://apps.game.qq.com/cmc/zmMcnTargetContentList?r0=jsonp&page=1&num=16&target='; -module.exports = async (ctx) => { - const type = ctx.params.type || 'all'; - if (type === 'all') { - const tasks = []; - for (const value of map.values()) { - tasks.push(getPage(value.channelid, value.name)); - } - const results = await Promise.all(tasks); - let items = []; - for (const result of results) { - items = [...items, ...result]; - } - ctx.state.data = { - title: `【全部】 - 英雄联盟 - 新闻列表`, - link: `https://lol.qq.com/news/index.shtml`, - description: `英雄联盟官方网站,海量风格各异的英雄,丰富、便捷的物品合成系统,游戏内置的匹配、排行和竞技系统,独创的“召唤师”系统及技能、符文、天赋等系统组合,必将带你进入一个崭新而又丰富多彩的游戏世界。`, - item: items, - }; - } else { - const OutName = map.get(type).name; - const OutId = map.get(type).channelid; - ctx.state.data = { - title: `【${OutName}】 - 英雄联盟 - 新闻列表`, - link: `https://lol.qq.com/news/index.shtml`, - description: `英雄联盟官方网站,海量风格各异的英雄,丰富、便捷的物品合成系统,游戏内置的匹配、排行和竞技系统,独创的“召唤师”系统及技能、符文、天赋等系统组合,必将带你进入一个崭新而又丰富多彩的游戏世界。`, - item: await getPage(OutId, OutName), - }; - } - async function getPage(id, typeName) { - let list; - if (id === '1934') { - // id=1934,社区的数据是另一个api - const response = await got({ - method: 'get', - url: 'https://apps.game.qq.com/cmc/cross?serviceId=3&source=zm&tagids=1934&typeids=1,2&withtop=yes&start=0&limit=16', - headers: { - Referer: refererUrl, - }, - }); - list = response.data.data.items; - } else { - // 非社区的数据处理,多了callback需要截取 - const response = ( - await got({ - method: 'get', - url: apiUrl + id, - headers: { - Referer: refererUrl, - }, - }) - ).data.trim(); - try { - const jsonString = response.slice(9, -2); - list = JSON.parse(jsonString).data.result; - } catch { - // console.error(error); - } - } - function getUrl(sRedirectURL, iDocID, sVID) { - // 由于数据源太多,具体的URL返回逻辑可以参考news/index.html页面里面的handleData方法 - - if (sRedirectURL) { - sRedirectURL = sRedirectURL.indexOf('?') > 0 ? sRedirectURL + '&docid=' + iDocID : sRedirectURL + '?docid=' + iDocID; - } else { - sRedirectURL = sVID ? 'http://lol.qq.com/v/v2/detail.shtml?docid=' + iDocID : 'http://lol.qq.com/news/detail.shtml?docid=' + iDocID; - } - - return sRedirectURL; - } - return list.map((item) => ({ - title: `【${typeName}】` + item.sTitle, - link: getUrl(item.sRedirectURL, item.iDocID, item.sVID), - pubDate: new Date(`${item.sCreated} UTC+8`).toUTCString(), - guid: item.iDocID, - })); - } -}; diff --git a/lib/routes-deprecated/ltaaa/index.js b/lib/routes-deprecated/ltaaa/index.js deleted file mode 100644 index eec7915f50bbf3..00000000000000 --- a/lib/routes-deprecated/ltaaa/index.js +++ /dev/null @@ -1,69 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); -const timezone = require('@/utils/timezone'); -const { parseDate } = require('@/utils/parse-date'); - -module.exports = async (ctx) => { - const category = ctx.params.category || 'latest'; - - const rootUrl = 'http://www.ltaaa.cn'; - const currentUrl = `${rootUrl}/${category === 'picture' ? category : `article${category === 'latest' ? '' : `/${category}`}`}`; - - const response = await got({ - method: 'get', - url: currentUrl, - }); - - const $ = cheerio.load(response.data); - - const list = $(category === 'picture' || category === 'curiosities' ? 'dd .title' : '.li-title a') - .slice(0, 10) - .map((_, item) => { - item = $(item); - - return { - title: item.text(), - link: `${rootUrl}${item.attr('href')}`, - }; - }) - .get(); - - const items = await Promise.all( - list.map((item) => - ctx.cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - const content = cheerio.load(detailResponse.data); - - if (category === 'picture') { - item.description = ''; - content('.show li').each(function () { - item.description += content(this).find('a').html() + (content(this).find('.pic-p').html() || ''); - }); - item.pubDate = parseDate( - content('.view a img') - .attr('src') - .match(/http:\/\/img\.ltaaa\.cn\/uploadfile\/(.*)\/\d+\.jpg/)[1], - 'YYYY/MM/DD' - ); - } else { - content('.post-param').find('a, span').remove(); - item.pubDate = timezone(new Date(content('.post-param').text().trim()), +8); - - content('.post-title, .post-param, .post-keywords, .like-post, .clear, .hook').remove(); - item.description = content('.post-body').html(); - } - - return item; - }) - ) - ); - - ctx.state.data = { - title: $('title').text(), - link: currentUrl, - item: items, - }; -}; diff --git a/lib/routes-deprecated/matters/author.js b/lib/routes-deprecated/matters/author.js deleted file mode 100644 index 2e6e65730bb87b..00000000000000 --- a/lib/routes-deprecated/matters/author.js +++ /dev/null @@ -1,49 +0,0 @@ -const got = require('@/utils/got'); -const { parseDate } = require('@/utils/parse-date'); - -module.exports = async (ctx) => { - const uid = ctx.params.uid; - const host = `https://matters.news`; - const url = `https://server.matters.news/graphql`; - const response = await got({ - method: 'post', - url, - json: { - query: ` - { - user(input: { userName: "${uid}" }) { - displayName - info { - description - } - articles(input: { first: 20 }) { - edges { - node { - slug - mediaHash - title - content - createdAt - } - } - } - } - }`, - }, - }); - - const user = response.data.data.user; - - ctx.state.data = { - title: `Matters | ${user.displayName}`, - link: `${host}/@${uid}`, - description: user.info.description, - item: user.articles.edges.map(({ node: article }) => ({ - title: article.title, - author: user.displayName, - description: article.content, - link: `${host}/@${uid}/${article.slug}-${article.mediaHash}`, - pubDate: parseDate(article.createdAt), - })), - }; -}; diff --git a/lib/routes-deprecated/matters/latest.js b/lib/routes-deprecated/matters/latest.js deleted file mode 100644 index a68eb370111f3a..00000000000000 --- a/lib/routes-deprecated/matters/latest.js +++ /dev/null @@ -1,69 +0,0 @@ -const got = require('@/utils/got'); -const { parseDate } = require('@/utils/parse-date'); - -module.exports = async (ctx) => { - const type = ctx.params.type || 'latest'; - const url = `https://server.matters.news/graphql`; - const options = { - latest: { - title: '最新', - apiType: 'newest', - }, - heat: { - title: '熱議', - apiType: 'hottest', - }, - essence: { - title: '精華', - apiType: 'icymi', - }, - }; - - const response = await got({ - method: 'post', - url, - json: { - query: ` - { - viewer { - id - recommendation { - feed: ${options[type].apiType}(input: { first: 10 }) { - edges { - node { - author { - userName - displayName - } - slug - mediaHash - title - content - createdAt - } - } - } - } - } - }`, - }, - }); - - const item = response.data.data.viewer.recommendation.feed.edges.map(({ node }) => { - const link = `https://matters.news/@${node.author.userName}/${node.slug}-${node.mediaHash}`; - const article = node.content; - return { - title: node.title, - link, - description: article, - author: node.author.displayName, - pubDate: parseDate(node.createdAt), - }; - }); - - ctx.state.data = { - title: `Matters | ${options[type].title}`, - link: 'https://matters.news/', - item, - }; -}; diff --git a/lib/routes-deprecated/matters/tags.js b/lib/routes-deprecated/matters/tags.js deleted file mode 100644 index 14629f5a7a2e48..00000000000000 --- a/lib/routes-deprecated/matters/tags.js +++ /dev/null @@ -1,56 +0,0 @@ -const got = require('@/utils/got'); -const { parseDate } = require('@/utils/parse-date'); - -module.exports = async (ctx) => { - const tid = ctx.params.tid; - const host = `https://matters.news`; - const url = `https://server.matters.news/graphql`; - const response = await got({ - method: 'post', - url, - json: { - query: ` - { - node(input: { id: "${tid}" }) { - ... on Tag { - content - articles(input:{first: 20}){ - edges { - node { - id - title - slug - cover - summary - mediaHash - content - createdAt - author { - id - userName - displayName - } - } - } - } - } - } - }`, - }, - }); - - const node = response.data.data.node; - - ctx.state.data = { - title: `Matters | ${node.content}`, - link: `${host}/tags/${tid}`, - description: node.content, - item: node.articles.edges.map(({ node: article }) => ({ - title: article.title, - author: article.author.displayName, - description: article.content, - link: `${host}/@${article.author.id}/${article.slug}-${article.mediaHash}`, - pubDate: parseDate(article.createdAt), - })), - }; -}; diff --git a/lib/routes-deprecated/p-articles/contributors.js b/lib/routes-deprecated/p-articles/contributors.js deleted file mode 100644 index 1d7dc09cfff806..00000000000000 --- a/lib/routes-deprecated/p-articles/contributors.js +++ /dev/null @@ -1,45 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); -const url = require('url'); -const utils = require('./utils'); - -const host = 'https://p-articles.com/'; - -module.exports = async (ctx) => { - const author = ctx.params.author; - - const link = `https://p-articles.com/contributors/${author}`; - - const response = await got.get(link); - const $ = cheerio.load(response.data); - - const list = $('div.contect_box_05in > a') - .map(function () { - const info = { - title: $(this).find('h3').text().trim(), - link: url.resolve(host, $(this).attr('href')), - }; - return info; - }) - .get(); - - const out = await Promise.all( - list.map(async (info) => { - const link = info.link; - - const cache = await ctx.cache.get(link); - if (cache) { - return JSON.parse(cache); - } - const response = await got.get(link); - - return utils.ProcessFeed(ctx, info, response.data); - }) - ); - - ctx.state.data = { - title: `虛詞作者-${author}`, - link, - item: out, - }; -}; diff --git a/lib/routes-deprecated/p-articles/section.js b/lib/routes-deprecated/p-articles/section.js deleted file mode 100644 index b0b5631e4fc8e8..00000000000000 --- a/lib/routes-deprecated/p-articles/section.js +++ /dev/null @@ -1,53 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); -const url = require('url'); -const utils = require('./utils'); - -const host = 'https://p-articles.com/'; - -module.exports = async (ctx) => { - let section_name = ctx.params.section; - section_name = section_name.replace('-', '/'); - section_name += '/'; - - const link = url.resolve(host, section_name); - - const response = await got.get(link); - const $ = cheerio.load(response.data); - - const top_info = { - title: $('div.inner_top_title_01 > h1 > a').text(), - link: url.resolve(host, $('div.inner_top_title_01 > h1 > a').attr('href')), - }; - - const list = $('div.contect_box_04 > a') - .map(function () { - const info = { - title: $(this).find('h1').text().trim(), - link: url.resolve(host, $(this).attr('href')), - }; - return info; - }) - .get(); - - list.unshift(top_info); - - const out = await Promise.all( - list.map(async (info) => { - const link = info.link; - - const cache = await ctx.cache.get(link); - if (cache) { - return JSON.parse(cache); - } - const response = await got.get(link); - - return utils.ProcessFeed(ctx, info, response.data); - }) - ); - ctx.state.data = { - title: `虛詞版块-${section_name}`, - link, - item: out, - }; -}; diff --git a/lib/routes-deprecated/p-articles/utils.js b/lib/routes-deprecated/p-articles/utils.js deleted file mode 100644 index bb3552f6a7f6d6..00000000000000 --- a/lib/routes-deprecated/p-articles/utils.js +++ /dev/null @@ -1,30 +0,0 @@ -const cheerio = require('cheerio'); -const { parseRelativeDate } = require('@/utils/parse-date'); -const timezone = require('@/utils/timezone'); - -const ProcessFeed = (ctx, info, data) => { - const title = info.title; - const itemUrl = info.link; - - const $ = cheerio.load(data); - - const author = $('div.detail_title_02 > h4 > a:nth-child(2)').text().trim(); - - const date_value = $('div.detail_title_02 > h4 ').text().trim(); - - const description = $('div.detail_contect_01').html(); - - const single = { - title, - link: itemUrl, - description, - author, - pubDate: timezone(parseRelativeDate(date_value), 8), - }; - ctx.cache.set(itemUrl, JSON.stringify(single)); - return Promise.resolve(single); -}; - -module.exports = { - ProcessFeed, -}; diff --git a/lib/routes-deprecated/qstheory/index.js b/lib/routes-deprecated/qstheory/index.js deleted file mode 100644 index 95d5a4e3bec31e..00000000000000 --- a/lib/routes-deprecated/qstheory/index.js +++ /dev/null @@ -1,122 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -const rootUrl = 'http://www.qstheory.cn/'; - -const config = { - toutiao: { - title: '头条', - url: `${rootUrl}/v9zhuanqu/toutiao/index.htm`, - }, - qswp: { - title: '网评', - url: `${rootUrl}/qswp.htm`, - }, - qssp: { - title: '视频', - url: `${rootUrl}/qssp/index.htm`, - }, - qslgxd: { - title: '原创', - url: `${rootUrl}/qslgxd/index.htm`, - }, - economy: { - title: '经济', - url: `${rootUrl}/economy/index.htm`, - }, - politics: { - title: '政治', - url: `${rootUrl}/politics/index.htm`, - }, - culture: { - title: '文化', - url: `${rootUrl}/culture/index.htm`, - }, - society: { - title: '社会', - url: `${rootUrl}/society/index.htm`, - }, - cpc: { - title: '党建', - url: `${rootUrl}/cpc/index.htm`, - }, - science: { - title: '科教', - url: `${rootUrl}/science/index.htm`, - }, - zoology: { - title: '生态', - url: `${rootUrl}/zoology/index.htm`, - }, - defense: { - title: '国防', - url: `${rootUrl}/defense/index.htm`, - }, - international: { - title: '国际', - url: `${rootUrl}/international/index.htm`, - }, - books: { - title: '图书', - url: `${rootUrl}/books/index.htm`, - }, - xxbj: { - title: '学习笔记', - url: `${rootUrl}/qszq/xxbj/index.htm`, - }, -}; - -module.exports = async (ctx) => { - const category = ctx.params.category || 'toutiao'; - - const currentUrl = config[category].url; - const response = await got({ - method: 'get', - url: currentUrl, - }); - - const $ = cheerio.load(response.data); - - const list = $('.list-style1 ul li a, .text h2 a, .no-pic ul li a') - .slice(0, 15) - .map((_, item) => { - item = $(item); - return { - title: item.text(), - link: item.attr('href'), - }; - }) - .get(); - - const items = await Promise.all( - list.map((item) => - ctx.cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - const content = cheerio.load(detailResponse.data); - - content('.fs-text, .fs-pinglun, .hidden-xs').remove(); - - item.author = content('.appellation').text(); - item.description = content('.highlight, .text').html() || content('.content').html(); - item.pubDate = new Date( - content('.puttime_mobi, .pubtime, .headtitle span') - .text() - .replace('发表于', '') - .replaceAll(/(年|月)/g, '-') - .replace('日', '') - ).toUTCString(); - - return item; - }) - ) - ); - - ctx.state.data = { - title: $('title').text(), - link: currentUrl, - item: items, - }; -}; diff --git a/lib/routes-deprecated/universities/bjtu/gs/index.js b/lib/routes-deprecated/universities/bjtu/gs/index.js deleted file mode 100644 index 742c56bb985fa7..00000000000000 --- a/lib/routes-deprecated/universities/bjtu/gs/index.js +++ /dev/null @@ -1,130 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - const type = ctx.params.type; - const struct = { - zs: { - selector: { - list: '.mainleft_box li', - }, - url: 'https://gs.bjtu.edu.cn/cms/zszt/item/?tag=1', - name: '招生 - 北京交通大学研究生院', - }, - noti: { - selector: { - list: '.tab-content li', - }, - url: 'https://gs.bjtu.edu.cn/cms/item/?tag=2', - name: '通知公告 - 北京交通大学研究生院', - }, - news: { - selector: { - list: '.tab-content li', - }, - url: 'https://gs.bjtu.edu.cn/cms/item/?tag=3', - name: '新闻动态 - 北京交通大学研究生院', - }, - zsxc: { - selector: { - list: '.tab-content li', - }, - url: 'https://gs.bjtu.edu.cn/cms/item/?tag=4', - name: '招生宣传 - 北京交通大学研究生院', - }, - py: { - selector: { - list: '.tab-content li', - }, - url: 'https://gs.bjtu.edu.cn/cms/item/?tag=5', - name: '培养 - 北京交通大学研究生院', - }, - xw: { - selector: { - list: '.tab-content li', - }, - url: 'https://gs.bjtu.edu.cn/cms/item/?tag=7', - name: '学位 - 北京交通大学研究生院', - }, - ygbtzgg: { - selector: { - list: '.tab-content li', - }, - url: 'https://gs.bjtu.edu.cn/cms/item/?tag=9', - name: '通知公告 - 研工部 - 北京交通大学研究生院', - }, - ygbnews: { - selector: { - list: '.tab-content li', - }, - url: 'https://gs.bjtu.edu.cn/cms/item/?tag=10', - name: '新闻动态 - 研工部 - 北京交通大学研究生院', - }, - all: { - selector: { - list: '.tab-content li', - }, - url: 'https://gs.bjtu.edu.cn/cms/item/?tag=12', - name: '所有文章 - 北京交通大学研究生院', - }, - bszs: { - selector: { - list: '.mainleft_box li', - }, - url: 'https://gs.bjtu.edu.cn/cms/zszt/item/?cat=2', - name: '博士招生 - 北京交通大学研究生院', - }, - sszs: { - selector: { - list: '.mainleft_box li', - }, - url: 'https://gs.bjtu.edu.cn/cms/zszt/item/?cat=3', - name: '硕士招生 - 北京交通大学研究生院', - }, - zsjz: { - selector: { - list: '.mainleft_box li', - }, - url: 'https://gs.bjtu.edu.cn/cms/zszt/item/?cat=4', - name: '招生简章 - 北京交通大学研究生院', - }, - zcfg: { - selector: { - list: '.mainleft_box li', - }, - url: 'https://gs.bjtu.edu.cn/cms/zszt/item/?cat=5', - name: '政策法规 - 北京交通大学研究生院', - }, - }; - - const url = struct[type].url; - const response = await got({ - method: 'get', - url, - }); - - const data = response.data; - - const $ = cheerio.load(data); - const list = $(struct[type].selector.list); - - ctx.state.data = { - title: struct[type].name, - link: url, - description: '北京交通大学研究生院', - item: - list && - list - .map((index, item) => { - item = $(item); - const date = new Date(Date.parse(item.find('li span').text().slice(1, 11).replaceAll('-', '/'))); - const bj_date = date.getTime() / 1000 + 8 * 60 * 60; - const title = item - .find('li a') - .text() - .replaceAll(/\[.*?]/g, ''); - return { title, description: title, link: item.find('li a').attr('href'), pubDate: new Date(Number.parseInt(bj_date) * 1000) }; - }) - .get(), - }; -}; diff --git a/lib/routes.test.ts b/lib/routes.test.ts index 20fedf119bdc21..d9cb64df8fe4ce 100644 --- a/lib/routes.test.ts +++ b/lib/routes.test.ts @@ -69,10 +69,16 @@ async function checkRSS(response) { describe('routes', () => { for (const route in routes) { - it.concurrent(route, async () => { - const response = await app.request(routes[route]); - expect(response.status).toBe(200); - await checkRSS(response); - }); + it.concurrent( + route, + { + timeout: 60000, + }, + async () => { + const response = await app.request(routes[route]); + expect(response.status).toBe(200); + await checkRSS(response); + } + ); } }); diff --git a/lib/routes/005/index.ts b/lib/routes/005/index.ts index 85f9f0d4548f0f..af4785a5d47d7f 100644 --- a/lib/routes/005/index.ts +++ b/lib/routes/005/index.ts @@ -108,9 +108,9 @@ export const route: Route = { example: '/005/zx', parameters: { category: '分类,可在对应分类页 URL 中找到,默认为二次元资讯' }, description: ` - | 二次元资讯 | 慢慢说 | 道听途说 | 展会资讯 | - | ---------- | ------ | -------- | -------- | - | zx | zwh | dtts | zh | +| 二次元资讯 | 慢慢说 | 道听途说 | 展会资讯 | +| ---------- | ------ | -------- | -------- | +| zx | zwh | dtts | zh | `, categories: ['anime'], diff --git a/lib/routes/005/namespace.ts b/lib/routes/005/namespace.ts index fcf20a8fb0f1bd..d1d87624113ef9 100644 --- a/lib/routes/005/namespace.ts +++ b/lib/routes/005/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: '005.tv', categories: ['anime'], description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/0818tuan/index.ts b/lib/routes/0818tuan/index.ts index 436d28bc786d51..d39ee8927076c8 100644 --- a/lib/routes/0818tuan/index.ts +++ b/lib/routes/0818tuan/index.ts @@ -22,8 +22,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| 最新线报 | 实测活动 | 优惠券 | - | -------- | -------- | ------ | - | 1 | 2 | 3 |`, +| -------- | -------- | ------ | +| 1 | 2 | 3 |`, }; async function handler(ctx) { diff --git a/lib/routes/0818tuan/namespace.ts b/lib/routes/0818tuan/namespace.ts index 0823033afc387e..8625a0692fbfd7 100644 --- a/lib/routes/0818tuan/namespace.ts +++ b/lib/routes/0818tuan/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '0818 团', url: '0818tuan.com', + lang: 'zh-CN', }; diff --git a/lib/routes/0x80/namespace.ts b/lib/routes/0x80/namespace.ts index 8866d08e7919a9..a84007277e7ee4 100644 --- a/lib/routes/0x80/namespace.ts +++ b/lib/routes/0x80/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: 'Wojciech Muła', url: '0x80.pl', description: '', + lang: 'en', }; diff --git a/lib/routes/10jqka/namespace.ts b/lib/routes/10jqka/namespace.ts new file mode 100644 index 00000000000000..89c8f220524e73 --- /dev/null +++ b/lib/routes/10jqka/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '同花顺财经', + url: '10jqka.com.cn', + categories: ['finance'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/10jqka/realtimenews.ts b/lib/routes/10jqka/realtimenews.ts new file mode 100644 index 00000000000000..48f821d30969e9 --- /dev/null +++ b/lib/routes/10jqka/realtimenews.ts @@ -0,0 +1,143 @@ +import { Route } from '@/types'; + +import got from '@/utils/got'; +import { load } from 'cheerio'; +import iconv from 'iconv-lite'; +import { parseDate } from '@/utils/parse-date'; + +export const handler = async (ctx) => { + const { tag } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + + const rootUrl = 'https://news.10jqka.com.cn'; + const apiUrl = new URL('tapp/news/push/stock', rootUrl).href; + const currentUrl = new URL('realtimenews.html', rootUrl).href; + + const { data: currentResponse } = await got(currentUrl, { + responseType: 'buffer', + }); + + const $ = load(iconv.decode(currentResponse, 'gbk')); + + const language = $('html').prop('lang'); + + const { data: response } = await got(apiUrl, { + searchParams: { + page: 1, + tag: tag ?? '', + }, + }); + + const items = + response.data?.list.slice(0, limit).map((item) => { + const title = item.title; + const description = item.digest; + const guid = `10jqka-${item.seq}`; + const image = item.picUrl; + + return { + title, + description, + pubDate: parseDate(item.ctime, 'X'), + link: item.url, + category: [...new Set([item.color === '2' ? '重要' : undefined, ...item.tags.map((c) => c.name), ...item.tagInfo.map((c) => c.name)])].filter(Boolean), + author: item.source, + guid, + id: guid, + content: { + html: description, + text: description, + }, + image, + banner: item.picUrl, + updated: parseDate(item.rtime, 'X'), + language, + }; + }) ?? []; + + const title = $('title').text(); + const image = $('h1 a img').prop('src'); + + return { + title, + description: title.split(/_/).pop(), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $('meta[property="og:site_name"]').prop('content'), + language, + }; +}; + +export const route: Route = { + path: '/realtimenews/:tag?', + name: '7×24小时要闻直播', + url: 'news.10jqka.com.cn', + maintainers: ['nczitzk'], + handler, + example: '/10jqka/realtimenews', + parameters: { tag: '标签,默认为全部' }, + description: `::: tip + 若订阅 [7×24小时要闻直播](https://news.10jqka.com.cn/realtimenews.html) 的 \`公告\` 标签。将 \`公告\` 作为标签参数填入,此时路由为 [\`/10jqka/realtimenews/公告\`](https://rsshub.app/10jqka/realtimenews/公告)。 + + 若订阅 [7×24小时要闻直播](https://news.10jqka.com.cn/realtimenews.html) 的 \`公告\` 和 \`A股\` 标签。将 \`公告,A股\` 作为标签参数填入,此时路由为 [\`/10jqka/realtimenews/公告,A股\`](https://rsshub.app/10jqka/realtimenews/公告,A股)。 +::: + +| 全部 | 重要 | A股 | 港股 | 美股 | 机会 | 异动 | 公告 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | + `, + categories: ['finance'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + title: '全部', + source: ['news.10jqka.com.cn/realtimenews.html'], + target: '/realtimenews/全部', + }, + { + title: '重要', + source: ['news.10jqka.com.cn/realtimenews.html'], + target: '/realtimenews/重要', + }, + { + title: 'A股', + source: ['news.10jqka.com.cn/realtimenews.html'], + target: '/realtimenews/A股', + }, + { + title: '港股', + source: ['news.10jqka.com.cn/realtimenews.html'], + target: '/realtimenews/港股', + }, + { + title: '美股', + source: ['news.10jqka.com.cn/realtimenews.html'], + target: '/realtimenews/美股', + }, + { + title: '机会', + source: ['news.10jqka.com.cn/realtimenews.html'], + target: '/realtimenews/机会', + }, + { + title: '异动', + source: ['news.10jqka.com.cn/realtimenews.html'], + target: '/realtimenews/异动', + }, + { + title: '公告', + source: ['news.10jqka.com.cn/realtimenews.html'], + target: '/realtimenews/公告', + }, + ], +}; diff --git a/lib/routes/12306/namespace.ts b/lib/routes/12306/namespace.ts index e7b13a5dfe36ad..7f402d08444a27 100644 --- a/lib/routes/12306/namespace.ts +++ b/lib/routes/12306/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '12306', url: 'kyfw.12306.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/12306/zxdt.ts b/lib/routes/12306/zxdt.ts index c2e5f199db9192..69b6696bba0974 100644 --- a/lib/routes/12306/zxdt.ts +++ b/lib/routes/12306/zxdt.ts @@ -2,7 +2,7 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; -import * as url from 'node:url'; +import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/zxdt/:id?', @@ -40,41 +40,23 @@ async function handler(ctx) { const name = $('div.nav_center > a:nth-child(4)').text(); const list = $('#newList > ul > li') - .map(function () { - const info = { - title: $(this).find('a').text(), - link: $(this).find('a').attr('href'), - date: $(this).find('span').text().slice(1, -1), - }; - return info; - }) - .get(); + .toArray() + .map((item) => ({ + title: $(item).find('a').text(), + link: new URL($(item).find('a').attr('href'), link).href, + pubDate: parseDate($(item).find('span').text().slice(1, -1)), + })); const out = await Promise.all( - list.map(async (info) => { - const title = info.title; - const date = info.date; - const itemUrl = url.resolve(link, info.link); + list.map((info) => + cache.tryGet(info.link, async () => { + const response = await got.get(info.link); + const $ = load(response.data); + info.description = $('.article-box').html() || $('.content_text').html() || '文章已被删除'; - const cacheIn = await cache.get(itemUrl); - if (cacheIn) { - return JSON.parse(cacheIn); - } - - const response = await got.get(itemUrl); - const $ = load(response.data); - let description = $('.article-box').html(); - description = description ? description.replaceAll('src="', `src="${url.resolve(itemUrl, '.')}`).trim() : $('.content_text').html() || '文章已被删除'; - - const single = { - title, - link: itemUrl, - description, - pubDate: new Date(date).toUTCString(), - }; - cache.set(itemUrl, JSON.stringify(single)); - return single; - }) + return info; + }) + ) ); return { diff --git a/lib/routes/12371/namespace.ts b/lib/routes/12371/namespace.ts index 1d2b130936da62..c6ad54d95c212f 100644 --- a/lib/routes/12371/namespace.ts +++ b/lib/routes/12371/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: '共产党员网', url: 'www.12371.cn', categories: ['government'], + lang: 'zh-CN', }; diff --git a/lib/routes/12371/zxfb.ts b/lib/routes/12371/zxfb.ts index cb87d363c9e364..756779f6fd143b 100644 --- a/lib/routes/12371/zxfb.ts +++ b/lib/routes/12371/zxfb.ts @@ -59,6 +59,6 @@ export const route: Route = { handler, url: 'www.12371.cn', description: `| 最新发布 | - | :------: | - | zxfb |`, +| :------: | +| zxfb |`, }; diff --git a/lib/routes/141jav/index.ts b/lib/routes/141jav/index.ts index 85d9b1026dcd3d..b7f58edad2498b 100644 --- a/lib/routes/141jav/index.ts +++ b/lib/routes/141jav/index.ts @@ -10,49 +10,53 @@ import { art } from '@/utils/render'; import path from 'node:path'; export const route: Route = { - path: '/{.*}?', + path: '/:type/:keyword{.*}?', categories: ['multimedia'], name: '通用', maintainers: ['cgkings', 'nczitzk'], + parameters: { type: '类型,可查看下表的类型说明', keyword: '关键词,可查看下表的关键词说明' }, + handler, description: `**类型** - | 最新 | 热门 | 随机 | 指定演员 | 指定标签 | - | ---- | ------- | ------ | -------- | -------- | - | new | popular | random | actress | tag | +| 最新 | 热门 | 随机 | 指定演员 | 指定标签 | 日期 | +| ---- | ------- | ------ | -------- | -------- | ---- | +| new | popular | random | actress | tag | date | - **关键词** +**关键词** - | 空 | 日期范围 | 演员名 | 标签名 | - | -- | ----------- | ------------ | -------------- | - | | 7 / 30 / 60 | Yua%20Mikami | Adult%20Awards | +| 空 | 日期范围 | 演员名 | 标签名 | 年月日 | +| -- | ----------- | ------------ | -------------- | ---------- | +| | 7 / 30 / 60 | Yua%20Mikami | Adult%20Awards | 2020/07/30 | - **示例说明** +**示例说明** - - \`/141jav/new\` +- \`/141jav/new\` - 仅当类型为 \`new\` \`popular\` 或 \`random\` 时关键词为 **空** + 仅当类型为 \`new\` \`popular\` 或 \`random\` 时关键词为 **空** - - \`/141jav/popular/30\` +- \`/141jav/popular/30\` - \`popular\` \`random\` 类型的关键词可填写 \`7\` \`30\` 或 \`60\` 三个 **日期范围** 之一,分别对应 **7 天**、**30 天** 或 **60 天内** + \`popular\` \`random\` 类型的关键词可填写 \`7\` \`30\` 或 \`60\` 三个 **日期范围** 之一,分别对应 **7 天**、**30 天** 或 **60 天内** - - \`/141jav/actress/Yua%20Mikami\` +- \`/141jav/actress/Yua%20Mikami\` - \`actress\` 类型的关键词必须填写 **演员名** ,可在 [此处](https://141jav.com/actress) 演员单页链接中获取 + \`actress\` 类型的关键词必须填写 **演员名** ,可在 [此处](https://141jav.com/actress/) 演员单页链接中获取 - - \`/141jav/tag/Adult%20Awards\` +- \`/141jav/tag/Adult%20Awards\` - \`tag\` 类型的关键词必须填写 **标签名** 且标签中的 \`/\` 必须替换为 \`%2F\` ,可在 [此处](https://141jav.com/tag) 标签单页链接中获取 + \`tag\` 类型的关键词必须填写 **标签名** 且标签中的 \`/\` 必须替换为 \`%2F\` ,可在 [此处](https://141jav.com/tag/) 标签单页链接中获取 - - \`/141jav/date/2020/07/30\` +- \`/141jav/date/2020/07/30\` - \`date\` 类型的关键词必须填写 **日期**`, - handler, + \`date\` 类型的关键词必须填写 **日期(年/月/日)**`, }; async function handler(ctx) { const rootUrl = 'https://www.141jav.com'; - const currentUrl = `${rootUrl}${getSubPath(ctx)}`; + const type = ctx.req.param('type'); + const keyword = ctx.req.param('keyword') ?? ''; + + const currentUrl = `${rootUrl}/${type}${keyword ? `/${keyword}` : ''}`; const response = await got({ method: 'get', @@ -62,7 +66,7 @@ async function handler(ctx) { const $ = load(response.data); if (getSubPath(ctx) === '/') { - ctx.redirect(`/141jav${$('.overview').first().attr('href')}`); + ctx.set('redirect', `/141jav${$('.overview').first().attr('href')}`); return; } diff --git a/lib/routes/141jav/namespace.ts b/lib/routes/141jav/namespace.ts index 0c6a56916bcb33..e114d671eb2872 100644 --- a/lib/routes/141jav/namespace.ts +++ b/lib/routes/141jav/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '141JAV', url: '141jav.com', - description: `:::tip + description: `::: tip 官方提供的订阅源不支持 BT 下载订阅,地址为 [https://141jav.com/feeds/](https://141jav.com/feeds/) :::`, + lang: 'en', }; diff --git a/lib/routes/141ppv/index.ts b/lib/routes/141ppv/index.ts index cf2d42c39914aa..fbf1239b4c7146 100644 --- a/lib/routes/141ppv/index.ts +++ b/lib/routes/141ppv/index.ts @@ -10,49 +10,53 @@ import { art } from '@/utils/render'; import path from 'node:path'; export const route: Route = { - path: '/{.*}?', + path: '/:type/:keyword{.*}?', categories: ['multimedia'], name: '通用', maintainers: ['cgkings', 'nczitzk'], + parameters: { type: '类型,可查看下表的类型说明', keyword: '关键词,可查看下表的关键词说明' }, handler, description: `**类型** - | 最新 | 热门 | 随机 | 指定演员 | 指定标签 | - | ---- | ------- | ------ | -------- | -------- | - | new | popular | random | actress | tag | +| 最新 | 热门 | 随机 | 指定演员 | 指定标签 | 日期 | +| ---- | ------- | ------ | -------- | -------- | ---- | +| new | popular | random | actress | tag | date | - **关键词** +**关键词** - | 空 | 日期范围 | 演员名 | 标签名 | - | -- | ----------- | ------------ | -------------- | - | | 7 / 30 / 60 | Yua%20Mikami | Adult%20Awards | +| 空 | 日期范围 | 演员名 | 标签名 | 年月日 | +| -- | ----------- | ------------ | -------------- | ---------- | +| | 7 / 30 / 60 | Yua%20Mikami | Adult%20Awards | 2020/07/30 | - **示例说明** +**示例说明** - - \`/141ppv/new\` +- \`/141ppv/new\` - 仅当类型为 \`new\` \`popular\` 或 \`random\` 时关键词为 **空** + 仅当类型为 \`new\` \`popular\` 或 \`random\` 时关键词为 **空** - - \`/141ppv/popular/30\` +- \`/141ppv/popular/30\` - \`popular\` \`random\` 类型的关键词可填写 \`7\` \`30\` 或 \`60\` 三个 **日期范围** 之一,分别对应 **7 天**、**30 天** 或 **60 天内** + \`popular\` \`random\` 类型的关键词可填写 \`7\` \`30\` 或 \`60\` 三个 **日期范围** 之一,分别对应 **7 天**、**30 天** 或 **60 天内** - - \`/141ppv/actress/Yua%20Mikami\` +- \`/141ppv/actress/Yua%20Mikami\` - \`actress\` 类型的关键词必须填写 **演员名** ,可在 [此处](https://141ppv.com/actress) 演员单页链接中获取 + \`actress\` 类型的关键词必须填写 **演员名** ,可在 [此处](https://141ppv.com/actress/) 演员单页链接中获取 - - \`/141ppv/tag/Adult%20Awards\` +- \`/141ppv/tag/Adult%20Awards\` - \`tag\` 类型的关键词必须填写 **标签名** 且标签中的 \`/\` 必须替换为 \`%2F\` ,可在 [此处](https://141ppv.com/tag) 标签单页链接中获取 + \`tag\` 类型的关键词必须填写 **标签名** 且标签中的 \`/\` 必须替换为 \`%2F\` ,可在 [此处](https://141ppv.com/tag/) 标签单页链接中获取 - - \`/141ppv/date/2020/07/30\` +- \`/141ppv/date/2020/07/30\` - \`date\` 类型的关键词必须填写 **日期**`, + \`date\` 类型的关键词必须填写 **日期(年/月/日)**`, }; async function handler(ctx) { const rootUrl = 'https://www.141ppv.com'; - const currentUrl = `${rootUrl}${getSubPath(ctx)}`; + const type = ctx.req.param('type'); + const keyword = ctx.req.param('keyword') ?? ''; + + const currentUrl = `${rootUrl}/${type}${keyword ? `/${keyword}` : ''}`; const response = await got({ method: 'get', @@ -62,7 +66,7 @@ async function handler(ctx) { const $ = load(response.data); if (getSubPath(ctx) === '/') { - ctx.redirect(`/141ppv${$('.overview').first().attr('href')}`); + ctx.set('redirect', `/141ppv${$('.overview').first().attr('href')}`); return; } diff --git a/lib/routes/141ppv/namespace.ts b/lib/routes/141ppv/namespace.ts index 73c2292c5b8974..2147332b3b6f57 100644 --- a/lib/routes/141ppv/namespace.ts +++ b/lib/routes/141ppv/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '141PPV', url: '141ppv.com', - description: `:::tip + description: `::: tip 官方提供的订阅源不支持 BT 下载订阅,地址为 [https://141ppv.com/feeds/](https://141ppv.com/feeds/) :::`, + lang: 'en', }; diff --git a/lib/routes/163/exclusive.ts b/lib/routes/163/exclusive.ts index 3a6a6ade0f007a..715fbbf05921be 100644 --- a/lib/routes/163/exclusive.ts +++ b/lib/routes/163/exclusive.ts @@ -99,23 +99,23 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 分类 | 编号 | - | -------- | ---- | - | 首页 | | - | 轻松一刻 | qsyk | - | 槽值 | cz | - | 人间 | rj | - | 大国小民 | dgxm | - | 三三有梗 | ssyg | - | 数读 | sd | - | 看客 | kk | - | 下划线 | xhx | - | 谈心社 | txs | - | 哒哒 | dd | - | 胖编怪聊 | pbgl | - | 曲一刀 | qyd | - | 今日之声 | jrzs | - | 浪潮 | lc | - | 沸点 | fd |`, +| -------- | ---- | +| 首页 | | +| 轻松一刻 | qsyk | +| 槽值 | cz | +| 人间 | rj | +| 大国小民 | dgxm | +| 三三有梗 | ssyg | +| 数读 | sd | +| 看客 | kk | +| 下划线 | xhx | +| 谈心社 | txs | +| 哒哒 | dd | +| 胖编怪聊 | pbgl | +| 曲一刀 | qyd | +| 今日之声 | jrzs | +| 浪潮 | lc | +| 沸点 | fd |`, }; async function handler(ctx) { diff --git a/lib/routes/163/music/djradio.ts b/lib/routes/163/music/djradio.ts index c735a47e6823bc..665cbd3fddab11 100644 --- a/lib/routes/163/music/djradio.ts +++ b/lib/routes/163/music/djradio.ts @@ -6,12 +6,14 @@ import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; import path from 'node:path'; +import cache from '@/utils/cache'; +import { config } from '@/config'; export const route: Route = { - path: '/music/djradio/:id', + path: '/music/djradio/:id/:info?', categories: ['multimedia'], example: '/163/music/djradio/347317067', - parameters: { id: '节目 id, 可在电台节目页 URL 中找到' }, + parameters: { id: '节目 id, 可在电台节目页 URL 中找到', info: '默认在正文尾部显示节目相关信息,任意值为不显示' }, features: { requireConfig: false, requirePuppeteer: false, @@ -25,35 +27,39 @@ export const route: Route = { handler, }; +const ProcessFeed = (id, limit, offset) => + cache.tryGet( + `163:music:djradio:${id}:${limit}:${offset}`, + async () => + await got.post('https://music.163.com/api/dj/program/byradio', { + headers: { + Referer: 'https://music.163.com/', + }, + form: { + radioId: id, + limit, + offset, + }, + }), + config.cache.routeExpire, + false + ); + async function handler(ctx) { const id = ctx.req.param('id'); + const info = !ctx.req.param('info'); - const ProcessFeed = (limit, offset) => - got.post('https://music.163.com/api/dj/program/byradio', { - headers: { - Referer: 'https://music.163.com/', - }, - form: { - radioId: id, - limit, - offset, - }, - }); - - const response = await ProcessFeed(1, 0); + const response = await ProcessFeed(id, 1, 0); const programs = response.data.programs || []; const { radio, dj } = programs[0] || { radio: {}, dj: {} }; const count = response.data.count || 0; - const countPage = []; - for (let i = 0; i < Math.ceil(count / 500); i++) { - countPage.push(i); - } + const countPage = Array.from({ length: Math.ceil(count / 500) }, (_, i) => i); const items = await Promise.all( countPage.map(async (item) => { - const response = await ProcessFeed(500, item * 500); + const response = await ProcessFeed(id, 500, item * 500); const programs = response.data.programs || []; const list = programs.map((pg) => { const description = (pg.description || '').split('\n').map((p) => p); @@ -64,6 +70,7 @@ async function handler(ctx) { pg, description, itunes_duration: mm_ss_duration, + info, }); return { diff --git a/lib/routes/163/music/playlist.ts b/lib/routes/163/music/playlist.ts index 350321408902b5..4c59cb0ca74f13 100644 --- a/lib/routes/163/music/playlist.ts +++ b/lib/routes/163/music/playlist.ts @@ -67,7 +67,8 @@ async function handler(ctx) { date: new Date(thisSong.album.publishTime).toLocaleDateString(), picUrl: thisSong.album.picUrl, }), - link: `https://music.163.com/#/song?id=${item.id}`, + link: `https://music.163.com/song?id=${item.id}`, + guid: `https://music.163.com/#/song?id=${item.id}`, pubDate: new Date(item.at), author: singer, }; diff --git a/lib/routes/163/namespace.ts b/lib/routes/163/namespace.ts index 4005a73ca1d6f1..13478eae1a348f 100644 --- a/lib/routes/163/namespace.ts +++ b/lib/routes/163/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '网易公开课', url: '163.com', - description: `:::tip + description: `::: tip 部分歌单及听歌排行信息为登陆后可见,自建时将环境变量\`NCM_COOKIES\`设为登陆后的 Cookie 值,即可正常获取。 :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/163/news/rank.ts b/lib/routes/163/news/rank.ts index 2c83f7064e4f45..7c4c95f26b471e 100644 --- a/lib/routes/163/news/rank.ts +++ b/lib/routes/163/news/rank.ts @@ -98,19 +98,19 @@ export const route: Route = { name: '排行榜', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 全站新闻 **点击榜** 的统计时间仅包含 “24 小时”、“本周”、“本月”,不包含 “1 小时”。即可用的\`time\`参数为\`day\`、\`week\`、\`month\`。 其他分类 **点击榜** 的统计时间仅包含 “1 小时”、“24 小时”、“本周”。即可用的\`time\`参数为\`hour\`、\`day\`、\`week\`。 而所有分类(包括全站)的 **跟贴榜** 的统计时间皆仅包含 “24 小时”、“本周”、“本月”。即可用的\`time\`参数为\`day\`、\`week\`、\`month\`。 - ::: +::: 新闻分类: - | 全站 | 新闻 | 娱乐 | 体育 | 财经 | 科技 | 汽车 | 女人 | 房产 | 游戏 | 旅游 | 教育 | - | ----- | ---- | ------------- | ------ | ----- | ---- | ---- | ---- | ----- | ---- | ------ | ---- | - | whole | news | entertainment | sports | money | tech | auto | lady | house | game | travel | edu |`, +| 全站 | 新闻 | 娱乐 | 体育 | 财经 | 科技 | 汽车 | 女人 | 房产 | 游戏 | 旅游 | 教育 | +| ----- | ---- | ------------- | ------ | ----- | ---- | ---- | ---- | ----- | ---- | ------ | ---- | +| whole | news | entertainment | sports | money | tech | auto | lady | house | game | travel | edu |`, }; async function handler(ctx) { @@ -135,7 +135,7 @@ async function handler(ctx) { const $ = load(iconv.decode(response.data, 'gbk')); const list = $('div.tabContents') - .eq(timeRange[time].index + (category === 'whole' ? (type === 'click' ? -1 : 2) : type === 'click' ? 0 : 2)) + .eq(timeRange[time].index + (category === 'whole' ? (type === 'click' ? -1 : 2) : (type === 'click' ? 0 : 2))) .find('table tbody tr td a') .toArray() .map((item) => { diff --git a/lib/routes/163/news/special.ts b/lib/routes/163/news/special.ts index c80244353cfa99..0cebb6b248b983 100644 --- a/lib/routes/163/news/special.ts +++ b/lib/routes/163/news/special.ts @@ -38,8 +38,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 轻松一刻 | 槽值 | 人间 | 大国小民 | 三三有梗 | 数读 | 看客 | 下划线 | 谈心社 | 哒哒 | 胖编怪聊 | 曲一刀 | 今日之声 | 浪潮 | 沸点 | - | -------- | ---- | ---- | -------- | -------- | ---- | ---- | ------ | ------ | ---- | -------- | ------ | -------- | ---- | ---- | - | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |`, +| -------- | ---- | ---- | -------- | -------- | ---- | ---- | ------ | ------ | ---- | -------- | ------ | -------- | ---- | ---- | +| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |`, }; async function handler(ctx) { diff --git a/lib/routes/163/renjian.ts b/lib/routes/163/renjian.ts index c7bcaa17a7902e..21437ee8383b3c 100644 --- a/lib/routes/163/renjian.ts +++ b/lib/routes/163/renjian.ts @@ -36,8 +36,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 特写 | 记事 | 大写 | 好读 | 看客 | - | ----- | ----- | ----- | ----- | ----- | - | texie | jishi | daxie | haodu | kanke |`, +| ----- | ----- | ----- | ----- | ----- | +| texie | jishi | daxie | haodu | kanke |`, }; async function handler(ctx) { diff --git a/lib/routes/163/templates/music/djradio-content.art b/lib/routes/163/templates/music/djradio-content.art index 1db4ef731c2590..a27f0b666ffa5d 100644 --- a/lib/routes/163/templates/music/djradio-content.art +++ b/lib/routes/163/templates/music/djradio-content.art @@ -4,8 +4,10 @@

{{$value}}

{{/each}} +{{ if info }}

时长: {{itunes_duration}}

查看节目

+{{ /if }} diff --git a/lib/routes/163/today.ts b/lib/routes/163/today.ts index acf5dff1c707a5..21c0c937246a92 100644 --- a/lib/routes/163/today.ts +++ b/lib/routes/163/today.ts @@ -28,9 +28,9 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'wp.m.163.com/163/html/newsapp/todayFocus/index.html', - description: `:::tip + description: `::: tip 参数 **需要获取全文** 设置为 \`true\` \`yes\` \`t\` \`y\` 等值后,RSS 会携带该新闻条目的对应全文。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/163/utils.ts b/lib/routes/163/utils.ts index eab4f07629ed03..7a2e95cbab1ec0 100644 --- a/lib/routes/163/utils.ts +++ b/lib/routes/163/utils.ts @@ -24,9 +24,9 @@ const parseDyArticle = (item, tryGet) => } }); - const imgUrl = new URL(item.imgsrc); + const imgsrc = item.imgsrc ? new URL(item.imgsrc).searchParams.get('url') : false; item.description = art(path.join(__dirname, 'templates/dy.art'), { - imgsrc: imgUrl.searchParams.get('url'), + imgsrc, postBody: $('.post_body').html(), }); diff --git a/lib/routes/18comic/album.ts b/lib/routes/18comic/album.ts index c0217927d221ea..1cbf1725183907 100644 --- a/lib/routes/18comic/album.ts +++ b/lib/routes/18comic/album.ts @@ -28,9 +28,9 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'jmcomic.group/', - description: `:::tip + description: `::: tip 专辑 id 不包括 URL 中标题的部分。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/18comic/blogs.ts b/lib/routes/18comic/blogs.ts index 7d2814d72e943b..1ccfe302ca5cf6 100644 --- a/lib/routes/18comic/blogs.ts +++ b/lib/routes/18comic/blogs.ts @@ -30,9 +30,9 @@ export const route: Route = { url: 'jmcomic.group/', description: `分类 - | 全部 | 紳夜食堂 | 遊戲文庫 | JG GAMES | 模型山下 | - | ---- | -------- | -------- | -------- | -------- | - | | dinner | raiders | jg | figure |`, +| 全部 | 紳夜食堂 | 遊戲文庫 | JG GAMES | 模型山下 | +| ---- | -------- | -------- | -------- | -------- | +| | dinner | raiders | jg | figure |`, }; async function handler(ctx) { diff --git a/lib/routes/18comic/index.ts b/lib/routes/18comic/index.ts index 404038b8ee0a04..f26042f437ad98 100644 --- a/lib/routes/18comic/index.ts +++ b/lib/routes/18comic/index.ts @@ -25,26 +25,26 @@ export const route: Route = { url: 'jmcomic.group/', description: `分类 - | 全部 | 其他漫畫 | 同人 | 韓漫 | 美漫 | 短篇 | 单本 | - | ---- | -------- | ------ | ------ | ------ | ----- | ------ | - | all | another | doujin | hanman | meiman | short | single | +| 全部 | 其他漫畫 | 同人 | 韓漫 | 美漫 | 短篇 | 单本 | +| ---- | -------- | ------ | ------ | ------ | ----- | ------ | +| all | another | doujin | hanman | meiman | short | single | 时间范围 - | 全部 | 今天 | 这周 | 本月 | - | ---- | ---- | ---- | ---- | - | a | t | w | m | +| 全部 | 今天 | 这周 | 本月 | +| ---- | ---- | ---- | ---- | +| a | t | w | m | 排列顺序 - | 最新 | 最多点阅的 | 最多图片 | 最高评分 | 最多评论 | 最多爱心 | - | ---- | ---------- | -------- | -------- | -------- | -------- | - | mr | mv | mp | tr | md | tf | +| 最新 | 最多点阅的 | 最多图片 | 最高评分 | 最多评论 | 最多爱心 | +| ---- | ---------- | -------- | -------- | -------- | -------- | +| mr | mv | mp | tr | md | tf | 关键字(供参考) - | YAOI | 女性向 | NTR | 非 H | 3D | 獵奇 | - | ---- | ------ | --- | ---- | -- | ---- |`, +| YAOI | 女性向 | NTR | 非 H | 3D | 獵奇 | +| ---- | ------ | --- | ---- | -- | ---- |`, }; async function handler(ctx) { diff --git a/lib/routes/18comic/namespace.ts b/lib/routes/18comic/namespace.ts index e910cb8fb0a7c8..035a80d5a742ae 100644 --- a/lib/routes/18comic/namespace.ts +++ b/lib/routes/18comic/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '禁漫天堂', url: '18comic.org', - description: `:::tip + description: `::: tip 禁漫天堂有多个备用域名,本路由默认使用域名 \`https://jmcomic.me\`,若该域名无法访问,可以通过在路由最后加上 \`?domain=<域名>\` 指定路由访问的域名。如指定备用域名为 \`https://jmcomic1.me\`,则在所有禁漫天堂路由最后加上 \`?domain=jmcomic1.me\` 即可,此时路由为 [\`/18comic?domain=jmcomic1.me\`](https://rsshub.app/18comic?domain=jmcomic1.me) :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/18comic/search.ts b/lib/routes/18comic/search.ts index a8aa8a09de2cba..66f9e5af4f3383 100644 --- a/lib/routes/18comic/search.ts +++ b/lib/routes/18comic/search.ts @@ -30,9 +30,9 @@ export const route: Route = { maintainers: [], handler, url: 'jmcomic.group/', - description: `:::tip + description: `::: tip 关键字必须超过两个字,这是来自网站的限制。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/19lou/index.ts b/lib/routes/19lou/index.ts index 25b092aca0f109..5869a5f356960a 100644 --- a/lib/routes/19lou/index.ts +++ b/lib/routes/19lou/index.ts @@ -34,20 +34,20 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 杭州 | 台州 | 嘉兴 | 宁波 | 湖州 | - | ---- | ------- | ------- | ------ | ------ | - | www | taizhou | jiaxing | ningbo | huzhou | +| ---- | ------- | ------- | ------ | ------ | +| www | taizhou | jiaxing | ningbo | huzhou | - | 绍兴 | 湖州 | 温州 | 金华 | 舟山 | - | -------- | ------ | ------- | ------ | -------- | - | shaoxing | huzhou | wenzhou | jinhua | zhoushan | +| 绍兴 | 湖州 | 温州 | 金华 | 舟山 | +| -------- | ------ | ------- | ------ | -------- | +| shaoxing | huzhou | wenzhou | jinhua | zhoushan | - | 衢州 | 丽水 | 义乌 | 萧山 | 余杭 | - | ------ | ------ | ---- | -------- | ------ | - | quzhou | lishui | yiwu | xiaoshan | yuhang | +| 衢州 | 丽水 | 义乌 | 萧山 | 余杭 | +| ------ | ------ | ---- | -------- | ------ | +| quzhou | lishui | yiwu | xiaoshan | yuhang | - | 临安 | 富阳 | 桐庐 | 建德 | 淳安 | - | ----- | ------ | ------ | ------ | ------ | - | linan | fuyang | tonglu | jiande | chunan |`, +| 临安 | 富阳 | 桐庐 | 建德 | 淳安 | +| ----- | ------ | ------ | ------ | ------ | +| linan | fuyang | tonglu | jiande | chunan |`, }; async function handler(ctx) { diff --git a/lib/routes/19lou/namespace.ts b/lib/routes/19lou/namespace.ts index fdb482029831cd..727582559cd484 100644 --- a/lib/routes/19lou/namespace.ts +++ b/lib/routes/19lou/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '19 楼', url: '19lou.com', + lang: 'zh-CN', }; diff --git a/lib/routes/1lou/index.ts b/lib/routes/1lou/index.ts index 0d3f16267b3c2c..df177c91242a97 100644 --- a/lib/routes/1lou/index.ts +++ b/lib/routes/1lou/index.ts @@ -114,7 +114,7 @@ export const route: Route = { handler, example: '/1lou/forum-2-1', parameters: { params: '路径参数,可以在对应页面的 URL 中找到' }, - description: `:::tip + description: `::: tip \`1lou.me/\` 后的内容填入 params 参数,以下是几个例子: 若订阅 [大陆电视剧](https://www.1lou.me/forum-2-1.htm?tagids=0_97_0_0),网址为 \`https://www.1lou.me/forum-2-1.htm?tagids=0_97_0_0\`。截取 \`https://www.1lou.me/\` 到末尾 \`.htm\` 的部分 \`forum-2-1\` 作为参数,并补充 \`tagids\`,此时路由为 [\`/1lou/forum-2-1?tagids=0_97_0_0\`](https://rsshub.app/1lou/forum-2-1?tagids=0_97_0_0)。 @@ -122,7 +122,7 @@ export const route: Route = { 若订阅 [最新发帖电视剧](https://www.1lou.me/forum-2-1.htm?orderby=tid&digest=0),网址为 \`https://www.1lou.me/forum-2-1.htm?orderby=tid&digest=0\`。截取 \`https://www.1lou.me/\` 到末尾 \`.htm\` 的部分 \`forum-2-1\` 作为参数,并补充 \`orderby\`,此时路由为 [\`/1lou/forum-2-1?orderby=tid\`](https://rsshub.app/1lou/forum-2-1?orderby=tid)。 若订阅 [搜素繁花主题贴](https://www.1lou.me/search-_E7_B9_81_E8_8A_B1-1.htm),网址为 \`https://www.1lou.me/search-_E7_B9_81_E8_8A_B1-1.htm\`。截取 \`https://www.1lou.me/\` 到末尾 \`.htm\` 的部分 \`search-_E7_B9_81_E8_8A_B1-1\` 作为参数,此时路由为 [\`/1lou/search-_E7_B9_81_E8_8A_B1-1\`](https://rsshub.app/1lou/search-_E7_B9_81_E8_8A_B1-1)。 - :::`, +:::`, categories: ['multimedia'], features: { diff --git a/lib/routes/1lou/namespace.ts b/lib/routes/1lou/namespace.ts index 03f3b6c0adb5be..f4a5cbf8b08ed0 100644 --- a/lib/routes/1lou/namespace.ts +++ b/lib/routes/1lou/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: '1lou.me', categories: ['multimedia'], description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/1point3acres/blog.ts b/lib/routes/1point3acres/blog.ts index 705473222769bd..3e1c0f4be0d302 100644 --- a/lib/routes/1point3acres/blog.ts +++ b/lib/routes/1point3acres/blog.ts @@ -25,8 +25,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 留学申请 | 找工求职 | 生活攻略 | 投资理财 | 签证移民 | 时政要闻 | - | ---------- | -------- | --------- | -------- | -------- | -------- | - | studyinusa | career | lifestyle | invest | visa | news |`, +| ---------- | -------- | --------- | -------- | -------- | -------- | +| studyinusa | career | lifestyle | invest | visa | news |`, }; async function handler(ctx) { diff --git a/lib/routes/1point3acres/category.ts b/lib/routes/1point3acres/category.ts index 93c401ec3c18a3..e39a5a566d98ed 100644 --- a/lib/routes/1point3acres/category.ts +++ b/lib/routes/1point3acres/category.ts @@ -23,21 +23,21 @@ export const route: Route = { name: '标签', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 更多标签可以在 [标签列表](https://instant.1point3acres.com/tags) 中找到。 - ::: +::: 分类 - | 热门帖子 | 最新帖子 | - | -------- | -------- | - | hot | new | +| 热门帖子 | 最新帖子 | +| -------- | -------- | +| hot | new | 排序方式 - | 最新回复 | 最新发布 | - | -------- | -------- | - | | post |`, +| 最新回复 | 最新发布 | +| -------- | -------- | +| | post |`, }; async function handler(ctx) { diff --git a/lib/routes/1point3acres/namespace.ts b/lib/routes/1point3acres/namespace.ts index f1af63dc713e44..86131f5ca7ba7b 100644 --- a/lib/routes/1point3acres/namespace.ts +++ b/lib/routes/1point3acres/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '一亩三分地', url: 'blog.1point3acres.com', + lang: 'zh-CN', }; diff --git a/lib/routes/1point3acres/offer.ts b/lib/routes/1point3acres/offer.ts index 6462d801336aa8..8bc81e45975e89 100644 --- a/lib/routes/1point3acres/offer.ts +++ b/lib/routes/1point3acres/offer.ts @@ -30,14 +30,14 @@ export const route: Route = { maintainers: ['EthanWng97'], handler, url: 'offer.1point3acres.com/', - description: `:::tip 三个 id 获取方式 + description: `::: tip 三个 id 获取方式 1. 打开 [https://offer.1point3acres.com](https://offer.1point3acres.com) 2. 打开控制台 3. 切换到 Network 面板 4. 点击 搜索 按钮 5. 点击 results?ps=15\&pg=1 POST 请求 6. 找到 Request Payload 请求参数,例如 \`filters: {planyr: "13", planmajor: "1", outname_w: "ACADIAU"}\` ,则三个 id 分别为: 13,1,ACADIAU - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/1point3acres/section.ts b/lib/routes/1point3acres/section.ts index 3b06513764b769..468caafb5168f1 100644 --- a/lib/routes/1point3acres/section.ts +++ b/lib/routes/1point3acres/section.ts @@ -36,28 +36,28 @@ export const route: Route = { handler, description: `分区 - | 分区 | id | - | -------- | --- | - | 留学申请 | 257 | - | 世界公民 | 379 | - | 投资理财 | 400 | - | 生活干货 | 31 | - | 职场达人 | 345 | - | 人际关系 | 391 | - | 海外求职 | 38 | - | 签证移民 | 265 | +| 分区 | id | +| -------- | --- | +| 留学申请 | 257 | +| 世界公民 | 379 | +| 投资理财 | 400 | +| 生活干货 | 31 | +| 职场达人 | 345 | +| 人际关系 | 391 | +| 海外求职 | 38 | +| 签证移民 | 265 | 分类 - | 热门帖子 | 最新帖子 | - | -------- | -------- | - | hot | new | +| 热门帖子 | 最新帖子 | +| -------- | -------- | +| hot | new | 排序方式 - | 最新回复 | 最新发布 | - | -------- | -------- | - | | post |`, +| 最新回复 | 最新发布 | +| -------- | -------- | +| | post |`, }; async function handler(ctx) { diff --git a/lib/routes/1point3acres/thread.ts b/lib/routes/1point3acres/thread.ts index 6940d7536daa2b..3a15a5c0bc9a9c 100644 --- a/lib/routes/1point3acres/thread.ts +++ b/lib/routes/1point3acres/thread.ts @@ -3,7 +3,9 @@ import cache from '@/utils/cache'; import { rootUrl, apiRootUrl, types, ProcessThreads } from './utils'; export const route: Route = { - path: ['/post/:type?/:order?', '/thread/:type?/:order?'], + path: '/thread/:type?/:order?', + example: '/1point3acres/thread/hot', + parameters: { type: '帖子分类, 见下表,默认为 hot,即热门帖子', order: '排序方式,见下表,默认为空,即最新回复' }, name: '帖子', categories: ['bbs'], maintainers: ['EthanWng97', 'DIYgod', 'nczitzk'], @@ -11,15 +13,15 @@ export const route: Route = { url: 'instant.1point3acres.com/', description: `分类 - | 热门帖子 | 最新帖子 | - | -------- | -------- | - | hot | new | +| 热门帖子 | 最新帖子 | +| -------- | -------- | +| hot | new | 排序方式 - | 最新回复 | 最新发布 | - | -------- | -------- | - | | post |`, +| 最新回复 | 最新发布 | +| -------- | -------- | +| | post |`, }; async function handler(ctx) { diff --git a/lib/routes/1point3acres/utils.ts b/lib/routes/1point3acres/utils.ts index 01ecb09b045569..c90830d36644cc 100644 --- a/lib/routes/1point3acres/utils.ts +++ b/lib/routes/1point3acres/utils.ts @@ -5,7 +5,9 @@ import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; import path from 'node:path'; -import bbcode from 'bbcodejs'; +import type { BBobCoreTagNodeTree } from '@bbob/types'; +import bbobHTML from '@bbob/html'; +import presetHTML5 from '@bbob/preset-html5'; const rootUrl = 'https://instant.1point3acres.com'; const apiRootUrl = 'https://api.1point3acres.com'; @@ -15,6 +17,17 @@ const types = { hot: '热门帖子', }; +const swapLinebreak = (tree: BBobCoreTagNodeTree) => + tree.walk((node) => { + if (typeof node === 'string' && node === '\n') { + return { + tag: 'br', + content: null, + }; + } + return node; + }); + const ProcessThreads = async (tryGet, apiUrl, order) => { const response = await got({ method: 'get', @@ -24,8 +37,6 @@ const ProcessThreads = async (tryGet, apiUrl, order) => { }, }); - const bbcodeParser = new bbcode.Parser(); - const items = await Promise.all( response.data.threads.map((item) => { const result = { @@ -48,20 +59,73 @@ const ProcessThreads = async (tryGet, apiUrl, order) => { }, }); - const data = detailResponse.data; + const thread = detailResponse.data.thread; + + const customPreset = presetHTML5.extend((tags) => ({ + ...tags, + attach: (node, { render }) => { + const id = render(node.content); + const attachment = thread.attachment_list.find((a) => a.aid === Number.parseInt(id)); + + if (attachment.isimage) { + return { + tag: 'img', + attrs: { + src: attachment.url, + }, + }; + } + + return { + tag: 'a', + attrs: { + href: `https://www.1point3acres.com/bbs/plugin.php?id=attachcenter:page&aid=${id}`, + rel: 'noopener', + target: '_blank', + }, + content: `https://www.1point3acres.com/bbs/plugin.php?id=attachcenter:page&aid=${id}`, + }; + }, + url: (node) => { + const link = Object.keys(node.attrs as Record)[0]; + if (link.startsWith('https://link.1p3a.com/?url=')) { + const url = decodeURIComponent(link.replace('https://link.1p3a.com/?url=', '')); + return { + tag: 'a', + attrs: { + href: url, + rel: 'noopener', + target: '_blank', + }, + content: node.content, + }; + } + + return { + tag: 'a', + attrs: { + href: link, + rel: 'noopener', + target: '_blank', + }, + content: node.content, + }; + }, + })); - result.description = bbcodeParser.toHTML(data.thread.message_bbcode); + result.description = bbobHTML(thread.message_bbcode, [customPreset(), swapLinebreak]); - for (const a of data.thread.attachment_list) { - if (a.isimage === 1) { - result.description = result.description.replaceAll( - new RegExp(`\\[attach\\]${a.aid}\\[\\/attach\\]`, 'g'), - art(path.join(__dirname, 'templates/image.art'), { - url: a.url, - height: a.height, - width: a.width, - }) - ); + if (!thread.message_bbcode.includes('[attach]') && thread.attachment_list.length > 0) { + for (const a of thread.attachment_list) { + result.description += + a.isimage === 1 + ? '
' + + art(path.join(__dirname, 'templates/image.art'), { + url: a.url, + height: a.height, + width: a.width, + }) + : ''; } } } catch { diff --git a/lib/routes/1x/namespace.ts b/lib/routes/1x/namespace.ts index 4e8cc283eb4262..0a8fb7a22477aa 100644 --- a/lib/routes/1x/namespace.ts +++ b/lib/routes/1x/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: '1x.com', categories: ['design', 'picture'], description: '1x.com • In Pursuit of the Sublime. Browse 200,000 curated photos from photographers all over the world.', + lang: 'en', }; diff --git a/lib/routes/2023game/index.ts b/lib/routes/2023game/index.ts index 55d900fd096269..c54423178af74d 100644 --- a/lib/routes/2023game/index.ts +++ b/lib/routes/2023game/index.ts @@ -26,9 +26,9 @@ export const route: Route = { url: 'www.2023game.com/', description: `分类 - | PS4游戏 | switch游戏 | 3DS游戏 | PSV游戏 | Xbox360 | PS3游戏 | 世嘉MD/SS | PSP游戏 | PC周边 | 怀旧掌机 | 怀旧主机 | PS4教程 | PS4金手指 | switch金手指 | switch教程 | switch补丁 | switch主题 | switch存档 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | ps4 | sgame | 3ds | psv | jiaocheng | ps3yx | zhuji.md | zhangji.psp | pcgame | zhangji | zhuji | ps4.psjc | ps41.ps4pkg | nsaita.cundang | nsaita.pojie | nsaita.buding | nsaita.zhutie | nsaita.zhuti |`, +| PS4游戏 | switch游戏 | 3DS游戏 | PSV游戏 | Xbox360 | PS3游戏 | 世嘉MD/SS | PSP游戏 | PC周边 | 怀旧掌机 | 怀旧主机 | PS4教程 | PS4金手指 | switch金手指 | switch教程 | switch补丁 | switch主题 | switch存档 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| ps4 | sgame | 3ds | psv | jiaocheng | ps3yx | zhuji.md | zhangji.psp | pcgame | zhangji | zhuji | ps4.psjc | ps41.ps4pkg | nsaita.cundang | nsaita.pojie | nsaita.buding | nsaita.zhutie | nsaita.zhuti |`, }; async function handler(ctx: Context): Promise { diff --git a/lib/routes/2023game/namespace.ts b/lib/routes/2023game/namespace.ts index 0405df7cc98a7b..0a703ee2861230 100644 --- a/lib/routes/2023game/namespace.ts +++ b/lib/routes/2023game/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '游戏星辰', url: 'www.2023game.com', + lang: 'zh-CN', }; diff --git a/lib/routes/2048/index.ts b/lib/routes/2048/index.ts index f2e7752eb480df..db9644d7ace38e 100644 --- a/lib/routes/2048/index.ts +++ b/lib/routes/2048/index.ts @@ -3,7 +3,7 @@ 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'; @@ -27,66 +27,71 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 最新合集 | 亞洲無碼 | 日本騎兵 | 歐美新片 | 國內原創 | 中字原創 | 三級寫真 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | 3 | 4 | 5 | 13 | 15 | 16 | 18 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| 3 | 4 | 5 | 13 | 15 | 16 | 18 | - | 有碼.HD | 亞洲 SM.HD | 日韓 VR/3D | 歐美 VR/3D | S-cute / Mywife / G-area | - | ------- | ---------- | ---------- | ---------- | ------------------------ | - | 116 | 114 | 96 | 97 | 119 | +| 有碼.HD | 亞洲 SM.HD | 日韓 VR/3D | 歐美 VR/3D | S-cute / Mywife / G-area | +| ------- | ---------- | ---------- | ---------- | ------------------------ | +| 116 | 114 | 96 | 97 | 119 | - | 網友自拍 | 亞洲激情 | 歐美激情 | 露出偷窺 | 高跟絲襪 | 卡通漫畫 | 原創达人 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | 23 | 24 | 25 | 26 | 27 | 28 | 135 | +| 網友自拍 | 亞洲激情 | 歐美激情 | 露出偷窺 | 高跟絲襪 | 卡通漫畫 | 原創达人 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| 23 | 24 | 25 | 26 | 27 | 28 | 135 | - | 唯美清純 | 网络正妹 | 亞洲正妹 | 素人正妹 | COSPLAY | 女优情报 | Gif 动图 | - | -------- | -------- | -------- | -------- | ------- | -------- | -------- | - | 21 | 274 | 276 | 277 | 278 | 29 | | +| 唯美清純 | 网络正妹 | 亞洲正妹 | 素人正妹 | COSPLAY | 女优情报 | Gif 动图 | +| -------- | -------- | -------- | -------- | ------- | -------- | -------- | +| 21 | 274 | 276 | 277 | 278 | 29 | | - | 獨家拍攝 | 稀有首發 | 网络见闻 | 主播實錄 | 珍稀套圖 | 名站同步 | 实用漫画 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | 213 | 94 | 283 | 111 | 88 | 131 | 180 | +| 獨家拍攝 | 稀有首發 | 网络见闻 | 主播實錄 | 珍稀套圖 | 名站同步 | 实用漫画 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| 213 | 94 | 283 | 111 | 88 | 131 | 180 | - | 网盘二区 | 网盘三区 | 分享福利 | 国产精选 | 高清福利 | 高清首发 | 多挂原创 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | 72 | 272 | 195 | 280 | 79 | 216 | 76 | +| 网盘二区 | 网盘三区 | 分享福利 | 国产精选 | 高清福利 | 高清首发 | 多挂原创 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| 72 | 272 | 195 | 280 | 79 | 216 | 76 | - | 磁链迅雷 | 正片大片 | H-GAME | 有声小说 | 在线视频 | 在线快播影院 | - | -------- | -------- | ------ | -------- | -------- | ------------ | - | 43 | 67 | 66 | 55 | 78 | 279 | +| 磁链迅雷 | 正片大片 | H-GAME | 有声小说 | 在线视频 | 在线快播影院 | +| -------- | -------- | ------ | -------- | -------- | ------------ | +| 43 | 67 | 66 | 55 | 78 | 279 | - | 综合小说 | 人妻意淫 | 乱伦迷情 | 长篇连载 | 文学作者 | TXT 小说打包 | - | -------- | -------- | -------- | -------- | -------- | ------------ | - | 48 | 103 | 50 | 54 | 100 | 109 | +| 综合小说 | 人妻意淫 | 乱伦迷情 | 长篇连载 | 文学作者 | TXT 小说打包 | +| -------- | -------- | -------- | -------- | -------- | ------------ | +| 48 | 103 | 50 | 54 | 100 | 109 | - | 聚友客栈 | 坛友自售 | - | -------- | -------- | - | 57 | 136 |`, +| 聚友客栈 | 坛友自售 | +| -------- | -------- | +| 57 | 136 |`, }; async function handler(ctx) { - const id = ctx.req.param('id') ?? '2'; + const id = ctx.req.param('id') ?? '3'; const rootUrl = 'https://hjd2048.com'; - - const entranceDomain = await cache.tryGet('2048:entranceDomain', async () => { - const { data: response } = await got('https://hjd.tw', { - headers: { - accept: '*/*', - }, - }); + // 获取地址发布页指向的 URL + const domainInfo = (await cache.tryGet('2048:domainInfo', async () => { + const response = await ofetch('https://2048.info'); const $ = load(response); - const targetLink = $('table.group-table tr').eq(1).find('td a').eq(0).attr('href'); - return targetLink; + const onclickValue = $('.button').first().attr('onclick'); + const targetUrl = onclickValue.match(/window\.open\('([^']+)'/)[1]; + + return { url: targetUrl }; + })) as { url: string }; + // 获取重定向后的url和safeid + const redirectResponse = await ofetch.raw(domainInfo.url); + const currentUrl = `${redirectResponse.url}thread.php?fid-${id}.html`; + const redirectPageContent = load(redirectResponse._data); + const safeId = + redirectPageContent('script') + .text() + .match(/var safeid='(.*?)',/)?.[1] ?? ''; + + const response = await ofetch.raw(currentUrl, { + headers: { + cookie: `_safe=${safeId}`, + }, }); - const currentUrl = `${entranceDomain}/2048/thread.php?fid-${id}.html`; - - const response = await got({ - method: 'get', - url: currentUrl, - }); - - const $ = load(response.data); + const $ = load(response._data); const currentHost = `https://${new URL(response.url).host}`; // redirected host $('#shortcut').remove(); @@ -101,7 +106,7 @@ async function handler(ctx) { return { title: item.text(), - link: `${currentHost}/2048/${item.attr('href')}`, + link: `${currentHost}/${item.attr('href')}`, guid: `${rootUrl}/2048/${item.attr('href')}`, }; }) @@ -110,34 +115,38 @@ async function handler(ctx) { const items = await Promise.all( list.map((item) => cache.tryGet(item.guid, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, + const detailResponse = await ofetch(item.link, { + headers: { + cookie: `_safe=${safeId}`, + }, }); - const content = load(detailResponse.data); + const content = load(detailResponse); content('.ads, .tips').remove(); content('ignore_js_op').each(function () { - content(this).replaceWith(``); + const img = content(this).find('img'); + const originalSrc = img.attr('data-original'); + const fallbackSrc = img.attr('src'); + // 判断是否有 data-original 属性,若有则使用其值,否则使用 src 属性值 + const imgSrc = originalSrc || fallbackSrc; + content(this).replaceWith(``); }); item.author = content('.fl.black').first().text(); item.pubDate = timezone(parseDate(content('span.fl.gray').first().attr('title')), +8); const downloadLink = content('#read_tpc').first().find('a').last(); + const copyLink = content('#copytext')?.first()?.text(); + if (downloadLink?.text()?.startsWith('http') && /bt\.azvmw\.com$/.test(new URL(downloadLink.text()).hostname)) { + const torrentResponse = await ofetch(downloadLink.text()); - if (downloadLink?.text()?.startsWith('http') && /datapps\.org$/.test(new URL(downloadLink.text()).hostname)) { - const torrentResponse = await got({ - method: 'get', - url: downloadLink.text(), - }); - - const torrent = load(torrentResponse.data); + const torrent = load(torrentResponse); item.enclosure_type = 'application/x-bittorrent'; - item.enclosure_url = `https://data.datapps.org/${torrent('.uk-button').last().attr('href')}`; + const ahref = torrent('.uk-button').last().attr('href'); + item.enclosure_url = ahref?.startsWith('http') ? ahref : `https://bt.azvmw.com/${ahref}`; const magnet = torrent('.uk-button').first().attr('href'); @@ -147,6 +156,10 @@ async function handler(ctx) { torrent: item.enclosure_url, }) ); + } else if (copyLink?.startsWith('magnet')) { + // copy link + item.enclosure_url = copyLink; + item.enclosure_type = 'x-scheme-handler/magnet'; } const desp = content('#read_tpc').first(); diff --git a/lib/routes/2048/namespace.ts b/lib/routes/2048/namespace.ts index 59ad6958dbf16e..46387693987259 100644 --- a/lib/routes/2048/namespace.ts +++ b/lib/routes/2048/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '2048 核基地', url: 'hjd2048.com', + lang: 'zh-CN', }; diff --git a/lib/routes/21caijing/channel.ts b/lib/routes/21caijing/channel.ts new file mode 100644 index 00000000000000..2170c569c39ffd --- /dev/null +++ b/lib/routes/21caijing/channel.ts @@ -0,0 +1,1082 @@ +import { type CheerioAPI, load } from 'cheerio'; +import { type Context } from 'hono'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate, parseRelativeDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +const processMenu = (data: any[]) => { + const result = {}; + + const processMenuItem = (item, parentPath = '', parentUrl?, parentShort?) => { + const currentPath = parentPath ? `${parentPath}/${item.name}` : item.name; + const currentUrl = item.url || parentUrl; + const currentApiUrl = item.api || item.children?.[0]?.api; + const currentShort = parentShort || currentUrl.split('/channel/').pop(); + + if (currentUrl && currentApiUrl) { + result[currentPath] = { + url: currentUrl, + apiUrl: currentApiUrl, + short: currentShort || '', + }; + } + + if (item.children && item.children.length > 0) { + for (const child of item.children) { + processMenuItem(child, currentPath, currentUrl, currentShort); + } + } + }; + + for (const item of data) { + processMenuItem(item); + } + + return result; +}; + +export const handler = async (ctx: Context): Promise => { + const { name = '热点' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const domain: string = 'm.21jingji.com'; + const baseUrl: string = `https://${domain}`; + const staticBaseUrl: string = 'https://static.21jingji.com'; + const menuUrl: string = new URL('m/webMenu.json', staticBaseUrl).href; + + const menuResponse = await ofetch(menuUrl); + const menu = processMenu(menuResponse); + + if (!menu.hasOwnProperty(name)) { + throw new InvalidParameterError('Invalid channel name'); + } + + const currentChannel = menu[name]; + + const apiUrl: string = new URL(currentChannel.apiUrl, baseUrl).href; + const targetUrl: string = new URL(`#/${currentChannel.url}`, baseUrl).href; + const authUrl: string = new URL('reader/cbhChannelAuth', baseUrl).href; + + const targetResponse = await ofetch(targetUrl); + const $: CheerioAPI = load(targetResponse); + const language: string = $('html').attr('lang') ?? 'zh-CN'; + + const authResponse = await ofetch(authUrl, { + method: 'POST', + responseType: 'json', + }); + + const response = await ofetch(apiUrl, { + query: { + short: currentChannel.short, + type: 'json', + page: 1, + }, + headers: { + authorization: authResponse.token, + host: domain, + referer: baseUrl, + }, + }); + + let items: DataItem[] = []; + + items = JSON.parse(response) + .slice(0, limit) + .map((item): DataItem => { + const title: string = item.title; + const pubDate: string = item.inputtime; + const linkUrl: string | undefined = item.url; + const categories: string[] = [...new Set(((item.keywords ?? '') as string)?.split(/,/).filter(Boolean))]; + const authors: DataItem['author'] = [...new Set([item.mp?.name, item.author, item.editor, item.source].filter(Boolean))].map((name) => ({ + name, + })); + const guid: string = `21jingji-${item.id}`; + const image: string | undefined = item.image ?? item.thumb ?? item.listthumb; + const updated: number | string = item.updatetime; + + const processedItem: DataItem = { + title, + pubDate: pubDate ? timezone(parseRelativeDate(pubDate), +8) : undefined, + link: linkUrl, + category: categories, + author: authors, + guid, + id: guid, + image, + banner: image, + updated: updated ? parseDate(updated, 'X') : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + $$('div.rela-box').remove(); + $$('div.copyright').remove(); + + const title: string = $$('div.titleHead h1').text(); + const description: string = $$('div.main_content, div.txtContent').html() ?? ''; + const pubDateStr: string | undefined = $$('div.author-infos span') + .text() + .match(/(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})/)?.[1]; + const categories: string[] = $$('meta[name="keywords"]').attr('content')?.split(/,/) ?? item.category ?? []; + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : item.pubDate, + category: categories, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? timezone(parseDate(upDatedStr), +8) : item.updated, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const author: string = $('title').text(); + + return { + title: `${author} - ${name}`, + description: $('meta[name=description]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/channel/:name{.+}?', + name: '频道', + url: 'm.21jingji.com', + maintainers: ['nczitzk'], + handler, + example: '/21caijing/channel/热点', + parameters: { + category: '分类,默认为热点,可在对应分类页 URL 中找到', + }, + description: `:::tip +若订阅 [热点](https://m.21jingji.com/#/),请将 \`热点\` 作为 \`name\` 参数填入,此时目标路由为 [\`/21caijing/channel/热点\`](https://rsshub.app/21caijing/channel/热点)。 + +若订阅 [投资通 - 盘前情报](https://m.21jingji.com/#/channel/investment),请将 \`投资通/盘前情报\` 作为 \`name\` 参数填入,此时目标路由为 [\`/21caijing/channel/投资通/盘前情报\`](https://rsshub.app/21caijing/channel/投资通/盘前情报)。 +::: + +
+更多分类 + +#### [热点](https://m.21jingji.com/#/) + +#### [投资通](https://m.21jingji.com/#/channel/investment) + +| [推荐](https://m.21jingji.com/#/channel/investment) | [盘前情报](https://m.21jingji.com/#/channel/premkt) | [公司洞察](https://m.21jingji.com/#/channel/gsdc) | [南财研选](https://m.21jingji.com/#/channel/ncyx) | [龙虎榜](https://m.21jingji.com/#/channel/lhb) | [公告精选](https://m.21jingji.com/#/channel/notice) | [牛熊透视](https://m.21jingji.com/#/channel/bullbear) | [一周前瞻](https://m.21jingji.com/#/channel/dailyfx) | [财经日历](https://m.21jingji.com/#/) | [风口掘金](https://m.21jingji.com/#/channel/windgap) | [实时解盘](https://m.21jingji.com/#/channel/marketanalysis) | [调研内参](https://m.21jingji.com/#/channel/research) | [趋势前瞻](https://m.21jingji.com/#/channel/tendency) | [硬核选基](https://m.21jingji.com/#/channel/yhxj) | [3 分钟理财](https://m.21jingji.com/#/channel/sfzlc) | [AI 智讯](https://m.21jingji.com/#/channel/aizx) | [北向资金](https://m.21jingji.com/#/channel/northmoney) | +| --------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| [投资通/推荐](https://rsshub.app/21caijing/channel/投资通/推荐) | [投资通/盘前情报](https://rsshub.app/21caijing/channel/投资通/盘前情报) | [投资通/公司洞察](https://rsshub.app/21caijing/channel/投资通/公司洞察) | [投资通/南财研选](https://rsshub.app/21caijing/channel/投资通/南财研选) | [投资通/龙虎榜](https://rsshub.app/21caijing/channel/投资通/龙虎榜) | [投资通/公告精选](https://rsshub.app/21caijing/channel/投资通/公告精选) | [投资通/牛熊透视](https://rsshub.app/21caijing/channel/投资通/牛熊透视) | [投资通/一周前瞻](https://rsshub.app/21caijing/channel/投资通/一周前瞻) | [投资通/财经日历](https://rsshub.app/21caijing/channel/投资通/财经日历) | [投资通/风口掘金](https://rsshub.app/21caijing/channel/投资通/风口掘金) | [投资通/实时解盘](https://rsshub.app/21caijing/channel/投资通/实时解盘) | [投资通/调研内参](https://rsshub.app/21caijing/channel/投资通/调研内参) | [投资通/趋势前瞻](https://rsshub.app/21caijing/channel/投资通/趋势前瞻) | [投资通/硬核选基](https://rsshub.app/21caijing/channel/投资通/硬核选基) | [投资通/3 分钟理财](https://rsshub.app/21caijing/channel/投资通/3分钟理财) | [投资通/AI 智讯](https://rsshub.app/21caijing/channel/投资通/AI智讯) | [投资通/北向资金](https://rsshub.app/21caijing/channel/投资通/北向资金) | + +#### [金融](https://m.21jingji.com/#/channel/finance) + +| [动态](https://m.21jingji.com/#/channel/finance) | [最保险](https://m.21jingji.com/#/channel/Insurance) | [资管](https://m.21jingji.com/#/channel/21zg) | [数字金融](https://m.21jingji.com/#/channel/szjr) | [私人银行](https://m.21jingji.com/#/channel/sryh) | [普惠](https://m.21jingji.com/#/channel/puhui) | [观债](https://m.21jingji.com/#/channel/21gz) | [金融研究](https://m.21jingji.com/#/channel/jryj) | [投教基地](https://m.21jingji.com/#/channel/tjjd) | [银行](https://m.21jingji.com/#/channel/bank) | [非银金融](https://m.21jingji.com/#/channel/nonbank) | [金融人事](https://m.21jingji.com/#/channel/jrrs) | +| ----------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [金融/动态](https://rsshub.app/21caijing/channel/金融/动态) | [金融/最保险](https://rsshub.app/21caijing/channel/金融/最保险) | [金融/资管](https://rsshub.app/21caijing/channel/金融/资管) | [金融/数字金融](https://rsshub.app/21caijing/channel/金融/数字金融) | [金融/私人银行](https://rsshub.app/21caijing/channel/金融/私人银行) | [金融/普惠](https://rsshub.app/21caijing/channel/金融/普惠) | [金融/观债](https://rsshub.app/21caijing/channel/金融/观债) | [金融/金融研究](https://rsshub.app/21caijing/channel/金融/金融研究) | [金融/投教基地](https://rsshub.app/21caijing/channel/金融/投教基地) | [金融/银行](https://rsshub.app/21caijing/channel/金融/银行) | [金融/非银金融](https://rsshub.app/21caijing/channel/金融/非银金融) | [金融/金融人事](https://rsshub.app/21caijing/channel/金融/金融人事) | + +#### [宏观](https://m.21jingji.com/#/channel/politics) + +#### [学习经济](https://m.21jingji.com/#/jujiao/xxjjIndexV3) + +| [经济思想](https://m.21jingji.com/#/https://m.21jingji.com/news/xxjj) | [学习经济卡片](https://m.21jingji.com/#/channel/mrjj) | [高质量发展](https://m.21jingji.com/#/channel/gzlfz) | [经济政策](https://m.21jingji.com/#/channel/jjzc) | [广东在行动](https://m.21jingji.com/#/channel/gdzxd) | [数说经济](https://m.21jingji.com/#/channel/ssjj) | [学习视频](https://m.21jingji.com/#/channel/xxsp) | [学习党史](https://m.21jingji.com/#/) | +| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| [学习经济/经济思想](https://rsshub.app/21caijing/channel/学习经济/经济思想) | [学习经济/学习经济卡片](https://rsshub.app/21caijing/channel/学习经济/学习经济卡片) | [学习经济/高质量发展](https://rsshub.app/21caijing/channel/学习经济/高质量发展) | [学习经济/经济政策](https://rsshub.app/21caijing/channel/学习经济/经济政策) | [学习经济/广东在行动](https://rsshub.app/21caijing/channel/学习经济/广东在行动) | [学习经济/数说经济](https://rsshub.app/21caijing/channel/学习经济/数说经济) | [学习经济/学习视频](https://rsshub.app/21caijing/channel/学习经济/学习视频) | [学习经济/学习党史](https://rsshub.app/21caijing/channel/学习经济/学习党史) | + +#### [大湾区](https://m.21jingji.com/#/channel/GHM_GreaterBay) + +| [动态](https://m.21jingji.com/#/channel/GHM_GreaterBay) | [湾区金融](https://m.21jingji.com/#/channel/wqjr) | [大湾区直播室](https://m.21jingji.com/#/channel/dwqzbs) | [高成长企业](https://m.21jingji.com/#/channel/gczqy) | [产业地理](https://m.21jingji.com/#/channel/cydl) | [数智湾区](https://m.21jingji.com/#/channel/szwq) | [湾区金融大咖会](https://m.21jingji.com/#/channel/wqjrdkh) | [“港”创科 25 人](https://m.21jingji.com/#/channel/gck) | [湾区论坛](https://m.21jingji.com/#/channel/wqlt) | +| --------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| [大湾区/动态](https://rsshub.app/21caijing/channel/大湾区/动态) | [大湾区/湾区金融](https://rsshub.app/21caijing/channel/大湾区/湾区金融) | [大湾区/大湾区直播室](https://rsshub.app/21caijing/channel/大湾区/大湾区直播室) | [大湾区/高成长企业](https://rsshub.app/21caijing/channel/大湾区/高成长企业) | [大湾区/产业地理](https://rsshub.app/21caijing/channel/大湾区/产业地理) | [大湾区/数智湾区](https://rsshub.app/21caijing/channel/大湾区/数智湾区) | [大湾区/湾区金融大咖会](https://rsshub.app/21caijing/channel/大湾区/湾区金融大咖会) | [大湾区/“港”创科 25 人](https://rsshub.app/21caijing/channel/大湾区/“港”创科25人) | [大湾区/湾区论坛](https://rsshub.app/21caijing/channel/大湾区/湾区论坛) | + +#### [证券](https://m.21jingji.com/#/channel/capital) + +| [动态](https://m.21jingji.com/#/channel/capital) | [赢基金](https://m.21jingji.com/#/channel/funds) | [券业观察](https://m.21jingji.com/#/channel/securities) | [期市一线](https://m.21jingji.com/#/channel/qsyx) | [ETF](https://m.21jingji.com/#/channel/govern) | +| ----------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------- | +| [证券/动态](https://rsshub.app/21caijing/channel/证券/动态) | [证券/赢基金](https://rsshub.app/21caijing/channel/证券/赢基金) | [证券/券业观察](https://rsshub.app/21caijing/channel/证券/券业观察) | [证券/期市一线](https://rsshub.app/21caijing/channel/证券/期市一线) | [证券/ETF](https://rsshub.app/21caijing/channel/证券/ETF) | + +#### [汽车](https://m.21jingji.com/#/channel/auto) + +| [热闻](https://m.21jingji.com/#/channel/autofocus) | [新汽车](https://m.21jingji.com/#/channel/newauto) | [车访间](https://m.21jingji.com/#/channel/autointerview) | [财说车](https://m.21jingji.com/#/channel/autofortune) | [汽车人](https://m.21jingji.com/#/channel/autopeople) | [汽车商业地理](https://m.21jingji.com/#/channel/autogeo) | [汽车金融](https://m.21jingji.com/#/channel/autofinance) | [行业报告](https://m.21jingji.com/#/channel/autoreport) | [聚焦](https://m.21jingji.com/#/channel/autospotlight) | +| ----------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------- | +| [汽车/热闻](https://rsshub.app/21caijing/channel/汽车/热闻) | [汽车/新汽车](https://rsshub.app/21caijing/channel/汽车/新汽车) | [汽车/车访间](https://rsshub.app/21caijing/channel/汽车/车访间) | [汽车/财说车](https://rsshub.app/21caijing/channel/汽车/财说车) | [汽车/汽车人](https://rsshub.app/21caijing/channel/汽车/汽车人) | [汽车/汽车商业地理](https://rsshub.app/21caijing/channel/汽车/汽车商业地理) | [汽车/汽车金融](https://rsshub.app/21caijing/channel/汽车/汽车金融) | [汽车/行业报告](https://rsshub.app/21caijing/channel/汽车/行业报告) | [汽车/聚焦](https://rsshub.app/21caijing/channel/汽车/聚焦) | + +#### [观点](https://m.21jingji.com/#/channel/opinion) + +#### [新健康](https://m.21jingji.com/#/channel/healthnews) + +| [动态](https://m.21jingji.com/#/channel/healthdt) | [21 健讯 Daily](https://m.21jingji.com/#/channel/healthinfo) | [21CC](https://m.21jingji.com/#/channel/21cc) | [21 健谈](https://m.21jingji.com/#/channel/healthtalk) | [名医说](https://m.21jingji.com/#/channel/doctorssay) | [数字医疗](https://m.21jingji.com/#/channel/digitalhealth) | [21H 院长对话](https://m.21jingji.com/#/channel/talkwithdean) | [医健 IPO 解码](https://m.21jingji.com/#/channel/medicalIPO) | [研究报告](https://m.21jingji.com/#/channel/yjbg) | [21 科普](https://m.21jingji.com/#/channel/healthkp) | +| --------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------- | +| [新健康/动态](https://rsshub.app/21caijing/channel/新健康/动态) | [新健康/21 健讯 Daily](https://rsshub.app/21caijing/channel/新健康/21健讯Daily) | [新健康/21CC](https://rsshub.app/21caijing/channel/新健康/21CC) | [新健康/21 健谈](https://rsshub.app/21caijing/channel/新健康/21健谈) | [新健康/名医说](https://rsshub.app/21caijing/channel/新健康/名医说) | [新健康/数字医疗](https://rsshub.app/21caijing/channel/新健康/数字医疗) | [新健康/21H 院长对话](https://rsshub.app/21caijing/channel/新健康/21H院长对话) | [新健康/医健 IPO 解码](https://rsshub.app/21caijing/channel/新健康/医健IPO解码) | [新健康/研究报告](https://rsshub.app/21caijing/channel/新健康/研究报告) | [新健康/21 科普](https://rsshub.app/21caijing/channel/新健康/21科普) | + +#### [ESG](https://m.21jingji.com/#/channel/esg) + +| [ESG 发布厅](https://m.21jingji.com/#/channel/esg) | [绿色公司](https://m.21jingji.com/#/channel/lsgs) | [绿色金融](https://m.21jingji.com/#/channel/lsjr) | [净零碳城市](https://m.21jingji.com/#/channel/jltcs) | [碳市场](https://m.21jingji.com/#/channel/) | [生物多样性](https://m.21jingji.com/#/channel/swdyx) | [行业周报](https://m.21jingji.com/#/channel/hyzb) | [研究报告](https://m.21jingji.com/#/) | +| -------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | +| [ESG/ESG 发布厅](https://rsshub.app/21caijing/channel/ESG/ESG发布厅) | [ESG/绿色公司](https://rsshub.app/21caijing/channel/ESG/绿色公司) | [ESG/绿色金融](https://rsshub.app/21caijing/channel/ESG/绿色金融) | [ESG/净零碳城市](https://rsshub.app/21caijing/channel/ESG/净零碳城市) | [ESG/碳市场](https://rsshub.app/21caijing/channel/ESG/碳市场) | [ESG/生物多样性](https://rsshub.app/21caijing/channel/ESG/生物多样性) | [ESG/行业周报](https://rsshub.app/21caijing/channel/ESG/行业周报) | [ESG/研究报告](https://rsshub.app/21caijing/channel/ESG/研究报告) | + +#### [全球市场](https://m.21jingji.com/#/channel/global) + +| [动态](https://m.21jingji.com/#/channel/global) | [全球财经连线](https://m.21jingji.com/#/channel/globaleconomics) | [直击华尔街](https://m.21jingji.com/#/channel/wallstreet) | [百家跨国公司看中国](https://m.21jingji.com/#/channel/mnc) | [全球央行观察](https://m.21jingji.com/#/channel/globalcentralbanks) | [全球能源观察](https://m.21jingji.com/#/channel/globalenergy) | [美股一线](https://m.21jingji.com/#/channel/USstock) | [港股一线](https://m.21jingji.com/#/channel/HKstock) | [全球金融观察](https://m.21jingji.com/#/channel/globalfinance) | [联合国现场](https://m.21jingji.com/#/channel/unitednations) | [全球央行月报](https://m.21jingji.com/#/channel/centralbankreport) | [全球商品观察](https://m.21jingji.com/#/channel/globalcommodities) | +| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| [全球市场/动态](https://rsshub.app/21caijing/channel/全球市场/动态) | [全球市场/全球财经连线](https://rsshub.app/21caijing/channel/全球市场/全球财经连线) | [全球市场/直击华尔街](https://rsshub.app/21caijing/channel/全球市场/直击华尔街) | [全球市场/百家跨国公司看中国](https://rsshub.app/21caijing/channel/全球市场/百家跨国公司看中国) | [全球市场/全球央行观察](https://rsshub.app/21caijing/channel/全球市场/全球央行观察) | [全球市场/全球能源观察](https://rsshub.app/21caijing/channel/全球市场/全球能源观察) | [全球市场/美股一线](https://rsshub.app/21caijing/channel/全球市场/美股一线) | [全球市场/港股一线](https://rsshub.app/21caijing/channel/全球市场/港股一线) | [全球市场/全球金融观察](https://rsshub.app/21caijing/channel/全球市场/全球金融观察) | [全球市场/联合国现场](https://rsshub.app/21caijing/channel/全球市场/联合国现场) | [全球市场/全球央行月报](https://rsshub.app/21caijing/channel/全球市场/全球央行月报) | [全球市场/全球商品观察](https://rsshub.app/21caijing/channel/全球市场/全球商品观察) | + +#### [一带一路](https://m.21jingji.com/#/channel/BandR) + +#### [数读](https://m.21jingji.com/#/channel/readnumber) + +#### [理财通](https://m.21jingji.com/#/channel/financing) + +| [动态](https://m.21jingji.com/#/channel/licaidongtai) | [数据库](https://m.21jingji.com/#/channel/sjk) | [研报](https://m.21jingji.com/#/channel/yanbao) | [投教](https://m.21jingji.com/#/channel/tj) | [政策](https://m.21jingji.com/#/channel/zhengce) | [固收+](https://m.21jingji.com/#/channel/gushou) | [纯固收](https://m.21jingji.com/#/channel/chungushou) | [现金](https://m.21jingji.com/#/channel/xianjin) | [混合](https://m.21jingji.com/#/channel/hunhe) | [权益](https://m.21jingji.com/#/channel/quanyi) | +| --------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | +| [理财通/动态](https://rsshub.app/21caijing/channel/理财通/动态) | [理财通/数据库](https://rsshub.app/21caijing/channel/理财通/数据库) | [理财通/研报](https://rsshub.app/21caijing/channel/理财通/研报) | [理财通/投教](https://rsshub.app/21caijing/channel/理财通/投教) | [理财通/政策](https://rsshub.app/21caijing/channel/理财通/政策) | [理财通/固收+](https://rsshub.app/21caijing/channel/理财通/固收+) | [理财通/纯固收](https://rsshub.app/21caijing/channel/理财通/纯固收) | [理财通/现金](https://rsshub.app/21caijing/channel/理财通/现金) | [理财通/混合](https://rsshub.app/21caijing/channel/理财通/混合) | [理财通/权益](https://rsshub.app/21caijing/channel/理财通/权益) | + +#### [直播](https://m.21jingji.com/#/channel/live) + +#### [长三角](https://m.21jingji.com/#/channel/yangtzeriverdelta) + +#### [论坛活动](https://m.21jingji.com/#/channel/market) + +#### [创投](https://m.21jingji.com/#/channel/entrepreneur) + +#### [投教](https://m.21jingji.com/#/channel/tjzjy) + +| [动态](https://m.21jingji.com/#/channel/tjzjy) | [投教知识](https://m.21jingji.com/#/channel/tjzs) | [公益活动](https://m.21jingji.com/#/channel/gyhd) | +| ----------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [投教/动态](https://rsshub.app/21caijing/channel/投教/动态) | [投教/投教知识](https://rsshub.app/21caijing/channel/投教/投教知识) | [投教/公益活动](https://rsshub.app/21caijing/channel/投教/公益活动) | + +#### [海洋经济](https://m.21jingji.com/#/channel/oceaneconomy) + +#### [数字合规](https://m.21jingji.com/#/channel/compliance) + +#### [公司](https://m.21jingji.com/#/channel/company) + +| [动态](https://m.21jingji.com/#/channel/company) | [电子通信](https://m.21jingji.com/#/channel/electrocommunication) | [互联网](https://m.21jingji.com/#/channel/internet) | [高端制造](https://m.21jingji.com/#/channel/highend) | [新能源](https://m.21jingji.com/#/channel/newenergy) | [消费](https://m.21jingji.com/#/channel/consumption) | [地产基建](https://m.21jingji.com/#/channel/infrastructure) | [IPO](https://m.21jingji.com/#/channel/IPO) | [文旅](https://m.21jingji.com/#/channel/culturetravel) | +| ----------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------- | +| [公司/动态](https://rsshub.app/21caijing/channel/公司/动态) | [公司/电子通信](https://rsshub.app/21caijing/channel/公司/电子通信) | [公司/互联网](https://rsshub.app/21caijing/channel/公司/互联网) | [公司/高端制造](https://rsshub.app/21caijing/channel/公司/高端制造) | [公司/新能源](https://rsshub.app/21caijing/channel/公司/新能源) | [公司/消费](https://rsshub.app/21caijing/channel/公司/消费) | [公司/地产基建](https://rsshub.app/21caijing/channel/公司/地产基建) | [公司/IPO](https://rsshub.app/21caijing/channel/公司/IPO) | [公司/文旅](https://rsshub.app/21caijing/channel/公司/文旅) | + +#### [人文](https://m.21jingji.com/#/channel/life) + +#### [SFC Global](https://m.21jingji.com/#/channel/SFCGlobal) + +| [News](https://m.21jingji.com/#/channel/SFCGlobal) | [SFC Markets and Finance](https://m.21jingji.com/#/channel/ SFCMarketsandFinance) | [SFC Market Talk](https://m.21jingji.com/#/channel/ SFCMarketTalk) | [CBN](https://m.21jingji.com/#/channel/CBN) | [Multinationals on China](https://m.21jingji.com/#/channel/MultinationalsonChina) | [Companies in the GBA](https://m.21jingji.com/#/channel/CompaniesintheGBA) | +| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| [SFC Global/News](https://rsshub.app/21caijing/channel/SFC Global/News) | [SFC Global/SFC Markets and Finance](https://rsshub.app/21caijing/channel/SFC Global/SFC Markets and Finance) | [SFC Global/SFC Market Talk](https://rsshub.app/21caijing/channel/SFC Global/SFC Market Talk) | [SFC Global/CBN](https://rsshub.app/21caijing/channel/SFC Global/CBN) | [SFC Global/Multinationals on China](https://rsshub.app/21caijing/channel/SFC Global/Multinationals on China) | [SFC Global/Companies in the GBA](https://rsshub.app/21caijing/channel/SFC Global/Companies in the GBA) | + +#### [南方财经报道](https://m.21jingji.com/#/channel/nfcjbd) + +#### [链上预制菜](https://m.21jingji.com/#/channel/precookedfood) + +| [动态](https://m.21jingji.com/#/channel/precookedfood) | [活动](https://m.21jingji.com/#/channel/foodevent) | [报道](https://m.21jingji.com/#/channel/foodnews) | [智库/课题](https://m.21jingji.com/#/channel/foodtopic) | [数据/创新案例](https://m.21jingji.com/#/channel/foodcase) | [链接平台](https://m.21jingji.com/#/channel/foodlink) | +| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| [链上预制菜/动态](https://rsshub.app/21caijing/channel/链上预制菜/动态) | [链上预制菜/活动](https://rsshub.app/21caijing/channel/链上预制菜/活动) | [链上预制菜/报道](https://rsshub.app/21caijing/channel/链上预制菜/报道) | [链上预制菜/智库/课题](https://rsshub.app/21caijing/channel/链上预制菜/智库/课题) | [链上预制菜/数据/创新案例](https://rsshub.app/21caijing/channel/链上预制菜/数据/创新案例) | [链上预制菜/链接平台](https://rsshub.app/21caijing/channel/链上预制菜/链接平台) | + +
+`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + title: '热点', + source: ['m.21jingji.com/#/'], + target: '/channel/热点', + }, + { + title: '投资通', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通', + }, + { + title: '投资通/推荐', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/推荐', + }, + { + title: '投资通/盘前情报', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/盘前情报', + }, + { + title: '投资通/公司洞察', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/公司洞察', + }, + { + title: '投资通/南财研选', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/南财研选', + }, + { + title: '投资通/龙虎榜', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/龙虎榜', + }, + { + title: '投资通/公告精选', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/公告精选', + }, + { + title: '投资通/牛熊透视', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/牛熊透视', + }, + { + title: '投资通/一周前瞻', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/一周前瞻', + }, + { + title: '投资通/财经日历', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/财经日历', + }, + { + title: '投资通/风口掘金', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/风口掘金', + }, + { + title: '投资通/实时解盘', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/实时解盘', + }, + { + title: '投资通/调研内参', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/调研内参', + }, + { + title: '投资通/趋势前瞻', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/趋势前瞻', + }, + { + title: '投资通/硬核选基', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/硬核选基', + }, + { + title: '投资通/3分钟理财', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/3分钟理财', + }, + { + title: '投资通/AI智讯', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/AI智讯', + }, + { + title: '投资通/北向资金', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投资通/北向资金', + }, + { + title: '金融', + source: ['m.21jingji.com/#/channel/finance'], + target: '/channel/金融', + }, + { + title: '金融/动态', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/动态', + }, + { + title: '金融/最保险', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/最保险', + }, + { + title: '金融/资管', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/资管', + }, + { + title: '金融/数字金融', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/数字金融', + }, + { + title: '金融/私人银行', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/私人银行', + }, + { + title: '金融/普惠', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/普惠', + }, + { + title: '金融/观债', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/观债', + }, + { + title: '金融/金融研究', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/金融研究', + }, + { + title: '金融/投教基地', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/投教基地', + }, + { + title: '金融/银行', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/银行', + }, + { + title: '金融/非银金融', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/非银金融', + }, + { + title: '金融/金融人事', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/金融/金融人事', + }, + { + title: '宏观', + source: ['m.21jingji.com/#/channel/politics'], + target: '/channel/宏观', + }, + { + title: '学习经济', + source: ['m.21jingji.com/#/jujiao/xxjjIndexV3'], + target: '/channel/学习经济', + }, + { + title: '学习经济/经济思想', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/学习经济/经济思想', + }, + { + title: '学习经济/学习经济卡片', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/学习经济/学习经济卡片', + }, + { + title: '学习经济/高质量发展', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/学习经济/高质量发展', + }, + { + title: '学习经济/经济政策', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/学习经济/经济政策', + }, + { + title: '学习经济/广东在行动', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/学习经济/广东在行动', + }, + { + title: '学习经济/数说经济', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/学习经济/数说经济', + }, + { + title: '学习经济/学习视频', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/学习经济/学习视频', + }, + { + title: '学习经济/学习党史', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/学习经济/学习党史', + }, + { + title: '大湾区', + source: ['m.21jingji.com/#/channel/GHM_GreaterBay'], + target: '/channel/大湾区', + }, + { + title: '大湾区/动态', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/大湾区/动态', + }, + { + title: '大湾区/湾区金融', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/大湾区/湾区金融', + }, + { + title: '大湾区/大湾区直播室', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/大湾区/大湾区直播室', + }, + { + title: '大湾区/高成长企业', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/大湾区/高成长企业', + }, + { + title: '大湾区/产业地理', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/大湾区/产业地理', + }, + { + title: '大湾区/数智湾区', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/大湾区/数智湾区', + }, + { + title: '大湾区/湾区金融大咖会', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/大湾区/湾区金融大咖会', + }, + { + title: '大湾区/“港”创科25人', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/大湾区/“港”创科25人', + }, + { + title: '大湾区/湾区论坛', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/大湾区/湾区论坛', + }, + { + title: '证券', + source: ['m.21jingji.com/#/channel/capital'], + target: '/channel/证券', + }, + { + title: '证券/动态', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/证券/动态', + }, + { + title: '证券/赢基金', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/证券/赢基金', + }, + { + title: '证券/券业观察', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/证券/券业观察', + }, + { + title: '证券/期市一线', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/证券/期市一线', + }, + { + title: '证券/ETF', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/证券/ETF', + }, + { + title: '汽车', + source: ['m.21jingji.com/#/channel/auto'], + target: '/channel/汽车', + }, + { + title: '汽车/热闻', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/汽车/热闻', + }, + { + title: '汽车/新汽车', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/汽车/新汽车', + }, + { + title: '汽车/车访间', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/汽车/车访间', + }, + { + title: '汽车/财说车', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/汽车/财说车', + }, + { + title: '汽车/汽车人', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/汽车/汽车人', + }, + { + title: '汽车/汽车商业地理', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/汽车/汽车商业地理', + }, + { + title: '汽车/汽车金融', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/汽车/汽车金融', + }, + { + title: '汽车/行业报告', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/汽车/行业报告', + }, + { + title: '汽车/聚焦', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/汽车/聚焦', + }, + { + title: '观点', + source: ['m.21jingji.com/#/channel/opinion'], + target: '/channel/观点', + }, + { + title: '新健康', + source: ['m.21jingji.com/#/channel/healthnews'], + target: '/channel/新健康', + }, + { + title: '新健康/动态', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/新健康/动态', + }, + { + title: '新健康/21健讯Daily', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/新健康/21健讯Daily', + }, + { + title: '新健康/21CC', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/新健康/21CC', + }, + { + title: '新健康/21健谈', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/新健康/21健谈', + }, + { + title: '新健康/名医说', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/新健康/名医说', + }, + { + title: '新健康/数字医疗', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/新健康/数字医疗', + }, + { + title: '新健康/21H院长对话', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/新健康/21H院长对话', + }, + { + title: '新健康/医健IPO解码', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/新健康/医健IPO解码', + }, + { + title: '新健康/研究报告', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/新健康/研究报告', + }, + { + title: '新健康/21科普', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/新健康/21科普', + }, + { + title: 'ESG', + source: ['m.21jingji.com/#/channel/esg'], + target: '/channel/ESG', + }, + { + title: 'ESG/ESG发布厅', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/ESG/ESG发布厅', + }, + { + title: 'ESG/绿色公司', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/ESG/绿色公司', + }, + { + title: 'ESG/绿色金融', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/ESG/绿色金融', + }, + { + title: 'ESG/净零碳城市', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/ESG/净零碳城市', + }, + { + title: 'ESG/碳市场', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/ESG/碳市场', + }, + { + title: 'ESG/生物多样性', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/ESG/生物多样性', + }, + { + title: 'ESG/行业周报', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/ESG/行业周报', + }, + { + title: 'ESG/研究报告', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/ESG/研究报告', + }, + { + title: '全球市场', + source: ['m.21jingji.com/#/channel/global'], + target: '/channel/全球市场', + }, + { + title: '全球市场/动态', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/动态', + }, + { + title: '全球市场/全球财经连线', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/全球财经连线', + }, + { + title: '全球市场/直击华尔街', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/直击华尔街', + }, + { + title: '全球市场/百家跨国公司看中国', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/百家跨国公司看中国', + }, + { + title: '全球市场/全球央行观察', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/全球央行观察', + }, + { + title: '全球市场/全球能源观察', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/全球能源观察', + }, + { + title: '全球市场/美股一线', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/美股一线', + }, + { + title: '全球市场/港股一线', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/港股一线', + }, + { + title: '全球市场/全球金融观察', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/全球金融观察', + }, + { + title: '全球市场/联合国现场', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/联合国现场', + }, + { + title: '全球市场/全球央行月报', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/全球央行月报', + }, + { + title: '全球市场/全球商品观察', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/全球市场/全球商品观察', + }, + { + title: '一带一路', + source: ['m.21jingji.com/#/channel/BandR'], + target: '/channel/一带一路', + }, + { + title: '数读', + source: ['m.21jingji.com/#/channel/readnumber'], + target: '/channel/数读', + }, + { + title: '理财通', + source: ['m.21jingji.com/#/channel/financing'], + target: '/channel/理财通', + }, + { + title: '理财通/动态', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/理财通/动态', + }, + { + title: '理财通/数据库', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/理财通/数据库', + }, + { + title: '理财通/研报', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/理财通/研报', + }, + { + title: '理财通/投教', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/理财通/投教', + }, + { + title: '理财通/政策', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/理财通/政策', + }, + { + title: '理财通/固收+', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/理财通/固收+', + }, + { + title: '理财通/纯固收', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/理财通/纯固收', + }, + { + title: '理财通/现金', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/理财通/现金', + }, + { + title: '理财通/混合', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/理财通/混合', + }, + { + title: '理财通/权益', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/理财通/权益', + }, + { + title: '直播', + source: ['m.21jingji.com/#/channel/live'], + target: '/channel/直播', + }, + { + title: '长三角', + source: ['m.21jingji.com/#/channel/yangtzeriverdelta'], + target: '/channel/长三角', + }, + { + title: '论坛活动', + source: ['m.21jingji.com/#/channel/market'], + target: '/channel/论坛活动', + }, + { + title: '创投', + source: ['m.21jingji.com/#/channel/entrepreneur'], + target: '/channel/创投', + }, + { + title: '投教', + source: ['m.21jingji.com/#/channel/tjzjy'], + target: '/channel/投教', + }, + { + title: '投教/动态', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投教/动态', + }, + { + title: '投教/投教知识', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投教/投教知识', + }, + { + title: '投教/公益活动', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/投教/公益活动', + }, + { + title: '海洋经济', + source: ['m.21jingji.com/#/channel/oceaneconomy'], + target: '/channel/海洋经济', + }, + { + title: '数字合规', + source: ['m.21jingji.com/#/channel/compliance'], + target: '/channel/数字合规', + }, + { + title: '公司', + source: ['m.21jingji.com/#/channel/company'], + target: '/channel/公司', + }, + { + title: '公司/动态', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/公司/动态', + }, + { + title: '公司/电子通信', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/公司/电子通信', + }, + { + title: '公司/互联网', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/公司/互联网', + }, + { + title: '公司/高端制造', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/公司/高端制造', + }, + { + title: '公司/新能源', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/公司/新能源', + }, + { + title: '公司/消费', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/公司/消费', + }, + { + title: '公司/地产基建', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/公司/地产基建', + }, + { + title: '公司/IPO', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/公司/IPO', + }, + { + title: '公司/文旅', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/公司/文旅', + }, + { + title: '人文', + source: ['m.21jingji.com/#/channel/life'], + target: '/channel/人文', + }, + { + title: 'SFC Global', + source: ['m.21jingji.com/#/channel/SFCGlobal'], + target: '/channel/SFC Global', + }, + { + title: 'SFC Global/News', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/SFC Global/News', + }, + { + title: 'SFC Global/SFC Markets and Finance', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/SFC Global/SFC Markets and Finance', + }, + { + title: 'SFC Global/SFC Market Talk', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/SFC Global/SFC Market Talk', + }, + { + title: 'SFC Global/CBN', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/SFC Global/CBN', + }, + { + title: 'SFC Global/Multinationals on China', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/SFC Global/Multinationals on China', + }, + { + title: 'SFC Global/Companies in the GBA', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/SFC Global/Companies in the GBA', + }, + { + title: '南方财经报道', + source: ['m.21jingji.com/#/channel/nfcjbd'], + target: '/channel/南方财经报道', + }, + { + title: '专题', + source: ['m.21jingji.com/#/jujiao'], + target: '/channel/专题', + }, + { + title: '链上预制菜', + source: ['m.21jingji.com/#/channel/precookedfood'], + target: '/channel/链上预制菜', + }, + { + title: '链上预制菜/动态', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/链上预制菜/动态', + }, + { + title: '链上预制菜/活动', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/链上预制菜/活动', + }, + { + title: '链上预制菜/报道', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/链上预制菜/报道', + }, + { + title: '链上预制菜/智库/课题', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/链上预制菜/智库/课题', + }, + { + title: '链上预制菜/数据/创新案例', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/链上预制菜/数据/创新案例', + }, + { + title: '链上预制菜/链接平台', + source: ['m.21jingji.com/#/channel/investment'], + target: '/channel/链上预制菜/链接平台', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/21caijing/namespace.ts b/lib/routes/21caijing/namespace.ts new file mode 100644 index 00000000000000..4c9b2ad5f71f76 --- /dev/null +++ b/lib/routes/21caijing/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '21财经', + url: '21caijing.com', + categories: ['finance'], + description: '', +}; diff --git a/lib/routes/2cycd/namespace.ts b/lib/routes/2cycd/namespace.ts index 4b264b1b811858..c05898788fb9e8 100644 --- a/lib/routes/2cycd/namespace.ts +++ b/lib/routes/2cycd/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '二次元虫洞', url: '2cycd.com', + lang: 'zh-CN', }; diff --git a/lib/routes/30secondsofcode/category.ts b/lib/routes/30secondsofcode/category.ts new file mode 100644 index 00000000000000..430c480f706078 --- /dev/null +++ b/lib/routes/30secondsofcode/category.ts @@ -0,0 +1,65 @@ +import { Data, Route } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import { processList } from './utils'; +export const route: Route = { + path: '/category/:category?/:subCategory?', + categories: ['programming'], + example: '/category/css/interactivity', + parameters: { + category: { + description: 'Main Category. For Complete list visit site "https://www.30secondsofcode.org/collections/p/1/"', + options: [ + { value: 'js', label: 'Javascript' }, + { value: 'css', label: 'CSS' }, + { value: 'algorithm', label: 'JavaScript Algorithms' }, + { value: 'react', label: 'React' }, + ], + }, + subCategory: { + description: 'Filter within Category. Visit Individual Category site for subCategories', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['30secondsofcode.org/:category/:subCategory/', '30secondsofcode.org/:category/'], + target: '/category/:category/:subCategory', + }, + ], + name: 'Category and Subcategory', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler(ctx) { + const category = ctx.req.param('category') ?? ''; + const subCategory = ctx.req.param('subCategory') ?? ''; + + const rootUrl = 'https://www.30secondsofcode.org'; + const currentUrl = `${rootUrl}${category ? `/${category}` : ''}${subCategory ? `/${subCategory}` : ''}${category || subCategory ? '/p/1/' : ''}`; + + const response = await ofetch(currentUrl); + const $ = load(response); + const heroElement = $('section.hero'); + const heading = heroElement.find('div > h1').text(); + const description = heroElement.find('div > p').text(); + const image = heroElement.find('img').attr('src'); + + const fullList = $('section.preview-list > ul > li').toArray(); + const items = await processList(fullList); + return { + title: heading, + description, + image: `${rootUrl}${image}`, + link: rootUrl, + item: items, + } as Data; +} diff --git a/lib/routes/30secondsofcode/namespace.ts b/lib/routes/30secondsofcode/namespace.ts new file mode 100644 index 00000000000000..b1b8b59d06ccc4 --- /dev/null +++ b/lib/routes/30secondsofcode/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '30 Seconds of code', + url: 'www.30secondsofcode.org', + lang: 'en', +}; diff --git a/lib/routes/30secondsofcode/new-and-popular.ts b/lib/routes/30secondsofcode/new-and-popular.ts new file mode 100644 index 00000000000000..27f174bd80ce53 --- /dev/null +++ b/lib/routes/30secondsofcode/new-and-popular.ts @@ -0,0 +1,41 @@ +import { Data, Route } from '@/types'; +import { load } from 'cheerio'; +import { processList, rootUrl } from './utils'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/latest', + categories: ['programming'], + example: '/latest', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['30secondsofcode.org'], + target: '/latest', + }, + ], + name: 'New & Popular Snippets', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler() { + const response = await ofetch(rootUrl); + + const $ = load(response); + const fullList = $('section.preview-list > ul > li').toArray(); + const items = await processList(fullList); + return { + title: 'New & Popular Snippets', + description: 'Discover short code snippets for all your development needs.', + link: rootUrl, + item: items, + } as Data; +} diff --git a/lib/routes/30secondsofcode/utils.ts b/lib/routes/30secondsofcode/utils.ts new file mode 100644 index 00000000000000..9ffbab8b5045b9 --- /dev/null +++ b/lib/routes/30secondsofcode/utils.ts @@ -0,0 +1,54 @@ +import { DataItem } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +export const rootUrl = 'https://www.30secondsofcode.org'; + +export async function processList(listElements) { + const items = await Promise.allSettled( + listElements.map((item) => { + const $ = load(item); + const link = $(' article > h3 > a').attr('href'); + const date = $(' article > small > time').attr('datetime'); + return processItem({ link, date }); + }) + ); + return items.map((item) => (item.status === 'fulfilled' ? item.value : ({ title: 'Error Reading Item' } as DataItem))); +} + +async function processItem({ link: articleLink, date }) { + return await cache.tryGet(`30secondsofcode:${articleLink}`, async () => { + const finalLink = `${rootUrl}${articleLink}`; + const response = await ofetch(finalLink); + const $ = load(response); + const tags = $.root() + .find('body > main > nav > ol > li:not(:first-child):not(:last-child)') + .toArray() + .map((tag) => $(tag).find('a').text()); + const article = $('main > article'); + const title = article.find('h1').text(); + article.find('img').each((_, element) => { + const img = $(element); + const src = img.attr('src'); + if (src?.startsWith('/')) { + img.attr('src', `${rootUrl}${src}`); + } + }); + const image = article.find('img').attr('src'); + const description = article.clone().find('h1, script').remove().end().html(); + + return { + title, + link: finalLink, + pubDate: parseDate(date), + description, + author: '30 Seconds of Code', + category: tags, + image: `${rootUrl}${image}`, + banner: `${rootUrl}${image}`, + language: 'en-us', + } as DataItem; + }); +} diff --git a/lib/routes/36kr/hot-list.ts b/lib/routes/36kr/hot-list.ts index 640cdd74f848cf..85c537670d7178 100644 --- a/lib/routes/36kr/hot-list.ts +++ b/lib/routes/36kr/hot-list.ts @@ -4,6 +4,7 @@ import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; import { rootUrl, ProcessItem } from './utils'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; const categories = { 24: { @@ -26,7 +27,7 @@ const categories = { export const route: Route = { path: '/hot-list/:category?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/36kr/hot-list', parameters: { category: '分类,默认为24小时热榜' }, features: { @@ -47,13 +48,17 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 24 小时热榜 | 资讯人气榜 | 资讯综合榜 | 资讯收藏榜 | - | ----------- | ---------- | ---------- | ---------- | - | 24 | renqi | zonghe | shoucang |`, +| ----------- | ---------- | ---------- | ---------- | +| 24 | renqi | zonghe | shoucang |`, }; async function handler(ctx) { const category = ctx.req.param('category') ?? '24'; + if (!categories[category]) { + throw new InvalidParameterError('This category does not exist. Please refer to the documentation for the correct usage.'); + } + const currentUrl = category === '24' ? rootUrl : `${rootUrl}/hot-list/catalog`; const response = await got({ diff --git a/lib/routes/36kr/index.ts b/lib/routes/36kr/index.ts index 4b9bc982380dca..c4cb54b447ffaf 100644 --- a/lib/routes/36kr/index.ts +++ b/lib/routes/36kr/index.ts @@ -18,7 +18,7 @@ const shortcuts = { export const route: Route = { path: '/:category/:subCategory?/:keyword?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/36kr/newsflashes', parameters: { category: '分类,必填项', @@ -27,9 +27,9 @@ export const route: Route = { }, name: '资讯, 快讯, 用户文章, 主题文章, 专题文章, 搜索文章, 搜索快讯', maintainers: ['nczitzk', 'fashioncj'], - description: `| 最新资讯频道 | 快讯 | 推荐资讯|生活|房产|职场|搜索文章|搜索快讯| - | ------- | -------- | -------- | -------- | -------- | --------| -------- | -------- | - | news | newsflashes | recommend | life | estate | workplace | search/articles/关键词 | search/articles/关键词 |`, + description: `| 最新资讯频道 | 快讯 | 推荐资讯 | 生活 | 房产 | 职场 | 搜索文章 | 搜索快讯 | +| ------- | -------- | -------- | -------- | -------- | --------| -------- | -------- | +| news | newsflashes | recommend | life | estate | workplace | search/articles/关键词 | search/articles/关键词 |`, handler, }; diff --git a/lib/routes/36kr/namespace.ts b/lib/routes/36kr/namespace.ts index 5501ecc1c04536..be656f3a50224e 100644 --- a/lib/routes/36kr/namespace.ts +++ b/lib/routes/36kr/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '36kr', url: '36kr.com', + lang: 'zh-CN', }; diff --git a/lib/routes/3dmgame/namespace.ts b/lib/routes/3dmgame/namespace.ts index 471319744d401f..50b1d57c9cbbbf 100644 --- a/lib/routes/3dmgame/namespace.ts +++ b/lib/routes/3dmgame/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '3DMGame', url: '3dmgame.com', + lang: 'zh-CN', }; diff --git a/lib/routes/3dmgame/news-center.ts b/lib/routes/3dmgame/news-center.ts index c8b305052c5919..756e10c1583bac 100644 --- a/lib/routes/3dmgame/news-center.ts +++ b/lib/routes/3dmgame/news-center.ts @@ -28,8 +28,8 @@ export const route: Route = { maintainers: ['zhboner', 'lyqluis'], handler, description: `| 新闻推荐 | 游戏新闻 | 动漫影视 | 智能数码 | 时事焦点 | - | -------- | -------- | -------- | -------- | ----------- | - | | game | acg | next | news\_36\_1 |`, +| -------- | -------- | -------- | -------- | ----------- | +| | game | acg | next | news\_36\_1 |`, }; async function handler(ctx) { @@ -52,7 +52,7 @@ async function handler(ctx) { } const a = item.find('.text a'); return { - title: a.text(), + title: a.first().text(), link: a.attr('href'), description: item.find('.miaoshu').text(), pubDate: timezone(parseDate(item.find('.time').text().trim()), 8), diff --git a/lib/routes/3kns/index.ts b/lib/routes/3kns/index.ts index 2167b3f66f6365..d897afc1c8e142 100644 --- a/lib/routes/3kns/index.ts +++ b/lib/routes/3kns/index.ts @@ -30,33 +30,33 @@ export const route: Route = { url: 'www.3kns.com/', description: `游戏类型(category) - | 不限 | 角色扮演 | 动作冒险 | 策略游戏 | 模拟经营 | 即时战略 | 格斗类 | 射击游戏 | 休闲益智 | 体育运动 | 街机格斗 | 无双类 | 其他游戏 | 赛车竞速 | - | ---- | -------- | -------- | -------- | -------- | -------- | ------ | -------- | -------- | -------- | -------- | ------ | -------- | -------- | - | all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | +| 不限 | 角色扮演 | 动作冒险 | 策略游戏 | 模拟经营 | 即时战略 | 格斗类 | 射击游戏 | 休闲益智 | 体育运动 | 街机格斗 | 无双类 | 其他游戏 | 赛车竞速 | +| ---- | -------- | -------- | -------- | -------- | -------- | ------ | -------- | -------- | -------- | -------- | ------ | -------- | -------- | +| all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 游戏语言(language) - | 不限 | 中文 | 英语 | 日语 | 其他 | 中文汉化 | 德语 | - | ---- | ---- | ---- | ---- | ---- | -------- | ---- | - | all | 1 | 2 | 3 | 4 | 5 | 6 | +| 不限 | 中文 | 英语 | 日语 | 其他 | 中文汉化 | 德语 | +| ---- | ---- | ---- | ---- | ---- | -------- | ---- | +| all | 1 | 2 | 3 | 4 | 5 | 6 | 游戏标签(tag) - | 不限 | 热门 | 多人聚会 | 僵尸 | 体感 | 大作 | 音乐 | 三国 | RPG | 格斗 | 闯关 | 横版 | 科幻 | 棋牌 | 运输 | 无双 | 卡通动漫 | 日系 | 养成 | 恐怖 | 运动 | 乙女 | 街机 | 飞行模拟 | 解谜 | 海战 | 战争 | 跑酷 | 即时策略 | 射击 | 经营 | 益智 | 沙盒 | 模拟 | 冒险 | 竞速 | 休闲 | 动作 | 生存 | 独立 | 拼图 | 魔改 xci | 卡牌 | 塔防 | - | ---- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | --- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | - | all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | +| 不限 | 热门 | 多人聚会 | 僵尸 | 体感 | 大作 | 音乐 | 三国 | RPG | 格斗 | 闯关 | 横版 | 科幻 | 棋牌 | 运输 | 无双 | 卡通动漫 | 日系 | 养成 | 恐怖 | 运动 | 乙女 | 街机 | 飞行模拟 | 解谜 | 海战 | 战争 | 跑酷 | 即时策略 | 射击 | 经营 | 益智 | 沙盒 | 模拟 | 冒险 | 竞速 | 休闲 | 动作 | 生存 | 独立 | 拼图 | 魔改 xci | 卡牌 | 塔防 | +| ---- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | --- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | +| all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 发售时间(pubDate) - | 不限 | 2017 年 | 2018 年 | 2019 年 | 2020 年 | 2021 年 | 2022 年 | 2023 年 | 2024 年 | - | ---- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | - | all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +| 不限 | 2017 年 | 2018 年 | 2019 年 | 2020 年 | 2021 年 | 2022 年 | 2023 年 | 2024 年 | +| ---- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | +| all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 游戏集合(collection) - | 不限 | 舞力全开 | 马里奥 | 生化危机 | 炼金工房 | 最终幻想 | 塞尔达 | 宝可梦 | 勇者斗恶龙 | 模拟器 | 秋之回忆 | 第一方 | 体感健身 | 开放世界 | 儿童乐园 | - | ---- | -------- | ------ | -------- | -------- | -------- | ------ | ------ | ---------- | ------ | -------- | ------ | -------- | -------- | -------- | - | all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |`, +| 不限 | 舞力全开 | 马里奥 | 生化危机 | 炼金工房 | 最终幻想 | 塞尔达 | 宝可梦 | 勇者斗恶龙 | 模拟器 | 秋之回忆 | 第一方 | 体感健身 | 开放世界 | 儿童乐园 | +| ---- | -------- | ------ | -------- | -------- | -------- | ------ | ------ | ---------- | ------ | -------- | ------ | -------- | -------- | -------- | +| all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |`, }; async function handler(ctx: Context): Promise { diff --git a/lib/routes/3kns/namespace.ts b/lib/routes/3kns/namespace.ts index de8ffd266f685b..08c1992d05a61b 100644 --- a/lib/routes/3kns/namespace.ts +++ b/lib/routes/3kns/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '3k-Switch游戏库', url: 'www.3kns.com', + lang: 'zh-CN', }; diff --git a/lib/routes/423down/index.ts b/lib/routes/423down/index.ts index dd754109ae8d1b..1d18e513222757 100644 --- a/lib/routes/423down/index.ts +++ b/lib/routes/423down/index.ts @@ -1,123 +1,280 @@ 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 rootUrl = 'https://www.423down.com'; +export const handler = async (ctx) => { + const { category = '' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 18; -const categeoryMap = { - index: { - all: '', - }, - android: { - apk: 'apk', - }, - computer: { - originalsoft: 'zd423', - multimedia: 'multimedia', - browser: 'browser', - image: 'image', - im: 'im', - work: 'work', - down: 'down', - systemsoft: 'systemsoft', - systemplus: 'systemplus', - security: 'security', - patch: 'patch', - hardware: 'hardware', - }, - os: { - win11: 'win11', - win10: 'win10', - win7: 'win7', - winxp: 'winxp', - winpe: 'pe-system', - }, -}; + const domain = '423down.com'; + const rootUrl = `https://www.${domain}`; + const currentUrl = new URL(category, rootUrl).href; -const titleMap = { - index: { - all: '首页', - }, - android: { - apk: '安卓软件', - }, - computer: { - originalsoft: '原创软件', - multimedia: '媒体播放', - browser: '网页浏览', - image: '图形图像', - im: '聊天软件', - work: '办公软件', - down: '上传下载', - systemsoft: '系统辅助', - systemplus: '系统必备', - security: '安全软件', - patch: '补丁相关', - hardwork: '硬件相关', - }, - os: { - win11: 'windows 11', - win10: 'Windows 10', - win7: 'Windows 7', - winxp: 'Windows XP', - winpe: 'Windows PE', - }, -}; + const { data: response } = await got(currentUrl); -export const route: Route = { - path: '/:category/:type?', - name: 'Unknown', - maintainers: [], - handler, -}; + const $ = load(response); -async function handler(ctx) { - const { category, type } = ctx.req.param(); + const language = $('html').prop('lang'); - const url = `${rootUrl}/${categeoryMap[category][type]}`; + let items = $('ul.excerpt li') + .toArray() + .filter((item) => { + item = $(item); - const response = await got.get(url); - const $ = load(response.data); - const list = $('div.content-wrap > div > ul > li > a') - .filter((_, item) => { - const notAnotherWebPage = $(item).attr('style') !== 'display: none !important;'; - return notAnotherWebPage; + const link = item.find('h2 a').prop('href'); + + return new RegExp(domain).test(link); }) - .map((_, item) => ({ - link: $(item).attr('href'), - })) - .get(); - - const items = await Promise.all( - list.map(async (item) => { - item = await cache.tryGet(item.link, async () => { - const detailResponse = await got.get(item.link); - const $ = load(detailResponse.data); - - const title = $('div.content-wrap > div > div.meta > h1 > a').text(); - const pageContent = $('div.content-wrap > div > div.entry').html(); - const pageComments = $('#postcomments > ol').html(); - const desc = pageContent + pageComments; - const date = $('div.content-wrap > div > div.meta > p').text(); - const categeory = $('div.content-wrap > div > div.meta > p > a:not(.comm)').text(); + .slice(0, limit) + .map((item) => { + item = $(item); + + const title = item.find('h2').text(); + const image = item.find('a.pic img').prop('src'); + const description = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + intro: item.find('div.note').text(), + }); + + return { + title, + description, + pubDate: parseDate(item.find('span.time').text(), 'MM-DD'), + link: item.find('h2 a').prop('href'), + category: item + .find('span.cat a') + .toArray() + .map((c) => $(c).text()), + content: { + html: description, + text: item.find('div.note').text(), + }, + image, + banner: image, + language, + enclosure_url: image, + enclosure_type: image ? `image/${image.split(/\./).pop()}` : undefined, + enclosure_title: title, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const title = $$('h1.meta-tit a').text(); + const description = + item.description + + art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.entry').html(), + }); item.title = title; - item.description = desc; - item.categeory = categeory; - item.pubDate = parseDate(date.split(' ')[0], 'YYYY-MM-DD'); + item.description = description; + item.pubDate = parseDate($$('p.meta-info').contents().first().text().trim().split(/\s/)[0], 'YYYY-MM-DD'); + item.category = $$('p.meta-info a[rel="category tag"]') + .toArray() + .map((c) => $$(c).text()); + item.content = { + html: description, + text: $$('div.entry').text(), + }; + item.language = language; return item; - }); - - return item; - }) + }) + ) ); + const title = $('title').first().text(); + const image = new URL('wp-content/themes/D7/img/423Down.png', rootUrl).href; + return { - title: `423down-${titleMap[category][type]}`, - link: url, + title, + description: $('title').last().text(), + link: currentUrl, item: items, + allowEmpty: true, + image, + author: title.split(/-/).pop()?.trim(), + language, }; -} +}; + +export const route: Route = { + path: '/:category{.+}?', + name: '423Down', + url: '423down.com', + maintainers: ['nczitzk'], + handler, + example: '/423down', + parameters: { category: '分类,默认为首页,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [Android - 423Down](https://www.423down.com/apk),网址为 \`https://www.423down.com/apk\`。截取 \`https://www.423down.com/\` 到末尾的部分 \`apk\` 作为参数填入,此时路由为 [\`/423down/apk\`](https://rsshub.app/423down/apk)。 +::: + +#### [安卓软件](https://www.423down.com/apk) + +| [安卓软件](https://www.423down.com/apk) | +| --------------------------------------- | +| [apk](https://rsshub.app/423down/apk) | + +#### 电脑软件 + +| [原创软件](https://www.423down.com/zd423) | [媒体播放](https://www.423down.com/multimedia) | [网页浏览](https://www.423down.com/browser) | [图形图像](https://www.423down.com/image) | [聊天软件](https://www.423down.com/im) | +| ----------------------------------------- | --------------------------------------------------- | --------------------------------------------- | ----------------------------------------- | -------------------------------------- | +| [zd423](https://rsshub.app/423down/zd423) | [multimedia](https://rsshub.app/423down/multimedia) | [browser](https://rsshub.app/423down/browser) | [image](https://rsshub.app/423down/image) | [im](https://rsshub.app/423down/im) | + +| [办公软件](https://www.423down.com/work) | [上传下载](https://www.423down.com/down) | [实用软件](https://www.423down.com/softtool) | [系统辅助](https://www.423down.com/systemsoft) | [系统必备](https://www.423down.com/systemplus) | +| ---------------------------------------- | ---------------------------------------- | ----------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | +| [work](https://rsshub.app/423down/work) | [down](https://rsshub.app/423down/down) | [softtool](https://rsshub.app/423down/softtool) | [systemsoft](https://rsshub.app/423down/systemsoft) | [systemplus](https://rsshub.app/423down/systemplus) | + +| [安全软件](https://www.423down.com/security) | [补丁相关](https://www.423down.com/patch) | [硬件相关](https://www.423down.com/hardware) | +| ----------------------------------------------- | ----------------------------------------- | ----------------------------------------------- | +| [security](https://rsshub.app/423down/security) | [patch](https://rsshub.app/423down/patch) | [hardware](https://rsshub.app/423down/hardware) | + +#### 操作系统 + +| [Windows 11](https://www.423down.com/win11) | [Windows 10](https://www.423down.com/win10) | [Windows 7](https://www.423down.com/win7) | [Windows XP](https://www.423down.com/win7/winxp) | [WinPE](https://www.423down.com/pe-system) | +| ------------------------------------------- | ------------------------------------------- | ----------------------------------------- | --------------------------------------------------- | ------------------------------------------------- | +| [win11](https://rsshub.app/423down/win11) | [win10](https://rsshub.app/423down/win10) | [win7](https://rsshub.app/423down/win7) | [win7/winxp](https://rsshub.app/423down/win7/winxp) | [pe-system](https://rsshub.app/423down/pe-system) | + `, + categories: ['program-update'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['423down.com/:category', '423down.com'], + target: (params) => { + const category = params.category; + + return `/423down${category ? `/${category}` : ''}`; + }, + }, + { + title: '首页', + source: ['www.423down.com'], + target: '/', + }, + { + title: '安卓软件', + source: ['www.423down.com/apk'], + target: '/apk', + }, + { + title: '电脑软件 - 原创软件', + source: ['www.423down.com/zd423'], + target: '/zd423', + }, + { + title: '电脑软件 - 媒体播放', + source: ['www.423down.com/multimedia'], + target: '/multimedia', + }, + { + title: '电脑软件 - 网页浏览', + source: ['www.423down.com/browser'], + target: '/browser', + }, + { + title: '电脑软件 - 图形图像', + source: ['www.423down.com/image'], + target: '/image', + }, + { + title: '电脑软件 - 聊天软件', + source: ['www.423down.com/im'], + target: '/im', + }, + { + title: '电脑软件 - 办公软件', + source: ['www.423down.com/work'], + target: '/work', + }, + { + title: '电脑软件 - 上传下载', + source: ['www.423down.com/down'], + target: '/down', + }, + { + title: '电脑软件 - 实用软件', + source: ['www.423down.com/softtool'], + target: '/softtool', + }, + { + title: '电脑软件 - 系统辅助', + source: ['www.423down.com/systemsoft'], + target: '/systemsoft', + }, + { + title: '电脑软件 - 系统必备', + source: ['www.423down.com/systemplus'], + target: '/systemplus', + }, + { + title: '电脑软件 - 安全软件', + source: ['www.423down.com/security'], + target: '/security', + }, + { + title: '电脑软件 - 补丁相关', + source: ['www.423down.com/patch'], + target: '/patch', + }, + { + title: '电脑软件 - 硬件相关', + source: ['www.423down.com/hardware'], + target: '/hardware', + }, + { + title: '操作系统 - Windows 11', + source: ['www.423down.com/win11'], + target: '/win11', + }, + { + title: '操作系统 - Windows 10', + source: ['www.423down.com/win10'], + target: '/win10', + }, + { + title: '操作系统 - Windows 7', + source: ['www.423down.com/win7'], + target: '/win7', + }, + { + title: '操作系统 - Windows XP', + source: ['www.423down.com/win7/winxp'], + target: '/win7/winxp', + }, + { + title: '操作系统 - WinPE', + source: ['www.423down.com/pe-system'], + target: '/pe-system', + }, + ], +}; diff --git a/lib/routes/423down/namespace.ts b/lib/routes/423down/namespace.ts index f72c2e10fb7d61..580ea8bbfa328d 100644 --- a/lib/routes/423down/namespace.ts +++ b/lib/routes/423down/namespace.ts @@ -2,5 +2,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '423Down', - url: 'www.423down.com', + url: '423down.com', + categories: ['program-update'], + description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/423down/templates/description.art b/lib/routes/423down/templates/description.art new file mode 100644 index 00000000000000..249654e7e618a4 --- /dev/null +++ b/lib/routes/423down/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
+ {{ image.alt }} +
+ {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} +
{{ 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; diff --git a/lib/routes/51read/namespace.ts b/lib/routes/51read/namespace.ts new file mode 100644 index 00000000000000..26b64b3c717e78 --- /dev/null +++ b/lib/routes/51read/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '51Read', + url: 'm.51read.org', + lang: 'zh-CN', +}; diff --git a/lib/routes/52hrtt/namespace.ts b/lib/routes/52hrtt/namespace.ts index bbd7776ca3f304..f7c5f126cde083 100644 --- a/lib/routes/52hrtt/namespace.ts +++ b/lib/routes/52hrtt/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '52hrtt 华人头条', url: '52hrtt.com', + lang: 'zh-CN', }; diff --git a/lib/routes/52hrtt/symposium.ts b/lib/routes/52hrtt/symposium.ts index d4d6489ade8614..1beb6a7520754e 100644 --- a/lib/routes/52hrtt/symposium.ts +++ b/lib/routes/52hrtt/symposium.ts @@ -31,9 +31,9 @@ export const route: Route = { 访问 “邱毅看平潭” 专题,会跳转到 \`https://www.52hrtt.com/global/n/w/symposium/F1626082387819\`。其中 \`F1626082387819\` 即为 **专题 id** 对应的地区代码。 - :::tip +::: tip 更多的专题可以点击 [这里](https://www.52hrtt.com/global/n/w/symposium) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/56kog/class.ts b/lib/routes/56kog/class.ts index c0f495647181eb..baeacd026d5f9d 100644 --- a/lib/routes/56kog/class.ts +++ b/lib/routes/56kog/class.ts @@ -19,12 +19,12 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| [玄幻魔法](https://www.56kog.com/class/1_1.html) | [武侠修真](https://www.56kog.com/class/2_1.html) | [历史军事](https://www.56kog.com/class/4_1.html) | [侦探推理](https://www.56kog.com/class/5_1.html) | [网游动漫](https://www.56kog.com/class/6_1.html) | - | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | - | 1\_1 | 2\_1 | 4\_1 | 5\_1 | 6\_1 | +| ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | +| 1\_1 | 2\_1 | 4\_1 | 5\_1 | 6\_1 | - | [恐怖灵异](https://www.56kog.com/class/8_1.html) | [都市言情](https://www.56kog.com/class/3_1.html) | [科幻](https://www.56kog.com/class/7_1.html) | [女生小说](https://www.56kog.com/class/9_1.html) | [其他](https://www.56kog.com/class/10_1.html) | - | ------------------------------------------------ | ------------------------------------------------ | -------------------------------------------- | ------------------------------------------------ | --------------------------------------------- | - | 8\_1 | 3\_1 | 7\_1 | 9\_1 | 10\_1 |`, +| [恐怖灵异](https://www.56kog.com/class/8_1.html) | [都市言情](https://www.56kog.com/class/3_1.html) | [科幻](https://www.56kog.com/class/7_1.html) | [女生小说](https://www.56kog.com/class/9_1.html) | [其他](https://www.56kog.com/class/10_1.html) | +| ------------------------------------------------ | ------------------------------------------------ | -------------------------------------------- | ------------------------------------------------ | --------------------------------------------- | +| 8\_1 | 3\_1 | 7\_1 | 9\_1 | 10\_1 |`, }; async function handler(ctx) { diff --git a/lib/routes/56kog/namespace.ts b/lib/routes/56kog/namespace.ts index 52f077338e58cd..a3e00cb9d6e4dc 100644 --- a/lib/routes/56kog/namespace.ts +++ b/lib/routes/56kog/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '明月中文网', url: '56kog.com', + lang: 'zh-CN', }; diff --git a/lib/routes/56kog/top.ts b/lib/routes/56kog/top.ts index 0f4465f931b318..f4a2b7a9273ab4 100644 --- a/lib/routes/56kog/top.ts +++ b/lib/routes/56kog/top.ts @@ -19,8 +19,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| [周点击榜](https://www.56kog.com/top/weekvisit.html) | [总收藏榜](https://www.56kog.com/top/goodnum.html) | [最新 入库](https://www.56kog.com/top/postdate.html) | - | ---------------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------- | - | weekvisit | goodnum | postdate |`, +| ---------------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------- | +| weekvisit | goodnum | postdate |`, }; async function handler(ctx) { diff --git a/lib/routes/591/list.ts b/lib/routes/591/list.ts index 375e846c2478ee..df20ad6bfc898c 100644 --- a/lib/routes/591/list.ts +++ b/lib/routes/591/list.ts @@ -124,9 +124,9 @@ export const route: Route = { name: 'Rental house', maintainers: ['Yukaii'], handler, - description: `:::tip + description: `::: tip Copy the URL of the 591 filter housing page and remove the front part \`https://rent.591.com.tw/?\`, you will get the query parameters. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/591/namespace.ts b/lib/routes/591/namespace.ts index a27fcf5d575c80..7e5055c3a51985 100644 --- a/lib/routes/591/namespace.ts +++ b/lib/routes/591/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '591 Rental house', url: 'rent.591.com.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/5eplay/index.ts b/lib/routes/5eplay/index.ts index 60e15b73798648..26c76bf4de63da 100644 --- a/lib/routes/5eplay/index.ts +++ b/lib/routes/5eplay/index.ts @@ -1,11 +1,8 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import { load } from 'cheerio'; -import zlib from 'zlib'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; -import { config } from '@/config'; -import { getAcwScV2ByArg1 } from './utils'; export const route: Route = { path: '/article', @@ -34,70 +31,23 @@ export const route: Route = { async function handler() { const rootUrl = 'https://csgo.5eplay.com/'; const apiUrl = `${rootUrl}api/article?page=1&type_id=0&time=0&order_by=0`; - const articleUrl = `${rootUrl}article`; const { data: response } = await got({ method: 'get', url: apiUrl, }); - // get acw_sc__v2 - const acw_sc__v2 = await cache.tryGet( - articleUrl, - async () => { - // Zlib Z_BUF_ERROR: unexpected end of file, should close decompress - const detailResponse = await got( - { - method: 'get', - url: articleUrl, - }, - { - decompress: false, - } - ); - - const unzipData = zlib.createUnzip({ - chunkSize: 20 * 1024, - }); - unzipData.write(detailResponse.body); - - let acw_sc__v2 = ''; - for await (const data of unzipData) { - const strData = data.toString(); - const matches = strData.match(/var arg1='(.*?)';/); - if (matches) { - acw_sc__v2 = getAcwScV2ByArg1(matches[1]); - break; - } - } - return acw_sc__v2; - }, - Math.min(config.cache.routeExpire, 25 * 60), - false - ); - const items = await Promise.all( response.data.list.map((item) => cache.tryGet(item.jump_link, async () => { - if (!acw_sc__v2) { - return { - title: item.title, - description: item.title + (item.images?.[0] ? `` : ''), - pubDate: parseDate(item.dateline * 1000), - link: item.jump_link, - }; - } const { data: detailResponse } = await got({ method: 'get', url: item.jump_link, - headers: { - cookie: `acw_sc__v2=${acw_sc__v2}`, - }, }); const $ = load(detailResponse); const content = $('.article-text'); - const res = []; + const res: string[] = []; content.find('> p, > blockquote').each((i, e) => { res.push($(e).text()); diff --git a/lib/routes/5eplay/namespace.ts b/lib/routes/5eplay/namespace.ts index b48fc273346207..dca7393b9a2bfa 100644 --- a/lib/routes/5eplay/namespace.ts +++ b/lib/routes/5eplay/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '5EPLAY', url: 'csgo.5eplay.com', + lang: 'zh-CN', }; diff --git a/lib/routes/5eplay/utils.ts b/lib/routes/5eplay/utils.ts index d14c5b56db94d8..1e50b25fd159f1 100644 --- a/lib/routes/5eplay/utils.ts +++ b/lib/routes/5eplay/utils.ts @@ -13,10 +13,12 @@ const getAcwScV2ByArg1 = (arg1) => { } return res; }; - const unsbox = function (str) { + const unsbox = function (str: string) { const code = [15, 35, 29, 24, 33, 16, 1, 38, 10, 9, 19, 31, 40, 27, 22, 23, 25, 13, 6, 11, 39, 18, 20, 8, 14, 21, 32, 26, 2, 30, 7, 4, 17, 5, 3, 28, 34, 37, 12, 36]; - const res = []; - for (const [i, cur] of str.entries()) { + const res: string[] = []; + // eslint-disable-next-line unicorn/no-for-loop + for (let i = 0; i < str.length; i++) { + const cur = str[i]; for (const [j, element] of code.entries()) { if (element === i + 1) { res[j] = cur; diff --git a/lib/routes/5music/index.ts b/lib/routes/5music/index.ts new file mode 100644 index 00000000000000..6cae7b716abb83 --- /dev/null +++ b/lib/routes/5music/index.ts @@ -0,0 +1,87 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/new-releases/:category?', + categories: ['shopping'], + example: '/5music/new-releases', + parameters: { category: 'Category, see below, defaults to all' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.5music.com.tw/New_releases.asp', 'www.5music.com.tw/'], + target: '/new-releases', + }, + ], + name: '新貨上架', + maintainers: ['gideonsenku'], + handler, + description: `Categories: +| 華語 | 西洋 | 東洋 | 韓語 | 古典 | +| ---- | ---- | ---- | ---- | ---- | +| A | B | F | M | D |`, + url: 'www.5music.com.tw/New_releases.asp', +}; + +async function handler(ctx) { + const category = ctx.req.param('category') ?? 'A'; + const url = `https://www.5music.com.tw/New_releases.asp?mut=${category}`; + + const { data } = await got(url); + const $ = load(data); + + const items = $('.releases-list .tbody > .box') + .toArray() + .map((item) => { + const $item = $(item); + const cells = $item.find('.td'); + + // 解析艺人名称 (可能包含中英文名) + const artistCell = $(cells[0]); + const artist = artistCell + .find('a') + .toArray() + .map((el) => $(el).text().trim()) + .join(' / '); + + // 解析专辑信息 + const albumCell = $(cells[1]); + const album = albumCell.find('a').first().text().trim(); + const albumLink = albumCell.find('a').first().attr('href'); + + const releaseDate = $(cells[2]).text().trim(); + const company = $(cells[3]).text().trim(); + const format = $(cells[4]).text().trim(); + + return { + title: `${artist} - ${album}`, + description: ` +

艺人: ${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; + +const get = async (url: string, encoding = 'gbk') => new TextDecoder(encoding).decode(await ofetch(url, { responseType: 'arrayBuffer' })); + +const parseObject = (reg: RegExp, str: string): Record => { + const obj = {}; + const match = reg.exec(str); + if (match) { + for (const line of match[0].matchAll(/(\w+):\s?["']?([\S\s]+?)["']?[\n,}]/g)) { + obj[line[1]] = line[2]; + } + } + + return obj; +}; + +const decrypt = (txt: string, articleid: string, chapterid: string, decryptionMap: Record) => { + if (!txt || txt.length < 10) { + return txt; + } + + const lineMap = {}; + const articleKey = Number(articleid) + 3_061_711; + const chapterKey = Number(chapterid) + 3_421_001; + for (const key of Object.keys(decryptionMap)) { + lineMap[(Number(key) ^ chapterKey) - articleKey] = (Number(decryptionMap[key]) ^ chapterKey) - articleKey; + } + + return txt + .replaceAll(/\u2003|\n/g, '') + .split('

') + .flatMap((line, index, array) => (lineMap[index] ? array[lineMap[index]] : line).split('
')) + .slice(1, -2) + .join('

'); +}; diff --git a/lib/routes/69shu/namespace.ts b/lib/routes/69shu/namespace.ts new file mode 100644 index 00000000000000..6c19ebe51938bd --- /dev/null +++ b/lib/routes/69shu/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '69书吧', + url: '69shuba.cx', + lang: 'zh-CN', +}; diff --git a/lib/routes/6park/index.ts b/lib/routes/6park/index.ts index c7c6bfb992f37f..c71e220d52076c 100644 --- a/lib/routes/6park/index.ts +++ b/lib/routes/6park/index.ts @@ -6,16 +6,21 @@ import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: '/:id?/:type?/:keyword?', + path: '/index/:id?/:type?/:keyword?', + name: '首页', + maintainers: ['nczitzk', 'cscnk52'], + handler, + example: '/6park/index', + parameters: { id: '分站,见下表,默认为史海钩沉', type: '类型,可选值为 gold、type,默认为空', keyword: '关键词,可选,默认为空' }, radar: [ { source: ['club.6parkbbs.com/:id/index.php', 'club.6parkbbs.com/'], target: '/:id?', }, ], - name: 'Unknown', - maintainers: [], - handler, + description: `| 婚姻家庭 | 魅力时尚 | 女性频道 | 生活百态 | 美食厨房 | 非常影音 | 车迷沙龙 | 游戏天地 | 卡通漫画 | 体坛纵横 | 运动健身 | 电脑前线 | 数码家电 | 旅游风向 | 摄影部落 | 奇珍异宝 | 笑口常开 | 娱乐八卦 | 吃喝玩乐 | 文化长廊 | 军事纵横 | 百家论坛 | 科技频道 | 爱子情怀 | 健康人生 | 博论天下 | 史海钩沉 | 网际谈兵 | 经济观察 | 谈股论金 | 杂论闲侃 | 唯美乐园 | 学习园地 | 命理玄机 | 宠物情缘 | 网络歌坛 | 音乐殿堂 | 情感世界 | +|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------| +| life9 | life1 | chan10 | life2 | life6 | fr | enter7 | enter3 | enter6 | enter5 | sport | know1 | chan6 | life7 | chan8 | page | enter1 | enter8 | netstar | life10 | nz | other | chan2 | chan5 | life5 | bolun | chan1 | military | finance | chan4 | pk | gz1 | gz2 | gz3 | life8 | chan7 | enter4 | life3 |`, }; async function handler(ctx) { @@ -27,7 +32,7 @@ async function handler(ctx) { const rootUrl = 'https://club.6parkbbs.com'; const indexUrl = `${rootUrl}/${id}/index.php`; - const currentUrl = `${indexUrl}${type === '' || keyword === '' ? '' : type === 'gold' ? '?app=forum&act=gold' : `?action=search&act=threadsearch&app=forum&${type}=${keyword}&submit=${type === 'type' ? '查询' : '栏目搜索'}`}`; + const currentUrl = `${indexUrl}${type === '' || keyword === '' ? '' : (type === 'gold' ? '?app=forum&act=gold' : `?action=search&act=threadsearch&app=forum&${type}=${keyword}&submit=${type === 'type' ? '查询' : '栏目搜索'}`)}`; const response = await got({ method: 'get', diff --git a/lib/routes/6park/namespace.ts b/lib/routes/6park/namespace.ts index f549c08fd6002e..5664e409c7aead 100644 --- a/lib/routes/6park/namespace.ts +++ b/lib/routes/6park/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '留园网', url: 'club.6parkbbs.com', + lang: 'zh-CN', }; diff --git a/lib/routes/6park/news.ts b/lib/routes/6park/news.ts index ae6c0ea1c2ea4a..1f65ce6de3ac5d 100644 --- a/lib/routes/6park/news.ts +++ b/lib/routes/6park/news.ts @@ -13,8 +13,17 @@ export const route: Route = { target: '/:id?', }, ], - name: 'Unknown', - maintainers: [], + name: '新闻栏目', + maintainers: ['nczitzk', 'cscnk52'], + parameters: { + site: '分站,可选newspark、local,默认为 newspark', + id: '栏目 id,可选,默认为空', + keyword: '关键词,可选,默认为空', + }, + description: `::: tip 提示 +若订阅 [时政](https://www.6parknews.com/newspark/index.php?type=1),其网址为 ,其中 \`newspark\` 为分站,\`1\` 为栏目 id。 +若订阅 [美国](https://local.6parknews.com/index.php?type_id=1),其网址为 ,其中 \`local\` 为分站,\`1\` 为栏目 id。 +:::`, handler, }; diff --git a/lib/routes/6v123/latest-movies.ts b/lib/routes/6v123/latest-movies.ts index 25de6cfb349a50..760338075581c8 100644 --- a/lib/routes/6v123/latest-movies.ts +++ b/lib/routes/6v123/latest-movies.ts @@ -28,7 +28,7 @@ export const route: Route = { }; async function handler(ctx) { - const item = await processItems(ctx, baseURL); + const item = await processItems(ctx, baseURL, [/第*集/, /第*季/, /(ep)\d+/i, /(s)\d+/i, /更新/]); return { title: '6v电影-最新电影', diff --git a/lib/routes/6v123/latest-tvseries.ts b/lib/routes/6v123/latest-tvseries.ts index b9f1dd64d02171..133f5d6992bdde 100644 --- a/lib/routes/6v123/latest-tvseries.ts +++ b/lib/routes/6v123/latest-tvseries.ts @@ -28,7 +28,7 @@ export const route: Route = { }; async function handler(ctx) { - const item = await processItems(ctx, baseURL); + const item = await processItems(ctx, baseURL, []); return { title: '6v电影-最新电影', diff --git a/lib/routes/6v123/namespace.ts b/lib/routes/6v123/namespace.ts index 6a83d25a41b67c..915540500262b3 100644 --- a/lib/routes/6v123/namespace.ts +++ b/lib/routes/6v123/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '6v 电影', url: 'hao6v.cc', + lang: 'zh-CN', }; diff --git a/lib/routes/6v123/utils.ts b/lib/routes/6v123/utils.ts index 72ccebc62b501c..d1425e8c58e4f7 100644 --- a/lib/routes/6v123/utils.ts +++ b/lib/routes/6v123/utils.ts @@ -5,80 +5,82 @@ import { load } from 'cheerio'; import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; -const utils = { - loadDetailPage: async function loadDetailPage(link) { - const response = await got.get(link, { - responseType: 'buffer', - }); - response.data = iconv.decode(response.data, 'gb2312'); +export async function loadDetailPage(link) { + const response = await got.get(link, { + responseType: 'buffer', + }); + response.data = iconv.decode(response.data, 'gb2312'); - const $ = load(response.data); + const $ = load(response.data); - const detailInfo = { - title: $('title') - .text() - .replaceAll(/,免费下载,迅雷下载|,6v电影/g, ''), - description: $('meta[name="description"]').attr('content'), - enclosure_urls: $('table td') - .map((i, e) => ({ - title: $(e).text().replace('磁力:', ''), - magnet: $(e).find('a').attr('href'), - })) - .toArray() - .filter((item) => item.magnet?.includes('magnet')), - }; - return detailInfo; - }, - processItems: async (ctx, baseURL) => { - const response = await got.get(baseURL, { - responseType: 'buffer', - }); - response.data = iconv.decode(response.data, 'gb2312'); + return { + title: $('title') + .text() + .replaceAll(/,免费下载,迅雷下载|,6v电影/g, ''), + description: $('meta[name="description"]').attr('content'), + enclosure_urls: $('table td') + .toArray() + .map((e) => ({ + title: $(e).text().replace('磁力:', ''), + magnet: $(e).find('a').attr('href'), + })) + .filter((item) => item.magnet?.includes('magnet')), + }; +} - const $ = load(response.data); - const list = $('ul.list')[0].children; +export async function processItems(ctx, baseURL, exclude) { + const response = await got.get(baseURL, { + responseType: 'buffer', + }); + response.data = iconv.decode(response.data, 'gb2312'); - const process = await Promise.all( - list.map((item) => { - const link = $(item).find('a'); - const href = link.attr('href'); - const pubDate = timezone(parseDate($(item).find('span').text().replaceAll(/[[\]]/g, ''), 'MM-DD'), +8); + const $ = load(response.data); + const list = $('ul.list')[0].children; - if (href === undefined) { - return; - } + const process = await Promise.all( + list.map((item) => { + const link = $(item).find('a'); + const href = link.attr('href'); + const pubDate = timezone(parseDate($(item).find('span').text().replaceAll(/[[\]]/g, ''), 'MM-DD'), +8); + const text = link.text(); + + if (href === undefined) { + return; + } - const itemUrl = 'https://www.hao6v.cc' + link.attr('href'); + if (exclude && exclude.some((e) => e.test(text))) { + // 过滤掉满足正则的标题条目 + return; + } - return cache.tryGet(itemUrl, async () => { - const detailInfo = await utils.loadDetailPage(itemUrl); + const itemUrl = 'https://www.hao6v.cc' + link.attr('href'); - if (detailInfo.enclosure_urls.length > 1) { - return detailInfo.enclosure_urls.map((url) => ({ - enclosure_url: url.magnet, - enclosure_type: 'application/x-bittorrent', - title: `${link.text()} ( ${url.title} )`, - description: detailInfo.description, - pubDate, - link: itemUrl, - guid: `${itemUrl}#${url.title}`, - })); - } + return cache.tryGet(itemUrl, async () => { + const detailInfo = await loadDetailPage(itemUrl); - return { - enclosure_url: detailInfo.enclosure_urls.length === 0 ? '' : detailInfo.enclosure_urls[0].magnet, + if (detailInfo.enclosure_urls.length > 1) { + return detailInfo.enclosure_urls.map((url) => ({ + enclosure_url: url.magnet, enclosure_type: 'application/x-bittorrent', - title: link.text(), + title: `${link.text()} ( ${url.title} )`, description: detailInfo.description, pubDate, link: itemUrl, - }; - }); - }) - ); + guid: `${itemUrl}#${url.title}`, + })); + } - return process.filter((item) => item !== undefined).flat(); - }, -}; + return { + enclosure_url: detailInfo.enclosure_urls.length === 0 ? '' : detailInfo.enclosure_urls[0].magnet, + enclosure_type: 'application/x-bittorrent', + title: link.text(), + description: detailInfo.description, + pubDate, + link: itemUrl, + }; + }); + }) + ); -export default utils; + return process.filter((item) => item !== undefined).flat(); +} diff --git a/lib/routes/78dm/index.ts b/lib/routes/78dm/index.ts index 490990fa1f888a..2db355d7e07b81 100644 --- a/lib/routes/78dm/index.ts +++ b/lib/routes/78dm/index.ts @@ -2,7 +2,6 @@ import { Route } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); -import { getSubPath } from '@/utils/common-utils'; import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; @@ -11,77 +10,464 @@ import { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; import path from 'node:path'; -export const route: Route = { - path: '*', - name: 'Unknown', - maintainers: [], - handler, -}; +export const handler = async (ctx) => { + const { category = 'news' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; -async function handler(ctx) { const rootUrl = 'https://www.78dm.net'; - const currentUrl = `${rootUrl}${getSubPath(ctx) === '/' ? '/news' : /\/\d+$/.test(getSubPath(ctx)) ? `${getSubPath(ctx)}.html` : getSubPath(ctx)}`; + const currentUrl = new URL(category.includes('/') ? `${category}.html` : category, rootUrl).href; + + const { data: response } = await got(currentUrl); - const response = await got({ - method: 'get', - url: currentUrl, - }); + const $ = load(response); - const $ = load(response.data); + const language = $('html').prop('lang'); - let items = $('.card-title') - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 30) + let items = $('section.box-content div.card a.card-title') + .slice(0, limit) .toArray() .map((item) => { - item = $(item); + item = $(item).parent(); + + const title = item.find('a.card-title').text(); + + const src = item.find('a.card-image img').prop('data-src'); + const image = src?.startsWith('//') ? `https:${src}` : src; + + const description = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + }); + const pubDate = item.find('div.card-info span.item').last().text(); - const link = item.attr('href'); + const href = item.find('a.card-title').prop('href'); return { - title: item.text(), - link: /^\/\//.test(link) ? `https:${link}` : link, + title, + description, + pubDate: pubDate && /\d{4}(?:\.\d{2}){2}\s\d{2}:\d{2}/.test(pubDate) ? timezone(parseDate(pubDate, 'YYYY.MM.DD HH:mm'), +8) : undefined, + link: href?.startsWith('//') ? `https:${href}` : href, + category: [ + ...new Set([ + ...item + .find('span.tag-title') + .toArray() + .map((c) => $(c).text()), + item.find('div.card-info span.item').first().text(), + ]), + ].filter(Boolean), + image, + banner: image, + 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); + + const $$ = load(detailResponse); + + $$('i.p-status').remove(); + + $$('div.image-text-content p img.lazy').each((_, el) => { + el = $$(el); - const content = load(detailResponse.data); + const src = el.prop('data-src'); + const image = src?.startsWith('//') ? `https:${src}` : src; - content('.tag, .level').remove(); - content('.lazy').each(function () { - content(this).replaceWith( - art(path.join(__dirname, 'templates/image.art'), { - image: content(this).attr('data-src'), + el.parent().replaceWith( + art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: el.prop('title') ?? '', + }, + ] + : undefined, }) ); }); - item.author = content('.push-username').first().text().split('楼主')[0]; - item.pubDate = timezone( - parseDate( - content('.push-time') - .first() - .text() - .match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2})/)[1] - ), - +8 - ); - item.description = content('.image-text-content').first().html(); + const title = $$('h2.title').text(); + const description = + item.description + + art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.image-text-content').first().html(), + }); + + item.title = title; + item.description = description; + item.pubDate = timezone(parseDate($$('p.push-time').text().split(/:/).pop()), +8); + item.author = $$('a.push-username').contents().first().text(); + item.content = { + html: description, + text: $$('div.image-text-content').first().text(), + }; + item.language = language; return item; }) ) ); + const title = $('title').text(); + const image = new URL($('a.logo img').prop('src'), rootUrl).href; + return { - title: `78动漫 - ${$('title').text().split('_')[0]} - ${$('.actived').first().text()}`, + title: `${title} | ${$('div.actived').text()}`, + description: $('meta[name="description"]').prop('content'), link: currentUrl, item: items, + allowEmpty: true, + image, + author: $('meta[property="og:site_name"]').prop('content'), + language, }; -} +}; + +export const route: Route = { + path: '/:category{.+}?', + name: '分类', + url: '78dm.net', + maintainers: ['nczitzk'], + handler, + example: '/78dm/news', + parameters: { category: '分类,默认为 `news`,即新品速递,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [新品速递](https://www.78dm.net/news),网址为 \`https://www.78dm.net/news\`。截取 \`https://www.78dm.net/\` 到末尾的部分 \`news\` 作为参数填入,此时路由为 [\`/78dm/news\`](https://rsshub.app/78dm/news)。 + + 若订阅 [精彩评测 - 变形金刚](https://www.78dm.net/eval_list/109/0/0/1.html),网址为 \`https://www.78dm.net/eval_list/109/0/0/1.html\`。截取 \`https://www.78dm.net/\` 到末尾 \`.html\` 的部分 \`eval_list/109/0/0/1\` 作为参数填入,此时路由为 [\`/78dm/eval_list/109/0/0/1\`](https://rsshub.app/78dm/eval_list/109/0/0/1)。 +::: + +
+更多分类 + +#### [新品速递](https://www.78dm.net/news) + +| 分类 | ID | +| -------------------------------------------------------------- | ---------------------------------------------------------------------- | +| [全部](https://www.78dm.net/news/0/0/0/0/0/0/0/1.html) | [news/0/0/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/0/0/0/0/0/0/1) | +| [变形金刚](https://www.78dm.net/news/3/0/0/0/0/0/0/1.html) | [news/3/0/0/0/0/0/0/1](https://rsshub.app/78dm/news/3/0/0/0/0/0/0/1) | +| [高达](https://www.78dm.net/news/4/0/0/0/0/0/0/1.html) | [news/4/0/0/0/0/0/0/1](https://rsshub.app/78dm/news/4/0/0/0/0/0/0/1) | +| [圣斗士](https://www.78dm.net/news/2/0/0/0/0/0/0/1.html) | [news/2/0/0/0/0/0/0/1](https://rsshub.app/78dm/news/2/0/0/0/0/0/0/1) | +| [海贼王](https://www.78dm.net/news/8/0/0/0/0/0/0/1.html) | [news/8/0/0/0/0/0/0/1](https://rsshub.app/78dm/news/8/0/0/0/0/0/0/1) | +| [PVC 手办](https://www.78dm.net/news/0/5/0/0/0/0/0/1.html) | [news/0/5/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/5/0/0/0/0/0/1) | +| [拼装模型](https://www.78dm.net/news/0/1/0/0/0/0/0/1.html) | [news/0/1/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/1/0/0/0/0/0/1) | +| [机甲成品](https://www.78dm.net/news/0/2/0/0/0/0/0/1.html) | [news/0/2/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/2/0/0/0/0/0/1) | +| [特摄](https://www.78dm.net/news/0/3/0/0/0/0/0/1.html) | [news/0/3/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/3/0/0/0/0/0/1) | +| [美系](https://www.78dm.net/news/0/4/0/0/0/0/0/1.html) | [news/0/4/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/4/0/0/0/0/0/1) | +| [GK](https://www.78dm.net/news/0/6/0/0/0/0/0/1.html) | [news/0/6/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/6/0/0/0/0/0/1) | +| [扭蛋盒蛋食玩](https://www.78dm.net/news/0/7/0/0/0/0/0/1.html) | [news/0/7/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/7/0/0/0/0/0/1) | +| [其他](https://www.78dm.net/news/0/8/0/0/0/0/0/1.html) | [news/0/8/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/8/0/0/0/0/0/1) | +| [综合](https://www.78dm.net/news/0/9/0/0/0/0/0/1.html) | [news/0/9/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/9/0/0/0/0/0/1) | +| [军模](https://www.78dm.net/news/0/10/0/0/0/0/0/1.html) | [news/0/10/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/10/0/0/0/0/0/1) | +| [民用](https://www.78dm.net/news/0/11/0/0/0/0/0/1.html) | [news/0/11/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/11/0/0/0/0/0/1) | +| [配件](https://www.78dm.net/news/0/12/0/0/0/0/0/1.html) | [news/0/12/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/12/0/0/0/0/0/1) | +| [工具](https://www.78dm.net/news/0/13/0/0/0/0/0/1.html) | [news/0/13/0/0/0/0/0/1](https://rsshub.app/78dm/news/0/13/0/0/0/0/0/1) | + +#### [精彩评测](https://www.78dm.net/eval_list) + +| 分类 | ID | +| --------------------------------------------------------- | ------------------------------------------------------------------ | +| [全部](https://www.78dm.net/eval_list/0/0/0/1.html) | [eval_list/0/0/0/1](https://rsshub.app/78dm/eval_list/0/0/0/1) | +| [变形金刚](https://www.78dm.net/eval_list/109/0/0/1.html) | [eval_list/109/0/0/1](https://rsshub.app/78dm/eval_list/109/0/0/1) | +| [高达](https://www.78dm.net/eval_list/110/0/0/1.html) | [eval_list/110/0/0/1](https://rsshub.app/78dm/eval_list/110/0/0/1) | +| [圣斗士](https://www.78dm.net/eval_list/111/0/0/1.html) | [eval_list/111/0/0/1](https://rsshub.app/78dm/eval_list/111/0/0/1) | +| [海贼王](https://www.78dm.net/eval_list/112/0/0/1.html) | [eval_list/112/0/0/1](https://rsshub.app/78dm/eval_list/112/0/0/1) | +| [PVC 手办](https://www.78dm.net/eval_list/115/0/0/1.html) | [eval_list/115/0/0/1](https://rsshub.app/78dm/eval_list/115/0/0/1) | +| [拼装模型](https://www.78dm.net/eval_list/113/0/0/1.html) | [eval_list/113/0/0/1](https://rsshub.app/78dm/eval_list/113/0/0/1) | +| [机甲成品](https://www.78dm.net/eval_list/114/0/0/1.html) | [eval_list/114/0/0/1](https://rsshub.app/78dm/eval_list/114/0/0/1) | +| [特摄](https://www.78dm.net/eval_list/116/0/0/1.html) | [eval_list/116/0/0/1](https://rsshub.app/78dm/eval_list/116/0/0/1) | +| [美系](https://www.78dm.net/eval_list/117/0/0/1.html) | [eval_list/117/0/0/1](https://rsshub.app/78dm/eval_list/117/0/0/1) | +| [GK](https://www.78dm.net/eval_list/118/0/0/1.html) | [eval_list/118/0/0/1](https://rsshub.app/78dm/eval_list/118/0/0/1) | +| [综合](https://www.78dm.net/eval_list/120/0/0/1.html) | [eval_list/120/0/0/1](https://rsshub.app/78dm/eval_list/120/0/0/1) | + +#### [好贴推荐](https://www.78dm.net/ht_list) + +| 分类 | ID | +| ------------------------------------------------------- | -------------------------------------------------------------- | +| [全部](https://www.78dm.net/ht_list/0/0/0/1.html) | [ht_list/0/0/0/1](https://rsshub.app/78dm/ht_list/0/0/0/1) | +| [变形金刚](https://www.78dm.net/ht_list/95/0/0/1.html) | [ht_list/95/0/0/1](https://rsshub.app/78dm/ht_list/95/0/0/1) | +| [高达](https://www.78dm.net/ht_list/96/0/0/1.html) | [ht_list/96/0/0/1](https://rsshub.app/78dm/ht_list/96/0/0/1) | +| [圣斗士](https://www.78dm.net/ht_list/98/0/0/1.html) | [ht_list/98/0/0/1](https://rsshub.app/78dm/ht_list/98/0/0/1) | +| [海贼王](https://www.78dm.net/ht_list/99/0/0/1.html) | [ht_list/99/0/0/1](https://rsshub.app/78dm/ht_list/99/0/0/1) | +| [PVC 手办](https://www.78dm.net/ht_list/100/0/0/1.html) | [ht_list/100/0/0/1](https://rsshub.app/78dm/ht_list/100/0/0/1) | +| [拼装模型](https://www.78dm.net/ht_list/101/0/0/1.html) | [ht_list/101/0/0/1](https://rsshub.app/78dm/ht_list/101/0/0/1) | +| [机甲成品](https://www.78dm.net/ht_list/102/0/0/1.html) | [ht_list/102/0/0/1](https://rsshub.app/78dm/ht_list/102/0/0/1) | +| [特摄](https://www.78dm.net/ht_list/103/0/0/1.html) | [ht_list/103/0/0/1](https://rsshub.app/78dm/ht_list/103/0/0/1) | +| [美系](https://www.78dm.net/ht_list/104/0/0/1.html) | [ht_list/104/0/0/1](https://rsshub.app/78dm/ht_list/104/0/0/1) | +| [GK](https://www.78dm.net/ht_list/105/0/0/1.html) | [ht_list/105/0/0/1](https://rsshub.app/78dm/ht_list/105/0/0/1) | +| [综合](https://www.78dm.net/ht_list/107/0/0/1.html) | [ht_list/107/0/0/1](https://rsshub.app/78dm/ht_list/107/0/0/1) | +| [装甲战车](https://www.78dm.net/ht_list/131/0/0/1.html) | [ht_list/131/0/0/1](https://rsshub.app/78dm/ht_list/131/0/0/1) | +| [舰船模型](https://www.78dm.net/ht_list/132/0/0/1.html) | [ht_list/132/0/0/1](https://rsshub.app/78dm/ht_list/132/0/0/1) | +| [飞机模型](https://www.78dm.net/ht_list/133/0/0/1.html) | [ht_list/133/0/0/1](https://rsshub.app/78dm/ht_list/133/0/0/1) | +| [民用模型](https://www.78dm.net/ht_list/134/0/0/1.html) | [ht_list/134/0/0/1](https://rsshub.app/78dm/ht_list/134/0/0/1) | +| [兵人模型](https://www.78dm.net/ht_list/135/0/0/1.html) | [ht_list/135/0/0/1](https://rsshub.app/78dm/ht_list/135/0/0/1) | +
+ `, + categories: ['new-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.78dm.net/:category?'], + target: (params) => { + const category = params.category?.replace(/\.html$/, ''); + + return `/78dm${category ? `/${category}` : ''}`; + }, + }, + { + title: '新品速递 - 全部', + source: ['www.78dm.net/news/0/0/0/0/0/0/0/1.html'], + target: '/news/0/0/0/0/0/0/0/1', + }, + { + title: '新品速递 - 变形金刚', + source: ['www.78dm.net/news/3/0/0/0/0/0/0/1.html'], + target: '/news/3/0/0/0/0/0/0/1', + }, + { + title: '新品速递 - 高达', + source: ['www.78dm.net/news/4/0/0/0/0/0/0/1.html'], + target: '/news/4/0/0/0/0/0/0/1', + }, + { + title: '新品速递 - 圣斗士', + source: ['www.78dm.net/news/2/0/0/0/0/0/0/1.html'], + target: '/news/2/0/0/0/0/0/0/1', + }, + { + title: '新品速递 - 海贼王', + source: ['www.78dm.net/news/8/0/0/0/0/0/0/1.html'], + target: '/news/8/0/0/0/0/0/0/1', + }, + { + title: '新品速递 - PVC手办', + source: ['www.78dm.net/news/0/5/0/0/0/0/0/1.html'], + target: '/news/0/5/0/0/0/0/0/1', + }, + { + title: '新品速递 - 拼装模型', + source: ['www.78dm.net/news/0/1/0/0/0/0/0/1.html'], + target: '/news/0/1/0/0/0/0/0/1', + }, + { + title: '新品速递 - 机甲成品', + source: ['www.78dm.net/news/0/2/0/0/0/0/0/1.html'], + target: '/news/0/2/0/0/0/0/0/1', + }, + { + title: '新品速递 - 特摄', + source: ['www.78dm.net/news/0/3/0/0/0/0/0/1.html'], + target: '/news/0/3/0/0/0/0/0/1', + }, + { + title: '新品速递 - 美系', + source: ['www.78dm.net/news/0/4/0/0/0/0/0/1.html'], + target: '/news/0/4/0/0/0/0/0/1', + }, + { + title: '新品速递 - GK', + source: ['www.78dm.net/news/0/6/0/0/0/0/0/1.html'], + target: '/news/0/6/0/0/0/0/0/1', + }, + { + title: '新品速递 - 扭蛋盒蛋食玩', + source: ['www.78dm.net/news/0/7/0/0/0/0/0/1.html'], + target: '/news/0/7/0/0/0/0/0/1', + }, + { + title: '新品速递 - 其他', + source: ['www.78dm.net/news/0/8/0/0/0/0/0/1.html'], + target: '/news/0/8/0/0/0/0/0/1', + }, + { + title: '新品速递 - 综合', + source: ['www.78dm.net/news/0/9/0/0/0/0/0/1.html'], + target: '/news/0/9/0/0/0/0/0/1', + }, + { + title: '新品速递 - 军模', + source: ['www.78dm.net/news/0/10/0/0/0/0/0/1.html'], + target: '/news/0/10/0/0/0/0/0/1', + }, + { + title: '新品速递 - 民用', + source: ['www.78dm.net/news/0/11/0/0/0/0/0/1.html'], + target: '/news/0/11/0/0/0/0/0/1', + }, + { + title: '新品速递 - 配件', + source: ['www.78dm.net/news/0/12/0/0/0/0/0/1.html'], + target: '/news/0/12/0/0/0/0/0/1', + }, + { + title: '新品速递 - 工具', + source: ['www.78dm.net/news/0/13/0/0/0/0/0/1.html'], + target: '/news/0/13/0/0/0/0/0/1', + }, + { + title: '精彩评测 - 全部', + source: ['www.78dm.net/eval_list/0/0/0/1.html'], + target: '/eval_list/0/0/0/1', + }, + { + title: '精彩评测 - 变形金刚', + source: ['www.78dm.net/eval_list/109/0/0/1.html'], + target: '/eval_list/109/0/0/1', + }, + { + title: '精彩评测 - 高达', + source: ['www.78dm.net/eval_list/110/0/0/1.html'], + target: '/eval_list/110/0/0/1', + }, + { + title: '精彩评测 - 圣斗士', + source: ['www.78dm.net/eval_list/111/0/0/1.html'], + target: '/eval_list/111/0/0/1', + }, + { + title: '精彩评测 - 海贼王', + source: ['www.78dm.net/eval_list/112/0/0/1.html'], + target: '/eval_list/112/0/0/1', + }, + { + title: '精彩评测 - PVC手办', + source: ['www.78dm.net/eval_list/115/0/0/1.html'], + target: '/eval_list/115/0/0/1', + }, + { + title: '精彩评测 - 拼装模型', + source: ['www.78dm.net/eval_list/113/0/0/1.html'], + target: '/eval_list/113/0/0/1', + }, + { + title: '精彩评测 - 机甲成品', + source: ['www.78dm.net/eval_list/114/0/0/1.html'], + target: '/eval_list/114/0/0/1', + }, + { + title: '精彩评测 - 特摄', + source: ['www.78dm.net/eval_list/116/0/0/1.html'], + target: '/eval_list/116/0/0/1', + }, + { + title: '精彩评测 - 美系', + source: ['www.78dm.net/eval_list/117/0/0/1.html'], + target: '/eval_list/117/0/0/1', + }, + { + title: '精彩评测 - GK', + source: ['www.78dm.net/eval_list/118/0/0/1.html'], + target: '/eval_list/118/0/0/1', + }, + { + title: '精彩评测 - 综合', + source: ['www.78dm.net/eval_list/120/0/0/1.html'], + target: '/eval_list/120/0/0/1', + }, + { + title: '好贴推荐 - 全部', + source: ['www.78dm.net/ht_list/0/0/0/1.html'], + target: '/ht_list/0/0/0/1', + }, + { + title: '好贴推荐 - 变形金刚', + source: ['www.78dm.net/ht_list/95/0/0/1.html'], + target: '/ht_list/95/0/0/1', + }, + { + title: '好贴推荐 - 高达', + source: ['www.78dm.net/ht_list/96/0/0/1.html'], + target: '/ht_list/96/0/0/1', + }, + { + title: '好贴推荐 - 圣斗士', + source: ['www.78dm.net/ht_list/98/0/0/1.html'], + target: '/ht_list/98/0/0/1', + }, + { + title: '好贴推荐 - 海贼王', + source: ['www.78dm.net/ht_list/99/0/0/1.html'], + target: '/ht_list/99/0/0/1', + }, + { + title: '好贴推荐 - PVC手办', + source: ['www.78dm.net/ht_list/100/0/0/1.html'], + target: '/ht_list/100/0/0/1', + }, + { + title: '好贴推荐 - 拼装模型', + source: ['www.78dm.net/ht_list/101/0/0/1.html'], + target: '/ht_list/101/0/0/1', + }, + { + title: '好贴推荐 - 机甲成品', + source: ['www.78dm.net/ht_list/102/0/0/1.html'], + target: '/ht_list/102/0/0/1', + }, + { + title: '好贴推荐 - 特摄', + source: ['www.78dm.net/ht_list/103/0/0/1.html'], + target: '/ht_list/103/0/0/1', + }, + { + title: '好贴推荐 - 美系', + source: ['www.78dm.net/ht_list/104/0/0/1.html'], + target: '/ht_list/104/0/0/1', + }, + { + title: '好贴推荐 - GK', + source: ['www.78dm.net/ht_list/105/0/0/1.html'], + target: '/ht_list/105/0/0/1', + }, + { + title: '好贴推荐 - 综合', + source: ['www.78dm.net/ht_list/107/0/0/1.html'], + target: '/ht_list/107/0/0/1', + }, + { + title: '好贴推荐 - 装甲战车', + source: ['www.78dm.net/ht_list/131/0/0/1.html'], + target: '/ht_list/131/0/0/1', + }, + { + title: '好贴推荐 - 舰船模型', + source: ['www.78dm.net/ht_list/132/0/0/1.html'], + target: '/ht_list/132/0/0/1', + }, + { + title: '好贴推荐 - 飞机模型', + source: ['www.78dm.net/ht_list/133/0/0/1.html'], + target: '/ht_list/133/0/0/1', + }, + { + title: '好贴推荐 - 民用模型', + source: ['www.78dm.net/ht_list/134/0/0/1.html'], + target: '/ht_list/134/0/0/1', + }, + { + title: '好贴推荐 - 兵人模型', + source: ['www.78dm.net/ht_list/135/0/0/1.html'], + target: '/ht_list/135/0/0/1', + }, + ], +}; diff --git a/lib/routes/78dm/namespace.ts b/lib/routes/78dm/namespace.ts index b73666bb457b61..2fa777e3cefd30 100644 --- a/lib/routes/78dm/namespace.ts +++ b/lib/routes/78dm/namespace.ts @@ -3,4 +3,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '78 动漫', url: '78dm.net', + categories: ['anime'], + description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/78dm/templates/description.art b/lib/routes/78dm/templates/description.art new file mode 100644 index 00000000000000..dfab19230c1108 --- /dev/null +++ b/lib/routes/78dm/templates/description.art @@ -0,0 +1,17 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
+ {{ image.alt }} +
+ {{ /if }} + {{ /each }} +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/78dm/templates/image.art b/lib/routes/78dm/templates/image.art deleted file mode 100644 index 929353dbbd7ddf..00000000000000 --- a/lib/routes/78dm/templates/image.art +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/lib/routes/7mmtv/index.ts b/lib/routes/7mmtv/index.ts index c50d55e3b593f8..170f74f03038d6 100644 --- a/lib/routes/7mmtv/index.ts +++ b/lib/routes/7mmtv/index.ts @@ -27,25 +27,25 @@ export const route: Route = { handler, description: `**Language** - | English | 日本語 | 한국의 | 中文 | - | ------- | ------ | ------ | ---- | - | en | ja | ko | zh | +| English | 日本語 | 한국의 | 中文 | +| ------- | ------ | ------ | ---- | +| en | ja | ko | zh | **Category** - | Chinese subtitles AV | Censored | Amateur | Uncensored | Asian self-timer | H comics | - | -------------------- | -------------- | ---------------- | ---------------- | ---------------- | ------------ | - | chinese\_list | censored\_list | amateurjav\_list | uncensored\_list | amateur\_list | hcomic\_list | +| Chinese subtitles AV | Censored | Amateur | Uncensored | Asian self-timer | H comics | +| -------------------- | -------------- | ---------------- | ---------------- | ---------------- | ------------ | +| chinese\_list | censored\_list | amateurjav\_list | uncensored\_list | amateur\_list | hcomic\_list | - | Chinese subtitles AV random | Censored random | Amateur random | Uncensored random | Asian self-timer random | H comics random | - | --------------------------- | ---------------- | ------------------ | ------------------ | ----------------------- | --------------- | - | chinese\_random | censored\_random | amateurjav\_random | uncensored\_random | amateur\_random | hcomic\_random | +| Chinese subtitles AV random | Censored random | Amateur random | Uncensored random | Asian self-timer random | H comics random | +| --------------------------- | ---------------- | ------------------ | ------------------ | ----------------------- | --------------- | +| chinese\_random | censored\_random | amateurjav\_random | uncensored\_random | amateur\_random | hcomic\_random | **Server** - | All Server | fembed(Full DL) | streamsb(Full DL) | doodstream | streamtape(Full DL) | avgle | embedgram | videovard(Full DL) | - | ---------- | --------------- | ----------------- | ---------- | ------------------- | ----- | --------- | ------------------ | - | all | 21 | 30 | 28 | 29 | 17 | 34 | 33 |`, +| All Server | fembed(Full DL) | streamsb(Full DL) | doodstream | streamtape(Full DL) | avgle | embedgram | videovard(Full DL) | +| ---------- | --------------- | ----------------- | ---------- | ------------------- | ----- | --------- | ------------------ | +| all | 21 | 30 | 28 | 29 | 17 | 34 | 33 |`, }; async function handler(ctx) { diff --git a/lib/routes/7mmtv/namespace.ts b/lib/routes/7mmtv/namespace.ts index b3896a64ea330c..30daaeef3d2835 100644 --- a/lib/routes/7mmtv/namespace.ts +++ b/lib/routes/7mmtv/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '7mmtv', url: '7mmtv.tv', + lang: 'zh-CN', }; diff --git a/lib/routes/81/81rc/index.ts b/lib/routes/81/81rc/index.ts index fe56b0af56fc7b..e702e9bb0aec68 100644 --- a/lib/routes/81/81rc/index.ts +++ b/lib/routes/81/81rc/index.ts @@ -1,41 +1,35 @@ import { Route } from '@/types'; -import { getSubPath } from '@/utils/common-utils'; + import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; -export const route: Route = { - path: '/81rc/*', - name: 'Unknown', - maintainers: [], - handler, -}; - -async function handler(ctx) { - const thePath = getSubPath(ctx).replace(/^\/81rc/, ''); +export const handler = async (ctx) => { + const { category = 'sy/gzdt_210283' } = ctx.req.param(); const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; const rootUrl = 'https://81rc.81.cn'; - - // The default is http://81rc.81.cn/sy/gzdt_210283. - const currentUrl = new URL(thePath || '/sy/gzdt_210283', rootUrl).href; + const currentUrl = new URL(category?.endsWith('/') ? `${category}/` : category, rootUrl).href; const { data: response } = await got(currentUrl); const $ = load(response); - let items = $('div.left-news ul li a') + const language = $('html').prop('lang'); + + let items = $('div.left-news ul li') .slice(0, limit) .toArray() .map((item) => { item = $(item); return { - title: item.text(), - link: new URL(item.prop('href'), rootUrl).href, - pubDate: timezone(parseDate(item.parent().find('span').text()), +8), + title: item.find('a').text(), + pubDate: timezone(parseDate(item.find('span').text()), +8), + link: item.find('a').prop('href'), + language, }; }); @@ -44,29 +38,71 @@ async function handler(ctx) { cache.tryGet(item.link, async () => { const { data: detailResponse } = await got(item.link); - const content = load(detailResponse); + const $$ = load(detailResponse); - item.description = content('.txt').html(); - item.author = content('meta[name="reporter"]').prop('content') || content('meta[name="author"]').prop('content'); + const description = $$('div.txt').html(); + + item.title = $$('h2').text(); + item.description = description; + item.pubDate = timezone(parseDate($$('div.time span').last().text()), +8); + item.author = $$('div.time span').first().text(); + item.content = { + html: description, + text: $$('div.txt').text(), + }; + item.language = language; return item; }) ) ); - const icon = $('link[rel="icon"]').prop('href'); + const title = $('title').text(); + const image = new URL('template/tenant207/t582/new.jpg', rootUrl).href; return { - item: items, - title: `军队人才网 - ${$('div.left-word') - .find('a') - .toArray() - .map((a) => $(a).text()) - .filter((a) => a !== '首页') - .join(' - ')}`, + title, + description: $('div.time').contents().first().text(), link: currentUrl, - language: 'zh-cn', - icon, - logo: icon, + item: items, + allowEmpty: true, + image, + author: title.split(/-/).pop()?.trim(), + language, }; -} +}; + +export const route: Route = { + path: '/81rc/:category{.+}?', + name: '中国人民解放军专业技术人才网', + url: '81rc.81.cn', + maintainers: ['nczitzk'], + handler, + example: '/81/81rc/sy/gzdt_210283', + parameters: { category: '分类,默认为 `sy/gzdt_210283`,即工作动态,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [工作动态](https://81rc.81.cn/sy/gzdt_210283),网址为 \`https://81rc.81.cn/sy/gzdt_210283\`。截取 \`https://81rc.81.cn/\` 到末尾的部分 \`sy/gzdt_210283\` 作为参数填入,此时路由为 [\`/81/81rc/sy/gzdt_210283\`](https://rsshub.app/81/81rc/sy/gzdt_210283)。 +::: + `, + categories: ['government'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['81rc.81.cn/:category'], + target: (params) => { + const category = params.category; + + return `/81/81rc/${category ? `/${category}` : ''}`; + }, + }, + ], +}; diff --git a/lib/routes/81/namespace.ts b/lib/routes/81/namespace.ts index 4bcdc81564e500..3019d5fbba3f6f 100644 --- a/lib/routes/81/namespace.ts +++ b/lib/routes/81/namespace.ts @@ -2,5 +2,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国军网', - url: '81rc.81.cn', + url: '81.cn', + categories: ['government'], + description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/8264/list.ts b/lib/routes/8264/list.ts index 0206f67e4db95e..acdb6ff2bc1a1f 100644 --- a/lib/routes/8264/list.ts +++ b/lib/routes/8264/list.ts @@ -28,62 +28,62 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 热门推荐 | 户外知识 | 户外装备 | - | -------- | -------- | -------- | - | 751 | 238 | 204 | +| -------- | -------- | -------- | +| 751 | 238 | 204 | -
- 更多列表 +
+更多列表 - #### 热门推荐 +#### 热门推荐 - | 业界 | 国际 | 专访 | 图说 | 户外 | 登山 | 攀岩 | - | ---- | ---- | ---- | ---- | ---- | ---- | ---- | - | 489 | 733 | 746 | 902 | 914 | 934 | 935 | +| 业界 | 国际 | 专访 | 图说 | 户外 | 登山 | 攀岩 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| 489 | 733 | 746 | 902 | 914 | 934 | 935 | - #### 户外知识 +#### 户外知识 - | 徒步 | 露营 | 安全急救 | 领队 | 登雪山 | - | ---- | ---- | -------- | ---- | ------ | - | 242 | 950 | 931 | 920 | 915 | +| 徒步 | 露营 | 安全急救 | 领队 | 登雪山 | +| ---- | ---- | -------- | ---- | ------ | +| 242 | 950 | 931 | 920 | 915 | - | 攀岩 | 骑行 | 跑步 | 滑雪 | 水上运动 | - | ---- | ---- | ---- | ---- | -------- | - | 916 | 917 | 918 | 919 | 921 | +| 攀岩 | 骑行 | 跑步 | 滑雪 | 水上运动 | +| ---- | ---- | ---- | ---- | -------- | +| 916 | 917 | 918 | 919 | 921 | - | 钓鱼 | 潜水 | 攀冰 | 冲浪 | 网球 | - | ---- | ---- | ---- | ---- | ---- | - | 951 | 952 | 953 | 966 | 967 | +| 钓鱼 | 潜水 | 攀冰 | 冲浪 | 网球 | +| ---- | ---- | ---- | ---- | ---- | +| 951 | 952 | 953 | 966 | 967 | - | 绳索知识 | 高尔夫 | 马术 | 户外摄影 | 羽毛球 | - | -------- | ------ | ---- | -------- | ------ | - | 968 | 969 | 970 | 973 | 971 | +| 绳索知识 | 高尔夫 | 马术 | 户外摄影 | 羽毛球 | +| -------- | ------ | ---- | -------- | ------ | +| 968 | 969 | 970 | 973 | 971 | - | 游泳 | 溯溪 | 健身 | 瑜伽 | - | ---- | ---- | ---- | ---- | - | 974 | 975 | 976 | 977 | +| 游泳 | 溯溪 | 健身 | 瑜伽 | +| ---- | ---- | ---- | ---- | +| 974 | 975 | 976 | 977 | - #### 户外装备 +#### 户外装备 - | 服装 | 冲锋衣 | 抓绒衣 | 皮肤衣 | 速干衣 | - | ---- | ------ | ------ | ------ | ------ | - | 209 | 923 | 924 | 925 | 926 | +| 服装 | 冲锋衣 | 抓绒衣 | 皮肤衣 | 速干衣 | +| ---- | ------ | ------ | ------ | ------ | +| 209 | 923 | 924 | 925 | 926 | - | 羽绒服 | 软壳 | 户外鞋 | 登山鞋 | 徒步鞋 | - | ------ | ---- | ------ | ------ | ------ | - | 927 | 929 | 211 | 928 | 930 | +| 羽绒服 | 软壳 | 户外鞋 | 登山鞋 | 徒步鞋 | +| ------ | ---- | ------ | ------ | ------ | +| 927 | 929 | 211 | 928 | 930 | - | 越野跑鞋 | 溯溪鞋 | 登山杖 | 帐篷 | 睡袋 | - | -------- | ------ | ------ | ---- | ---- | - | 933 | 932 | 220 | 208 | 212 | +| 越野跑鞋 | 溯溪鞋 | 登山杖 | 帐篷 | 睡袋 | +| -------- | ------ | ------ | ---- | ---- | +| 933 | 932 | 220 | 208 | 212 | - | 炉具 | 灯具 | 水具 | 面料 | 背包 | - | ---- | ---- | ---- | ---- | ---- | - | 792 | 218 | 219 | 222 | 207 | +| 炉具 | 灯具 | 水具 | 面料 | 背包 | +| ---- | ---- | ---- | ---- | ---- | +| 792 | 218 | 219 | 222 | 207 | - | 防潮垫 | 电子导航 | 冰岩绳索 | 综合装备 | - | ------ | -------- | -------- | -------- | - | 214 | 216 | 215 | 223 | -
`, +| 防潮垫 | 电子导航 | 冰岩绳索 | 综合装备 | +| ------ | -------- | -------- | -------- | +| 214 | 216 | 215 | 223 | +
`, }; async function handler(ctx) { diff --git a/lib/routes/8264/namespace.ts b/lib/routes/8264/namespace.ts index 3d2cf0432fcd8b..48522cf12fe0a4 100644 --- a/lib/routes/8264/namespace.ts +++ b/lib/routes/8264/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '8264', url: '8264.com', + lang: 'zh-CN', }; diff --git a/lib/routes/8kcos/namespace.ts b/lib/routes/8kcos/namespace.ts index 15e6c6a13355c7..0fcb25f00a58c0 100644 --- a/lib/routes/8kcos/namespace.ts +++ b/lib/routes/8kcos/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '8KCosplay', url: '8kcosplay.com', + lang: 'zh-CN', }; diff --git a/lib/routes/8world/namespace.ts b/lib/routes/8world/namespace.ts index d65a6da2d746e0..5cd25e46eb8e3f 100644 --- a/lib/routes/8world/namespace.ts +++ b/lib/routes/8world/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '8 视界', url: '8world.com', + lang: 'zh-CN', }; diff --git a/lib/routes/91porn/index.ts b/lib/routes/91porn/index.ts index e704db6ae970e6..43548beeb8bfa0 100644 --- a/lib/routes/91porn/index.ts +++ b/lib/routes/91porn/index.ts @@ -34,8 +34,8 @@ export const route: Route = { handler, url: '91porn.com/index.php', description: `| English | 简体中文 | 繁體中文 | - | ------- | -------- | -------- | - | en\_US | cn\_CN | zh\_ZH |`, +| ------- | -------- | -------- | +| en\_US | cn\_CN | zh\_ZH |`, }; async function handler(ctx) { diff --git a/lib/routes/91porn/namespace.ts b/lib/routes/91porn/namespace.ts index d9c38b15c6965d..3b2ca7baca4edc 100644 --- a/lib/routes/91porn/namespace.ts +++ b/lib/routes/91porn/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '91porn', url: '91porn.com', - description: `:::tip + description: `::: tip 91porn has multiple backup domains, routes use the permanent domain \`https://91porn.com\` by default. If the domain is not accessible, you can add \`?domain=\` to specify the domain to be used. If you want to specify the backup domain to \`https://0122.91p30.com\`, you can add \`?domain=0122.91p30.com\` to the end of all 91porn routes, then the route will become [\`/91porn?domain=0122.91p30.com\`](https://rsshub.app/91porn?domain=0122.91p30.com) :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/95mm/category.ts b/lib/routes/95mm/category.ts index 13e57d6e71e7fc..877995e83a2231 100644 --- a/lib/routes/95mm/category.ts +++ b/lib/routes/95mm/category.ts @@ -24,8 +24,8 @@ export const route: Route = { handler, url: '95mm.org/', description: `| 清纯唯美 | 摄影私房 | 明星写真 | 三次元 | 异域美景 | 性感妖姬 | 游戏主题 | 美女壁纸 | - | -------- | -------- | -------- | ------ | -------- | -------- | -------- | -------- | - | 1 | 2 | 4 | 5 | 6 | 7 | 9 | 11 |`, +| -------- | -------- | -------- | ------ | -------- | -------- | -------- | -------- | +| 1 | 2 | 4 | 5 | 6 | 7 | 9 | 11 |`, }; async function handler(ctx) { diff --git a/lib/routes/95mm/namespace.ts b/lib/routes/95mm/namespace.ts index 1b4f81d33efb92..ff0cd85269cfd9 100644 --- a/lib/routes/95mm/namespace.ts +++ b/lib/routes/95mm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MM 范', url: '95mm.org', + lang: 'zh-CN', }; diff --git a/lib/routes/95mm/tab.ts b/lib/routes/95mm/tab.ts index e5bdb711bb3abd..41b6eb18f521e1 100644 --- a/lib/routes/95mm/tab.ts +++ b/lib/routes/95mm/tab.ts @@ -24,7 +24,7 @@ export const route: Route = { handler, url: '95mm.org/', description: `| 最新 | 热门 | 校花 | 森系 | 清纯 | 童颜 | 嫩模 | 少女 | - | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |`, +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |`, }; async function handler(ctx) { diff --git a/lib/routes/95mm/utils.ts b/lib/routes/95mm/utils.ts index ae0de6936b91a7..b374ad0a392ecd 100644 --- a/lib/routes/95mm/utils.ts +++ b/lib/routes/95mm/utils.ts @@ -45,7 +45,7 @@ const ProcessItems = async (ctx, title, currentUrl) => { const images = detailResponse.data.match(/src": '(.*?)',"width/g); item.description = art(path.join(__dirname, 'templates/description.art'), { - images: images.map((i) => i.split("'")[1].replaceAll('\\/', '/')), + images: images.map((i) => i.split("'")[1].replaceAll(String.raw`\/`, '/')), }); return item; diff --git a/lib/routes/9to5/namespace.ts b/lib/routes/9to5/namespace.ts index 2e757a1c162a68..ec50d03f614ac8 100644 --- a/lib/routes/9to5/namespace.ts +++ b/lib/routes/9to5/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '9To5', url: '9to5toys.com', + lang: 'en', }; diff --git a/lib/routes/a9vg/a9vg.ts b/lib/routes/a9vg/a9vg.ts deleted file mode 100644 index b86c783bbfa3f7..00000000000000 --- a/lib/routes/a9vg/a9vg.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import * as cheerio from 'cheerio'; -import { parseDate } from '@/utils/parse-date'; -import timezone from '@/utils/timezone'; - -export const route: Route = { - path: '/', - radar: [ - { - source: ['a9vg.com/list/news', 'a9vg.com/'], - target: '', - }, - ], - name: 'Unknown', - maintainers: ['monnerHenster'], - handler, - url: 'a9vg.com/list/news', -}; - -async function handler() { - const baseUrl = 'http://www.a9vg.com'; - const link = `${baseUrl}/list/news`; - const { data } = await got(link); - - const $ = cheerio.load(data); - const list = $('.a9-rich-card-list li') - .toArray() - .map((elem) => { - const item = $(elem); - return { - title: item.find('.a9-rich-card-list_label').text(), - description: `
${item.find('.a9-rich-card-list_summary').text().trim()}`, - pubDate: timezone(parseDate(item.find('.a9-rich-card-list_infos').text().trim(), 'YYYY-MM-DD HH:mm:ss'), 8), - link: `${baseUrl}${item.find('.a9-rich-card-list_item').attr('href')}`, - }; - }); - - return { - title: 'A9VG 电玩部落', - link, - description: $('meta[name="description"]').attr('content'), - item: list, - }; -} diff --git a/lib/routes/a9vg/index.ts b/lib/routes/a9vg/index.ts new file mode 100644 index 00000000000000..a8921d6423c5cb --- /dev/null +++ b/lib/routes/a9vg/index.ts @@ -0,0 +1,214 @@ +import { Route } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +export const handler = async (ctx) => { + const { category = 'news/All' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + + const rootUrl = 'http://www.a9vg.com'; + const currentUrl = new URL(`list/${category}`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('a.a9-rich-card-list_item') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const image = item.find('img.a9-rich-card-list_image'); + const title = item.find('div.a9-rich-card-list_label').text(); + + return { + title, + link: new URL(item.prop('href'), rootUrl).href, + description: art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image.prop('src'), + alt: title, + }, + ] + : undefined, + }), + pubDate: timezone(parseDate(item.find('div.a9-rich-card-list_infos').text()), +8), + language, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + $$('ignore_js_op img, p img').each((_, el) => { + el = $$(el); + + el.parent().replaceWith( + art(path.join(__dirname, 'templates/description.art'), { + images: el.prop('file') + ? [ + { + src: el.prop('file'), + alt: el.next().find('div.xs0 p').first().text(), + }, + ] + : undefined, + }) + ); + }); + + item.title = $$('h1.ts, div.c-article-main_content-title').first().text(); + item.description = art(path.join(__dirname, 'templates/description.art'), { + description: $$('td.t_f, div.c-article-main_contentraw').first().html(), + }); + item.author = + $$('b a.blue').first().text() || + $$( + $$('span.c-article-main_content-intro-item') + .toArray() + .findLast((i) => $$(i).text().startsWith('作者')) + ) + .text() + .split(/:/) + .pop(); + item.pubDate = timezone( + parseDate( + $$('div.authi em') + .first() + .text() + .trim() + .match(/发表于 (\d+-\d+-\d+ \d+:\d+)/)?.[1] ?? $$('span.c-article-main_content-intro-item').first().text(), + ['YYYY-M-D HH:mm', 'YYYY-MM-DD HH:mm'] + ), + +8 + ); + item.language = language; + + return item; + }) + ) + ); + + const title = $('title').text(); + const image = new URL('images/logo.1cee7c0f.svg', rootUrl).href; + + return { + title, + description: $('meta[name="description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: title.split(/-/).pop(), + language, + }; +}; + +export const route: Route = { + path: '/:category{.+}?', + name: '新闻', + url: 'a9vg.com', + maintainers: ['monnerHenster', 'nczitzk'], + handler, + example: '/a9vg/news', + parameters: { category: '分类,默认为 ,可在对应分类页 URL 中找到, Category, by default' }, + description: `::: tip + 若订阅 [PS4](http://www.a9vg.com/list/news/PS4),网址为 \`http://www.a9vg.com/list/news/PS4\`。截取 \`http://www.a9vg.com/list\` 到末尾的部分 \`news/PS4\` 作为参数填入,此时路由为 [\`/a9vg/news/PS4\`](https://rsshub.app/a9vg/news/PS4)。 +::: + +| 分类 | ID | +| -------------------------------------------------- | ------------------------------------------------------ | +| [All](https://www.a9vg.com/list/news/All) | [news/All](https://rsshub.app/a9vg/news/All) | +| [PS4](https://www.a9vg.com/list/news/PS4) | [news/PS4](https://rsshub.app/a9vg/news/PS4) | +| [PS5](https://www.a9vg.com/list/news/PS5) | [news/PS5](https://rsshub.app/a9vg/news/PS5) | +| [Switch](https://www.a9vg.com/list/news/Switch) | [news/Switch](https://rsshub.app/a9vg/news/Switch) | +| [Xbox One](https://www.a9vg.com/list/news/XboxOne) | [news/XboxOne](https://rsshub.app/a9vg/news/XboxOne) | +| [XSX](https://www.a9vg.com/list/news/XSX) | [news/XSX](https://rsshub.app/a9vg/news/XSX) | +| [PC](https://www.a9vg.com/list/news/PC) | [news/PC](https://rsshub.app/a9vg/news/PC) | +| [业界](https://www.a9vg.com/list/news/Industry) | [news/Industry](https://rsshub.app/a9vg/news/Industry) | +| [厂商](https://www.a9vg.com/list/news/Factory) | [news/Factory](https://rsshub.app/a9vg/news/Factory) | + `, + categories: ['game'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.a9vg.com/list/:category'], + target: (params) => { + const category = params.category; + + return category ? `/${category}` : ''; + }, + }, + { + title: 'All', + source: ['www.a9vg.com/list/news/All'], + target: '/news/All', + }, + { + title: 'PS4', + source: ['www.a9vg.com/list/news/PS4'], + target: '/news/PS4', + }, + { + title: 'PS5', + source: ['www.a9vg.com/list/news/PS5'], + target: '/news/PS5', + }, + { + title: 'Switch', + source: ['www.a9vg.com/list/news/Switch'], + target: '/news/Switch', + }, + { + title: 'Xbox One', + source: ['www.a9vg.com/list/news/XboxOne'], + target: '/news/XboxOne', + }, + { + title: 'XSX', + source: ['www.a9vg.com/list/news/XSX'], + target: '/news/XSX', + }, + { + title: 'PC', + source: ['www.a9vg.com/list/news/PC'], + target: '/news/PC', + }, + { + title: '业界', + source: ['www.a9vg.com/list/news/Industry'], + target: '/news/Industry', + }, + { + title: '厂商', + source: ['www.a9vg.com/list/news/Factory'], + target: '/news/Factory', + }, + ], +}; diff --git a/lib/routes/a9vg/namespace.ts b/lib/routes/a9vg/namespace.ts index 074bd1620eaa8c..dc0110181863d4 100644 --- a/lib/routes/a9vg/namespace.ts +++ b/lib/routes/a9vg/namespace.ts @@ -3,4 +3,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'A9VG 电玩部落', url: 'a9vg.com', + description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/a9vg/templates/description.art b/lib/routes/a9vg/templates/description.art new file mode 100644 index 00000000000000..dfab19230c1108 --- /dev/null +++ b/lib/routes/a9vg/templates/description.art @@ -0,0 +1,17 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
+ {{ image.alt }} +
+ {{ /if }} + {{ /each }} +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/aamacau/index.ts b/lib/routes/aamacau/index.ts index 75230a4349a12f..23f8821d902dce 100644 --- a/lib/routes/aamacau/index.ts +++ b/lib/routes/aamacau/index.ts @@ -27,16 +27,16 @@ export const route: Route = { handler, url: 'aamacau.com/', description: `| 即時報道 | 每週專題 | 藝文爛鬼樓 | 論盡紙本 | 新聞事件 | 特別企劃 | - | ------------ | ----------- | ---------- | -------- | -------- | -------- | - | breakingnews | weeklytopic | culture | press | case | special | +| ------------ | ----------- | ---------- | -------- | -------- | -------- | +| breakingnews | weeklytopic | culture | press | case | special | - :::tip +::: tip 除了直接订阅分类全部文章(如 [每週專題](https://aamacau.com/topics/weeklytopic) 的对应路由为 [/aamacau/weeklytopic](https://rsshub.app/aamacau/weeklytopic)),你也可以订阅特定的专题,如 [【9-12】2021 澳門立法會選舉](https://aamacau.com/topics/【9-12】2021澳門立法會選舉) 的对应路由为 [/【9-12】2021 澳門立法會選舉](https://rsshub.app/aamacau/【9-12】2021澳門立法會選舉)。 分类中的专题也可以单独订阅,如 [新聞事件](https://aamacau.com/topics/case) 中的 [「武漢肺炎」新聞檔案](https://aamacau.com/topics/case/「武漢肺炎」新聞檔案) 对应路由为 [/case/「武漢肺炎」新聞檔案](https://rsshub.app/aamacau/case/「武漢肺炎」新聞檔案)。 同理,其他分类同上例子也可以订阅特定的单独专题。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/aamacau/namespace.ts b/lib/routes/aamacau/namespace.ts index afc407e648e7b8..d0eb36ae27115c 100644 --- a/lib/routes/aamacau/namespace.ts +++ b/lib/routes/aamacau/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '論盡媒體 AllAboutMacau Media', url: 'aamacau.com', + lang: 'zh-HK', }; diff --git a/lib/routes/abc/index.ts b/lib/routes/abc/index.ts index bd3075455d2250..001bb3ddd88b7b 100644 --- a/lib/routes/abc/index.ts +++ b/lib/routes/abc/index.ts @@ -3,7 +3,7 @@ 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 { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; @@ -11,6 +11,7 @@ import path from 'node:path'; export const route: Route = { path: '/:category{.+}?', + example: '/wa', radar: [ { source: ['abc.net.au/:category*'], @@ -23,14 +24,14 @@ export const route: Route = { name: 'Channel & Topic', categories: ['traditional-media'], description: ` - :::tip +::: tip All Topics in [Topic Library](https://abc.net.au/news/topics) are supported, you can fill in the field after \`topic\` in its URL, or fill in the \`documentId\`. For example, the URL for [Computer Science](https://www.abc.net.au/news/topic/computer-science) is \`https://www.abc.net.au/news/topic/computer-science\`, the \`category\` is \`news/topic/computer-science\`, and the \`documentId\` of the Topic is \`2302\`, so the route is [/abc/news/topic/computer-science](https://rsshub.app/abc/news/topic/computer-science) and [/abc/2302](https://rsshub.app/abc/2302). The supported channels are all listed in the table below. For other channels, please find the \`documentId\` in the source code of the channel page and fill it in as above. - :::`, - maintainers: ['nczitzk'], +:::`, + maintainers: ['nczitzk', 'pseudoyu'], handler, }; @@ -50,18 +51,18 @@ async function handler(ctx) { documentId = category; const feedUrl = new URL(`news/feed/${documentId}/rss.xml`, rootUrl).href; - const { data: feedResponse } = await got(feedUrl); + const feedResponse = await ofetch(feedUrl); currentUrl = feedResponse.match(/([\w-./:?]+)<\/link>/)[1]; } - const { data: currentResponse } = await got(currentUrl); + const currentResponse = await ofetch(currentUrl); const $ = load(currentResponse); documentId = documentId ?? $('div[data-uri^="coremedia://collection/"]').first().prop('data-uri').split(/\//).pop(); - const { data: response } = await got(apiUrl, { - searchParams: { + const response = await ofetch(apiUrl, { + query: { name: 'PaginationArticles', documentId, size: limit, @@ -71,7 +72,7 @@ async function handler(ctx) { let items = response.collection.slice(0, limit).map((i) => { const item = { title: i.title.children ?? i.title, - link: new URL(i.link.to, rootUrl).href, + link: i.link.startsWith('https://') ? i.link : new URL(i.link, rootUrl).href, description: art(path.join(__dirname, 'templates/description.art'), { image: i.image ? { @@ -82,8 +83,8 @@ async function handler(ctx) { }), author: i.newsBylineProps?.authors?.map((a) => a.name).join('/') ?? undefined, guid: `abc-${i.id}`, - pubDate: parseDate(i.timestamp.dates.firstPublished), - updated: i.timestamp.dates.lastUpdated ? parseDate(i.timestamp.dates.lastUpdated) : undefined, + pubDate: parseDate(i.dates.firstPublished), + updated: i.dates.lastUpdated ? parseDate(i.dates.lastUpdated) : undefined, }; if (i.mediaIndicator) { @@ -99,7 +100,7 @@ async function handler(ctx) { items.map((item) => cache.tryGet(item.link, async () => { try { - const { data: detailResponse } = await got(item.link); + const detailResponse = await ofetch(item.link); const content = load(detailResponse); @@ -126,7 +127,7 @@ async function handler(ctx) { item.title = content('meta[property="og:title"]').prop('content'); item.description = ''; - const enclosurePattern = '"(?:MIME|content)?Type":"([\\w]+/[\\w]+)".*?"(?:fileS|s)?ize":(\\d+),.*?"url":"([\\w-.:/?]+)"'; + const enclosurePattern = String.raw`"(?:MIME|content)?Type":"([\w]+/[\w]+)".*?"(?:fileS|s)?ize":(\d+),.*?"url":"([\w-.:/?]+)"`; const enclosureMatches = detailResponse.match(new RegExp(enclosurePattern, 'g')); @@ -173,7 +174,7 @@ async function handler(ctx) { ) ); - const icon = new URL($('link[rel="apple-touch-icon"]').prop('href'), rootUrl).href; + const icon = new URL($('link[rel="apple-touch-icon"]').prop('href') || '', rootUrl).href; return { item: items, diff --git a/lib/routes/abc/namespace.ts b/lib/routes/abc/namespace.ts index 19568cd0ee7fbf..4cfd6d4e033c2e 100644 --- a/lib/routes/abc/namespace.ts +++ b/lib/routes/abc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ABC News', url: 'abc.net.au', + lang: 'en', }; diff --git a/lib/routes/abmedia/namespace.ts b/lib/routes/abmedia/namespace.ts index b1b909fdd08f88..e20c52b77a9dc5 100644 --- a/lib/routes/abmedia/namespace.ts +++ b/lib/routes/abmedia/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '链新闻 ABMedia', url: 'www.abmedia.io', + lang: 'zh-TW', }; diff --git a/lib/routes/abskoop/namespace.ts b/lib/routes/abskoop/namespace.ts index ab54b3e351e745..ee0227a29655da 100644 --- a/lib/routes/abskoop/namespace.ts +++ b/lib/routes/abskoop/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'A 姐分享', url: 'nsfw.abskoop.com', + lang: 'zh-TW', }; diff --git a/lib/routes/academia/namespace.ts b/lib/routes/academia/namespace.ts new file mode 100644 index 00000000000000..c1610b575efaa3 --- /dev/null +++ b/lib/routes/academia/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Academia', + url: 'www.academia.edu', + lang: 'en', +}; diff --git a/lib/routes/academia/topics.ts b/lib/routes/academia/topics.ts new file mode 100644 index 00000000000000..c45c9937c40335 --- /dev/null +++ b/lib/routes/academia/topics.ts @@ -0,0 +1,39 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/topic/:interest', + example: '/academia/topic/Urban_History', + parameters: { interest: 'interest' }, + radar: [ + { + source: ['academia.edu/Documents/in/:interest'], + target: '/topic/:interest', + }, + ], + name: 'interest', + maintainers: ['K33k0', 'cscnk52'], + categories: ['journal'], + handler, + url: 'academia.edu', +}; + +async function handler(ctx) { + const interest = ctx.req.param('interest'); + const response = await ofetch(`https://www.academia.edu/Documents/in/${interest}`); + const $ = load(response); + const list = $('.works > .div') + .toArray() + .map((item) => ({ + title: $(item).find('.title').text(), + link: $(item).find('.title > a').attr('href'), + author: $(item).find('.authors').text().replace('by', '').trim(), + description: $(item).find('.summarized').text(), + })); + return { + title: `academia.edu | ${interest} documents`, + link: `https://academia.edu/Documents/in/${interest}`, + item: list, + }; +} diff --git a/lib/routes/accessbriefing/index.ts b/lib/routes/accessbriefing/index.ts index f4fc76be748981..76ffeec9a34e07 100644 --- a/lib/routes/accessbriefing/index.ts +++ b/lib/routes/accessbriefing/index.ts @@ -123,29 +123,29 @@ export const route: Route = { handler, example: '/accessbriefing/latest/news', parameters: { category: 'Category, Latest News by default' }, - description: `:::tip + description: `::: tip If you subscribe to [Latest News](https://www.accessbriefing.com/latest/news),where the URL is \`https://www.accessbriefing.com/latest/news\`, extract the part \`https://www.accessbriefing.com/\` to the end, and use it as the parameter to fill in. Therefore, the route will be [\`/accessbriefing/latest/news\`](https://rsshub.app/accessbriefing/latest/news). - ::: - - #### Latest - - | Category | ID | - | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | - | [News](https://www.accessbriefing.com/latest/news) | [latest/news](https://rsshub.app/target/site/latest/news) | - | [Products & Technology](https://www.accessbriefing.com/latest/products-and-technology) | [latest/products-and-technology](https://rsshub.app/target/site/latest/products-and-technology) | - | [Rental News](https://www.accessbriefing.com/latest/rental-news) | [latest/rental-news](https://rsshub.app/target/site/latest/rental-news) | - | [People](https://www.accessbriefing.com/latest/people) | [latest/people](https://rsshub.app/target/site/latest/people) | - | [Regualtions & Safety](https://www.accessbriefing.com/latest/regualtions-safety) | [latest/regualtions-safety](https://rsshub.app/target/site/latest/regualtions-safety) | - | [Finance](https://www.accessbriefing.com/latest/finance) | [latest/finance](https://rsshub.app/target/site/latest/finance) | - | [Sustainability](https://www.accessbriefing.com/latest/sustainability) | [latest/sustainability](https://rsshub.app/target/site/latest/sustainability) | - - #### Insight - - | Category | ID | - | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | - | [Interviews](https://www.accessbriefing.com/insight/interviews) | [insight/interviews](https://rsshub.app/target/site/insight/interviews) | - | [Longer reads](https://www.accessbriefing.com/insight/longer-reads) | [insight/longer-reads](https://rsshub.app/target/site/insight/longer-reads) | - | [Videos and podcasts](https://www.accessbriefing.com/insight/videos-and-podcasts) | [insight/videos-and-podcasts](https://rsshub.app/target/site/insight/videos-and-podcasts) | +::: + +#### Latest + +| Category | ID | +| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| [News](https://www.accessbriefing.com/latest/news) | [latest/news](https://rsshub.app/target/site/latest/news) | +| [Products & Technology](https://www.accessbriefing.com/latest/products-and-technology) | [latest/products-and-technology](https://rsshub.app/target/site/latest/products-and-technology) | +| [Rental News](https://www.accessbriefing.com/latest/rental-news) | [latest/rental-news](https://rsshub.app/target/site/latest/rental-news) | +| [People](https://www.accessbriefing.com/latest/people) | [latest/people](https://rsshub.app/target/site/latest/people) | +| [Regualtions & Safety](https://www.accessbriefing.com/latest/regualtions-safety) | [latest/regualtions-safety](https://rsshub.app/target/site/latest/regualtions-safety) | +| [Finance](https://www.accessbriefing.com/latest/finance) | [latest/finance](https://rsshub.app/target/site/latest/finance) | +| [Sustainability](https://www.accessbriefing.com/latest/sustainability) | [latest/sustainability](https://rsshub.app/target/site/latest/sustainability) | + +#### Insight + +| Category | ID | +| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| [Interviews](https://www.accessbriefing.com/insight/interviews) | [insight/interviews](https://rsshub.app/target/site/insight/interviews) | +| [Longer reads](https://www.accessbriefing.com/insight/longer-reads) | [insight/longer-reads](https://rsshub.app/target/site/insight/longer-reads) | +| [Videos and podcasts](https://www.accessbriefing.com/insight/videos-and-podcasts) | [insight/videos-and-podcasts](https://rsshub.app/target/site/insight/videos-and-podcasts) | `, categories: ['new-media'], diff --git a/lib/routes/accessbriefing/namespace.ts b/lib/routes/accessbriefing/namespace.ts index 6c647cc4ff9223..a2a058d3df2fdc 100644 --- a/lib/routes/accessbriefing/namespace.ts +++ b/lib/routes/accessbriefing/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: 'accessbriefing.com', categories: ['new-media'], description: '', + lang: 'en', }; diff --git a/lib/routes/acfun/article.ts b/lib/routes/acfun/article.ts index 3c7b623e911944..f6e4de7a40412a 100644 --- a/lib/routes/acfun/article.ts +++ b/lib/routes/acfun/article.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'; @@ -37,9 +37,35 @@ const timeRangeEnum = new Set(['all', 'oneDay', 'threeDay', 'oneWeek', 'oneMonth export const route: Route = { path: '/article/:categoryId/:sortType?/:timeRange?', - categories: ['anime'], + categories: ['anime', 'popular'], + view: ViewType.Articles, example: '/acfun/article/110', - parameters: { categoryId: '分区 ID,见下表', sortType: '排序,见下表,默认为 `createTime`', timeRange: '时间范围,见下表,仅在排序是 `hotScore` 有效,默认为 `all`' }, + parameters: { + categoryId: { + description: '分区 ID', + options: Object.keys(categoryMap).map((id) => ({ value: id, label: categoryMap[id].title })), + }, + sortType: { + description: '排序', + options: [ + { value: 'createTime', label: '最新发表' }, + { value: 'lastCommentTime', label: '最新动态' }, + { value: 'hotScore', label: '最热文章' }, + ], + default: 'createTime', + }, + timeRange: { + description: '时间范围,仅在排序是 `hotScore` 有效', + options: [ + { value: 'all', label: '时间不限' }, + { value: 'oneDay', label: '24 小时' }, + { value: 'threeDay', label: '三天' }, + { value: 'oneWeek', label: '一周' }, + { value: 'oneMonth', label: '一个月' }, + ], + default: 'all', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -52,16 +78,16 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| 二次元画师 | 综合 | 生活情感 | 游戏 | 动漫文化 | 漫画文学 | - | ---------- | ---- | -------- | ---- | -------- | -------- | - | 184 | 110 | 73 | 164 | 74 | 75 | +| ---------- | ---- | -------- | ---- | -------- | -------- | +| 184 | 110 | 73 | 164 | 74 | 75 | - | 最新发表 | 最新动态 | 最热文章 | - | ---------- | --------------- | -------- | - | createTime | lastCommentTime | hotScore | +| 最新发表 | 最新动态 | 最热文章 | +| ---------- | --------------- | -------- | +| createTime | lastCommentTime | hotScore | - | 时间不限 | 24 小时 | 三天 | 一周 | 一个月 | - | -------- | ------- | -------- | ------- | -------- | - | all | oneDay | threeDay | oneWeek | oneMonth |`, +| 时间不限 | 24 小时 | 三天 | 一周 | 一个月 | +| -------- | ------- | -------- | ------- | -------- | +| all | oneDay | threeDay | oneWeek | oneMonth |`, }; async function handler(ctx) { diff --git a/lib/routes/acfun/bangumi.ts b/lib/routes/acfun/bangumi.ts index 34774763f747a2..62bcb4683ed0d3 100644 --- a/lib/routes/acfun/bangumi.ts +++ b/lib/routes/acfun/bangumi.ts @@ -1,10 +1,11 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/bangumi/:id', - categories: ['anime'], + categories: ['anime', 'popular'], + view: ViewType.Videos, example: '/acfun/bangumi/5022158', parameters: { id: '番剧 id' }, features: { @@ -18,7 +19,7 @@ export const route: Route = { name: '番剧', maintainers: ['xyqfer'], handler, - description: `:::tip + description: `::: tip 番剧 id 不包含开头的 aa。 例如:\`https://www.acfun.cn/bangumi/aa5022158\` 的番剧 id 是 5022158,不包括开头的 aa。 :::`, diff --git a/lib/routes/acfun/namespace.ts b/lib/routes/acfun/namespace.ts index 5e4be56b667291..c69cd5feb257c8 100644 --- a/lib/routes/acfun/namespace.ts +++ b/lib/routes/acfun/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AcFun', url: 'www.acfun.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/acfun/video.ts b/lib/routes/acfun/video.ts index 6db5451b746f5e..51dbbd57e9f5d3 100644 --- a/lib/routes/acfun/video.ts +++ b/lib/routes/acfun/video.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; @@ -15,7 +15,9 @@ export const route: Route = { parameters: { uid: '用户 UID', }, - categories: ['anime'], + categories: ['anime', 'popular'], + example: '/acfun/user/video/6102', + view: ViewType.Videos, maintainers: ['wdssmq'], handler, }; diff --git a/lib/routes/acg17/namespace.ts b/lib/routes/acg17/namespace.ts index 161fe01ca04556..57c7ec67535401 100644 --- a/lib/routes/acg17/namespace.ts +++ b/lib/routes/acg17/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ACG17', url: 'acg17.com', + lang: 'zh-CN', }; diff --git a/lib/routes/acpaa/namespace.ts b/lib/routes/acpaa/namespace.ts index 5a7db88fa01e97..a72a98eeaf1df0 100644 --- a/lib/routes/acpaa/namespace.ts +++ b/lib/routes/acpaa/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中华全国专利代理师协会', url: 'acpaa.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/acs/namespace.ts b/lib/routes/acs/namespace.ts index 5ae76768586544..91fafa4667cb26 100644 --- a/lib/routes/acs/namespace.ts +++ b/lib/routes/acs/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Unknown', + name: 'ACS Publications', url: 'pubs.acs.org', + lang: 'en', }; diff --git a/lib/routes/aeaweb/index.ts b/lib/routes/aeaweb/index.ts index 5cf537e891c1a5..78b8442c996025 100644 --- a/lib/routes/aeaweb/index.ts +++ b/lib/routes/aeaweb/index.ts @@ -32,9 +32,9 @@ export const route: Route = { handler, description: `The URL of the journal [American Economic Review](https://www.aeaweb.org/journals/aer) is \`https://www.aeaweb.org/journals/aer\`, where \`aer\` is the id of the journal, so the route for this journal is \`/aeaweb/aer\`. - :::tip +::: tip More jounals can be found in [AEA Journals](https://www.aeaweb.org/journals). - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/aeaweb/namespace.ts b/lib/routes/aeaweb/namespace.ts index be041366e32b7e..b727a055f2b476 100644 --- a/lib/routes/aeaweb/namespace.ts +++ b/lib/routes/aeaweb/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'American Economic Association', url: 'aeaweb.org', + lang: 'en', }; diff --git a/lib/routes/aeon/category.ts b/lib/routes/aeon/category.ts index b2a707211592a5..7e4a87ecdec678 100644 --- a/lib/routes/aeon/category.ts +++ b/lib/routes/aeon/category.ts @@ -1,13 +1,24 @@ import { Route } from '@/types'; -import { load } from 'cheerio'; -import got from '@/utils/got'; -import { getData } from './utils'; +import ofetch from '@/utils/ofetch'; +import { getBuildId, getData } from './utils'; +import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/category/:category', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/aeon/category/philosophy', - parameters: { category: 'Category' }, + parameters: { + category: { + description: 'Category', + options: [ + { value: 'philosophy', label: 'Philosophy' }, + { value: 'science', label: 'Science' }, + { value: 'psychology', label: 'Psychology' }, + { value: 'society', label: 'Society' }, + { value: 'culture', label: 'Culture' }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -18,34 +29,40 @@ export const route: Route = { }, radar: [ { - source: ['aeon.aeon.co/:category'], + source: ['aeon.co/:category'], }, ], name: 'Categories', maintainers: ['emdoe'], handler, - description: `Supported categories: Philosophy, Science, Psychology, Society, and Culture.`, }; async function handler(ctx) { - const url = `https://aeon.co/${ctx.req.param('category')}`; - const { data: response } = await got(url); - const $ = load(response); + const category = ctx.req.param('category').toLowerCase(); + const url = `https://aeon.co/category/${category}`; + const buildId = await getBuildId(); + const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${category}.json`); - const data = JSON.parse($('script#__NEXT_DATA__').text()); + const section = response.pageProps.section; - const list = data.props.pageProps.section.articles.edges.map((item) => ({ - title: item.node.title, - author: item.node.authors.map((author) => author.displayName).join(', '), - link: `https://aeon.co/${item.node.type.toLowerCase()}s/${item.node.slug}`, + const list = section.articles.edges.map(({ node }) => ({ + title: node.title, + description: node.standfirstLong, + author: node.authors.map((author) => author.displayName).join(', '), + link: `https://aeon.co/${node.type}s/${node.slug}`, + pubDate: parseDate(node.createdAt), + category: [node.section.title, ...node.topics.map((topic) => topic.title)], + image: node.image.url, + type: node.type, + slug: node.slug, })); - const items = await getData(ctx, list); + const items = await getData(list); return { - title: `AEON | ${data.props.pageProps.section.title}`, + title: `AEON | ${section.title}`, link: url, - description: data.props.pageProps.section.metaDescription, + description: section.metaDescription, item: items, }; } diff --git a/lib/routes/aeon/namespace.ts b/lib/routes/aeon/namespace.ts index 7f20c03a894f55..16fc79f2faa96b 100644 --- a/lib/routes/aeon/namespace.ts +++ b/lib/routes/aeon/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AEON', - url: 'aeon.aeon.co', + url: 'aeon.co', + lang: 'en', }; diff --git a/lib/routes/aeon/templates/essay.art b/lib/routes/aeon/templates/essay.art index 5a2fab892134eb..8fed51cad667f4 100644 --- a/lib/routes/aeon/templates/essay.art +++ b/lib/routes/aeon/templates/essay.art @@ -1,3 +1,9 @@ - +{{ if banner.url }} +
+ {{ banner.alt }} + {{ if banner.caption }} +
{{ banner.caption }}
+ {{ /if }} +{{ /if }} {{@ authorsBio }} -{{@ content}} \ No newline at end of file +{{@ content }} diff --git a/lib/routes/aeon/templates/video.art b/lib/routes/aeon/templates/video.art index d1f546c3981a3f..c3d67356151f6b 100644 --- a/lib/routes/aeon/templates/video.art +++ b/lib/routes/aeon/templates/video.art @@ -1,10 +1,10 @@ {{ set video = article.hosterId }} {{ if article.hoster === 'vimeo' }} - {{ set video = "https://player.vimeo.com/video/" + video + "?dnt=1"}} -{{ else if article.hoster == 'youtube' }} + {{ set video = "https://player.vimeo.com/video/" + video + "?dnt=1" }} +{{ else if article.hoster === 'youtube' }} {{ set video = "https://www.youtube-nocookie.com/embed/" + video }} {{ /if }} -{{@ article.credits}} -{{@ article.description}} +{{@ article.credits }} +{{@ article.description }} diff --git a/lib/routes/aeon/type.ts b/lib/routes/aeon/type.ts index 02dc0e277be952..2994f7f0909f48 100644 --- a/lib/routes/aeon/type.ts +++ b/lib/routes/aeon/type.ts @@ -1,13 +1,22 @@ import { Route } from '@/types'; -import { load } from 'cheerio'; -import got from '@/utils/got'; -import { getData } from './utils'; +import ofetch from '@/utils/ofetch'; +import { getBuildId, getData } from './utils'; +import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/:type', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/aeon/essays', - parameters: { type: 'Type' }, + parameters: { + type: { + description: 'Type', + options: [ + { value: 'essays', label: 'Essays' }, + { value: 'videos', label: 'Videos' }, + { value: 'audio', label: 'Audio' }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -18,7 +27,7 @@ export const route: Route = { }, radar: [ { - source: ['aeon.aeon.co/:type'], + source: ['aeon.co/:type'], }, ], name: 'Types', @@ -26,28 +35,30 @@ export const route: Route = { handler, description: `Supported types: Essays, Videos, and Audio. - Compared to the official one, the RSS feed generated by RSSHub not only has more fine-grained options, but also eliminates pull quotes, which can't be easily distinguished from other paragraphs by any RSS reader, but only disrupt the reading flow. This feed also provides users with a bio of the author at the top. - - However, The content generated under \`audio\` does not contain links to audio files.`, + Compared to the official one, the RSS feed generated by RSSHub not only has more fine-grained options, but also eliminates pull quotes, which can't be easily distinguished from other paragraphs by any RSS reader, but only disrupt the reading flow. This feed also provides users with a bio of the author at the top.`, }; async function handler(ctx) { const type = ctx.req.param('type'); - const binaryType = type === 'videos' ? 'videos' : 'essays'; const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1); + const buildId = await getBuildId(); const url = `https://aeon.co/${type}`; - const { data: response } = await got(url); - const $ = load(response); - - const data = JSON.parse($('script#__NEXT_DATA__').text()); + const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${type}.json`); - const list = data.props.pageProps.articles.map((item) => ({ - title: item.title, - link: `https://aeon.co/${binaryType}/${item.slug}`, + const list = response.pageProps.articles.map((node) => ({ + title: node.title, + description: node.standfirstLong, + author: node.authors.map((author) => author.displayName).join(', '), + link: `https://aeon.co/${node.type}s/${node.slug}`, + pubDate: parseDate(node.createdAt), + category: [node.section.title, ...node.topics.map((topic) => topic.title)], + image: node.image.url, + type: node.type, + slug: node.slug, })); - const items = await getData(ctx, list); + const items = await getData(list); return { title: `AEON | ${capitalizedType}`, diff --git a/lib/routes/aeon/utils.ts b/lib/routes/aeon/utils.ts index 1cd3a9eee3e2b3..e18421579cd485 100644 --- a/lib/routes/aeon/utils.ts +++ b/lib/routes/aeon/utils.ts @@ -3,28 +3,61 @@ const __dirname = getCurrentPath(import.meta.url); import cache from '@/utils/cache'; import { load } from 'cheerio'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { art } from '@/utils/render'; import path from 'node:path'; +import { config } from '@/config'; +import { parseDate } from '@/utils/parse-date'; -const getData = async (ctx, list) => { +export const getBuildId = () => + cache.tryGet( + 'aeon:buildId', + async () => { + const response = await ofetch('https://aeon.co'); + const $ = load(response); + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + return nextData.buildId; + }, + config.cache.routeExpire, + false + ); + +const getData = async (list) => { const items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { - const { data: response } = await got(item.link); - const $ = load(response); + const buildId = await getBuildId(); + const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${item.type}s/${item.slug}.json?id=${item.slug}`); - const data = JSON.parse($('script#__NEXT_DATA__').text()); - const type = data.props.pageProps.article.type.toLowerCase(); + const data = response.pageProps.article; + const type = data.type.toLowerCase(); - item.pubDate = new Date(data.props.pageProps.article.publishedAt).toUTCString(); + item.pubDate = parseDate(data.publishedAt); if (type === 'video') { - item.description = art(path.join(__dirname, 'templates/video.art'), { article: data.props.pageProps.article }); + item.description = art(path.join(__dirname, 'templates/video.art'), { article: data }); } else { - // Essay or Audio - // But unfortunately, the method based on __NEXT_DATA__ - // does not include the information of the audio link. + if (data.audio?.id) { + const response = await ofetch('https://api.aeonmedia.co/graphql', { + method: 'POST', + body: { + query: `query getAudio($audioId: ID!) { + audio(id: $audioId) { + id + streamUrl + } + }`, + variables: { + audioId: data.audio.id, + }, + operationName: 'getAudio', + }, + }); + + delete item.image; + item.enclosure_url = response.data.audio.streamUrl; + item.enclosure_type = 'audio/mpeg'; + } // Besides, it seems that the method based on __NEXT_DATA__ // does not include the information of the two-column @@ -32,14 +65,11 @@ const getData = async (ctx, list) => { // e.g. https://aeon.co/essays/how-to-mourn-a-forest-a-lesson-from-west-papua . // But that's very rare. - item.author = data.props.pageProps.article.authors.map((author) => author.name).join(', '); - - const article = data.props.pageProps.article; - const capture = load(article.body); - const banner = article.image?.url; + const capture = load(data.body, null, false); + const banner = data.image; capture('p.pullquote').remove(); - const authorsBio = article.authors.map((author) => '

' + 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) => `

${post.asset.headlines.headline}

${post.asset.body}`).join(''); + break; + + case 'article': + case 'featureArticle': + item.description = renderArticle(asset, item.link); + break; + + default: + throw new Error(`Unknown asset type: ${content.assetType} in ${item.link}`); + } + + return item; +}; + +const renderArticle = (asset, link: string) => { + const $ = cheerio.load(asset.body, null, false); + $('x-placeholder').each((_, el) => { + const $el = $(el); + const id = $el.attr('id'); + if (!id) { + $el.replaceWith(''); + } + + const placeholder = asset.bodyPlaceholders[id!]; + switch (placeholder?.type) { + case 'callout': + case 'relatedStory': + $el.replaceWith(''); + break; + + case 'iframe': + $el.replaceWith(``); + break; + + case 'image': + $el.replaceWith(`${placeholder.data.altText}`); + break; + + case 'linkArticle': + $el.replaceWith(placeholder.data.text); + break; + + case 'linkExternal': + $el.replaceWith(`${placeholder.data.text}`); + break; + + case 'quote': + $el.replaceWith(placeholder.data.markup); + break; + + case 'scribd': + $el.replaceWith(`View on Scribd`); + break; + + case 'twitter': + $el.replaceWith(`${placeholder.data.url}`); + break; + + default: + throw new Error(`Unknown placeholder type: ${placeholder?.type} in ${link}`); + } + }); + + return $.html(); +}; diff --git a/lib/routes/agefans/namespace.ts b/lib/routes/agefans/namespace.ts index faf0ed19215eaf..1cbb5dbc53fb0b 100644 --- a/lib/routes/agefans/namespace.ts +++ b/lib/routes/agefans/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AGE 动漫', url: 'agemys.cc', + lang: 'zh-CN', }; diff --git a/lib/routes/agefans/update.ts b/lib/routes/agefans/update.ts index 94423cc52df70d..37a853d14eb564 100644 --- a/lib/routes/agefans/update.ts +++ b/lib/routes/agefans/update.ts @@ -3,6 +3,7 @@ import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; import { rootUrl } from './utils'; +import asyncPool from 'tiny-async-pool'; export const route: Route = { path: '/update', @@ -46,26 +47,27 @@ async function handler() { }; }); - const items = await Promise.all( - list.map((item) => - cache.tryGet(item.link, async () => { - const detailResponse = await got(item.link); - const content = load(detailResponse.data); + const items: any[] = []; + for await (const item of asyncPool(3, list, (item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got(item.link); + const content = load(detailResponse.data); - content('img').each((_, ele) => { - if (ele.attribs['data-original']) { - ele.attribs.src = ele.attribs['data-original']; - delete ele.attribs['data-original']; - } - }); - content('.video_detail_collect').remove(); + content('img').each((_, ele) => { + if (ele.attribs['data-original']) { + ele.attribs.src = ele.attribs['data-original']; + delete ele.attribs['data-original']; + } + }); + content('.video_detail_collect').remove(); - item.description = content('.video_detail_left').html(); + item.description = content('.video_detail_left').html(); - return item; - }) - ) - ); + return item; + }) + )) { + items.push(item); + } return { title: $('title').text(), diff --git a/lib/routes/agirls/namespace.ts b/lib/routes/agirls/namespace.ts index 339faebbd5984c..6a660dc1e741b8 100644 --- a/lib/routes/agirls/namespace.ts +++ b/lib/routes/agirls/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '电獭少女', url: 'agirls.aotter.net', + lang: 'zh-TW', }; diff --git a/lib/routes/agirls/topic-list.ts b/lib/routes/agirls/topic-list.ts index ceaecbc35ff832..4827ed103fe9c7 100644 --- a/lib/routes/agirls/topic-list.ts +++ b/lib/routes/agirls/topic-list.ts @@ -5,7 +5,7 @@ import { baseUrl } from './utils'; export const route: Route = { path: '/topic_list', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/agirls/topic_list', parameters: {}, features: { diff --git a/lib/routes/agirls/topic.ts b/lib/routes/agirls/topic.ts index ee288eacb32326..c27d3669149094 100644 --- a/lib/routes/agirls/topic.ts +++ b/lib/routes/agirls/topic.ts @@ -6,7 +6,7 @@ import { baseUrl, parseArticle } from './utils'; export const route: Route = { path: '/topic/:topic', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/agirls/topic/AppleWatch', parameters: { topic: '精选主题,可通过下方精选主题列表获得' }, features: { diff --git a/lib/routes/agirls/index.ts b/lib/routes/agirls/z-index.ts similarity index 90% rename from lib/routes/agirls/index.ts rename to lib/routes/agirls/z-index.ts index 1d6f0a8109b884..55092777767df5 100644 --- a/lib/routes/agirls/index.ts +++ b/lib/routes/agirls/z-index.ts @@ -6,7 +6,7 @@ import { baseUrl, parseArticle } from './utils'; export const route: Route = { path: '/:category?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/agirls/app', parameters: { category: '分类,默认为最新文章,可在对应主题页的 URL 中找到,下表仅列出部分' }, features: { @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| App 评测 | 手机开箱 | 笔电开箱 | 3C 周边 | 教学小技巧 | 科技情报 | - | -------- | -------- | -------- | ----------- | ---------- | -------- | - | app | phone | computer | accessories | tutorial | techlife |`, +| -------- | -------- | -------- | ----------- | ---------- | -------- | +| app | phone | computer | accessories | tutorial | techlife |`, }; async function handler(ctx) { diff --git a/lib/routes/agora0/index.ts b/lib/routes/agora0/index.ts index 63b81e780b4628..dc287a6a3ffa2b 100644 --- a/lib/routes/agora0/index.ts +++ b/lib/routes/agora0/index.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| muitinⒾ | aidemnⒾ | srettaⓂ | qⓅ | sucoⓋ | - | ------- | ------- | -------- | -- | ----- | - | initium | inmedia | matters | pq | vocus |`, +| ------- | ------- | -------- | -- | ----- | +| initium | inmedia | matters | pq | vocus |`, }; async function handler(ctx) { diff --git a/lib/routes/agora0/namespace.ts b/lib/routes/agora0/namespace.ts index 51fe8e88b7bafd..e970eba75d9a3e 100644 --- a/lib/routes/agora0/namespace.ts +++ b/lib/routes/agora0/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AG⓪RA', url: 'agorahub.github.io', + lang: 'en', }; diff --git a/lib/routes/agri/index.ts b/lib/routes/agri/index.ts new file mode 100644 index 00000000000000..e77a8820aef0f6 --- /dev/null +++ b/lib/routes/agri/index.ts @@ -0,0 +1,307 @@ +import { Route } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +export const handler = async (ctx) => { + const { category = 'zx/zxfb/' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + + const rootUrl = 'http://www.agri.cn'; + const currentUrl = new URL(category.endsWith('/') ? category : `${category}/`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('div.list_li_con, div.nxw_video_com') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const a = item.find('a').first(); + + const title = a.text(); + const image = item.find('img').first().prop('src') ? new URL(item.find('img').first().prop('src'), rootUrl).href : undefined; + const description = art(path.join(__dirname, 'templates/description.art'), { + intro: item.find('p.con_text').text() || undefined, + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + }); + + return { + title, + description, + pubDate: parseDate(item.find('span.con_date_span').text() || `${item.find('div.com_time_p2').text().trim()}${item.find('div.com_time_p1').text()}`, ['YYYY-MM-DD', 'YYYY.MM.DD']), + link: new URL(a.prop('href'), currentUrl).href, + content: { + html: description, + text: item.find('p.con_text').text() || undefined, + }, + image, + banner: image, + language, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const title = $$('div.detailCon_info_tit').text().trim(); + const description = art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.content_body_box').html(), + }); + + item.title = title; + item.description = description; + item.pubDate = timezone(parseDate($$('meta[name="publishdate"]').prop('content')), +8); + item.author = $$('meta[name="author"]').prop('content') || $$('meta[name="source"]').prop('content'); + item.content = { + html: description, + text: $$('div.content_body_box').text(), + }; + item.language = language; + + item.enclosure_url = $$('div.content_body_box video').prop('src') ?? undefined; + item.enclosure_type = item.enclosure_url ? 'video/mp4' : undefined; + item.enclosure_title = item.enclosure_url ? title : undefined; + + return item; + }) + ) + ); + + const image = new URL($('div.logo img').prop('src'), rootUrl).href; + + return { + title: $('title').text(), + link: currentUrl, + item: items, + allowEmpty: true, + image, + language, + }; +}; + +export const route: Route = { + path: '/:category{.+}?', + name: '分类', + url: 'www.agri.cn', + maintainers: ['nczitzk'], + handler, + example: '/agri/zx/zxfb', + parameters: { category: '分类,默认为 `zx/zxfb`,即最新发布,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [最新发布](http://www.agri.cn/zx/zxfb/),网址为 \`http://www.agri.cn/zx/zxfb/\`。截取 \`https://www.agri.cn/\` 到末尾的部分 \`zx/zxfb\` 作为参数填入,此时路由为 [\`/agri/zx/zxfb\`](https://rsshub.app/agri/zx/zxfb)。 +::: + +#### [机构](http://www.agri.cn/jg/) + +| 分类 | ID | +| --------------------------------------- | ------------------------------------------ | +| [成果展示](http://www.agri.cn/jg/cgzs/) | [jg/cgzs](https://rsshub.app/agri/jg/cgzs) | + +#### [资讯](http://www.agri.cn/zx/) + +| 分类 | ID | +| ------------------------------------------- | ------------------------------------------ | +| [最新发布](http://www.agri.cn/zx/zxfb/) | [zx/zxfb](https://rsshub.app/agri/zx/zxfb) | +| [农业要闻](http://www.agri.cn/zx/nyyw/) | [zx/nyyw](https://rsshub.app/agri/zx/nyyw) | +| [中心动态](http://www.agri.cn/zx/zxdt/) | [zx/zxdt](https://rsshub.app/agri/zx/zxdt) | +| [通知公告](http://www.agri.cn/zx/hxgg/) | [zx/hxgg](https://rsshub.app/agri/zx/hxgg) | +| [全国信息联播](http://www.agri.cn/zx/xxlb/) | [zx/xxlb](https://rsshub.app/agri/zx/xxlb) | + +#### [生产](http://www.agri.cn/sc/) + +| 分类 | ID | +| --------------------------------------- | ------------------------------------------ | +| [生产动态](http://www.agri.cn/sc/scdt/) | [sc/scdt](https://rsshub.app/agri/sc/scdt) | +| [农业品种](http://www.agri.cn/sc/nypz/) | [sc/nypz](https://rsshub.app/agri/sc/nypz) | +| [农事指导](http://www.agri.cn/sc/nszd/) | [sc/nszd](https://rsshub.app/agri/sc/nszd) | +| [农业气象](http://www.agri.cn/sc/nyqx/) | [sc/nyqx](https://rsshub.app/agri/sc/nyqx) | +| [专项监测](http://www.agri.cn/sc/zxjc/) | [sc/zxjc](https://rsshub.app/agri/sc/zxjc) | + +#### [数据](http://www.agri.cn/sj/) + +| 分类 | ID | +| --------------------------------------- | ------------------------------------------ | +| [市场动态](http://www.agri.cn/sj/scdt/) | [sj/scdt](https://rsshub.app/agri/sj/scdt) | +| [供需形势](http://www.agri.cn/sj/gxxs/) | [sj/gxxs](https://rsshub.app/agri/sj/gxxs) | +| [监测预警](http://www.agri.cn/sj/jcyj/) | [sj/jcyj](https://rsshub.app/agri/sj/jcyj) | + +#### [信息化](http://www.agri.cn/xxh/) + +| 分类 | ID | +| ---------------------------------------------- | ------------------------------------------------ | +| [智慧农业](http://www.agri.cn/xxh/zhny/) | [xxh/zhny](https://rsshub.app/agri/xxh/zhny) | +| [信息化标准](http://www.agri.cn/xxh/xxhbz/) | [xxh/xxhbz](https://rsshub.app/agri/xxh/xxhbz) | +| [中国乡村资讯](http://www.agri.cn/xxh/zgxczx/) | [xxh/zgxczx](https://rsshub.app/agri/xxh/zgxczx) | + +#### [视频](http://www.agri.cn/video/) + +| 分类 | ID | +| -------------------------------------------------- | ---------------------------------------------------------------- | +| [新闻资讯](http://www.agri.cn/video/xwzx/nyxw/) | [video/xwzx/nyxw](https://rsshub.app/agri/video/xwzx/nyxw) | +| [致富天地](http://www.agri.cn/video/zftd/) | [video/zftd](https://rsshub.app/agri/video/zftd) | +| [地方农业](http://www.agri.cn/video/dfny/beijing/) | [video/dfny/beijing](https://rsshub.app/agri/video/dfny/beijing) | +| [气象农业](http://www.agri.cn/video/qxny/) | [video/qxny](https://rsshub.app/agri/video/qxny) | +| [讲座培训](http://www.agri.cn/video/jzpx/) | [video/jzpx](https://rsshub.app/agri/video/jzpx) | +| [文化生活](http://www.agri.cn/video/whsh/) | [video/whsh](https://rsshub.app/agri/video/whsh) | + `, + categories: ['new-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.agri.cn/:category?'], + target: (params) => { + const category = params.category; + + return category ? `/${category}` : ''; + }, + }, + { + title: '机构 - 成果展示', + source: ['www.agri.cn/jg/cgzs/'], + target: '/jg/cgzs', + }, + { + title: '资讯 - 最新发布', + source: ['www.agri.cn/zx/zxfb/'], + target: '/zx/zxfb', + }, + { + title: '资讯 - 农业要闻', + source: ['www.agri.cn/zx/nyyw/'], + target: '/zx/nyyw', + }, + { + title: '资讯 - 中心动态', + source: ['www.agri.cn/zx/zxdt/'], + target: '/zx/zxdt', + }, + { + title: '资讯 - 通知公告', + source: ['www.agri.cn/zx/hxgg/'], + target: '/zx/hxgg', + }, + { + title: '资讯 - 全国信息联播', + source: ['www.agri.cn/zx/xxlb/'], + target: '/zx/xxlb', + }, + { + title: '生产 - 生产动态', + source: ['www.agri.cn/sc/scdt/'], + target: '/sc/scdt', + }, + { + title: '生产 - 农业品种', + source: ['www.agri.cn/sc/nypz/'], + target: '/sc/nypz', + }, + { + title: '生产 - 农事指导', + source: ['www.agri.cn/sc/nszd/'], + target: '/sc/nszd', + }, + { + title: '生产 - 农业气象', + source: ['www.agri.cn/sc/nyqx/'], + target: '/sc/nyqx', + }, + { + title: '生产 - 专项监测', + source: ['www.agri.cn/sc/zxjc/'], + target: '/sc/zxjc', + }, + { + title: '数据 - 市场动态', + source: ['www.agri.cn/sj/scdt/'], + target: '/sj/scdt', + }, + { + title: '数据 - 供需形势', + source: ['www.agri.cn/sj/gxxs/'], + target: '/sj/gxxs', + }, + { + title: '数据 - 监测预警', + source: ['www.agri.cn/sj/jcyj/'], + target: '/sj/jcyj', + }, + { + title: '信息化 - 智慧农业', + source: ['www.agri.cn/xxh/zhny/'], + target: '/xxh/zhny', + }, + { + title: '信息化 - 信息化标准', + source: ['www.agri.cn/xxh/xxhbz/'], + target: '/xxh/xxhbz', + }, + { + title: '信息化 - 中国乡村资讯', + source: ['www.agri.cn/xxh/zgxczx/'], + target: '/xxh/zgxczx', + }, + { + title: '视频 - 新闻资讯', + source: ['www.agri.cn/video/xwzx/nyxw/'], + target: '/video/xwzx/nyxw', + }, + { + title: '视频 - 致富天地', + source: ['www.agri.cn/video/zftd/'], + target: '/video/zftd', + }, + { + title: '视频 - 地方农业', + source: ['www.agri.cn/video/dfny/beijing/'], + target: '/video/dfny/beijing', + }, + { + title: '视频 - 气象农业', + source: ['www.agri.cn/video/qxny/'], + target: '/video/qxny', + }, + { + title: '视频 - 讲座培训', + source: ['www.agri.cn/video/jzpx/'], + target: '/video/jzpx', + }, + { + title: '视频 - 文化生活', + source: ['www.agri.cn/video/whsh/'], + target: '/video/whsh', + }, + ], +}; diff --git a/lib/routes/agri/namespace.ts b/lib/routes/agri/namespace.ts new file mode 100644 index 00000000000000..f059dffc2eb84a --- /dev/null +++ b/lib/routes/agri/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国农业农村信息网', + url: 'agri.cn', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/agri/templates/description.art b/lib/routes/agri/templates/description.art new file mode 100644 index 00000000000000..249654e7e618a4 --- /dev/null +++ b/lib/routes/agri/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
+ {{ image.alt }} +
+ {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} +
{{ 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)。 +::: + +
+更多分类 + +#### 图像处理 + +| 分类 | ID | +| ----------------------------------------------------- | ------------------------------------------------- | +| [图片背景移除](https://top.aibase.com/discover/37-49) | [37-49](https://rsshub.app/aibase/discover/37-49) | +| [图片无损放大](https://top.aibase.com/discover/37-50) | [37-50](https://rsshub.app/aibase/discover/37-50) | +| [图片AI修复](https://top.aibase.com/discover/37-51) | [37-51](https://rsshub.app/aibase/discover/37-51) | +| [图像生成](https://top.aibase.com/discover/37-52) | [37-52](https://rsshub.app/aibase/discover/37-52) | +| [Ai图片拓展](https://top.aibase.com/discover/37-53) | [37-53](https://rsshub.app/aibase/discover/37-53) | +| [Ai漫画生成](https://top.aibase.com/discover/37-54) | [37-54](https://rsshub.app/aibase/discover/37-54) | +| [Ai生成写真](https://top.aibase.com/discover/37-55) | [37-55](https://rsshub.app/aibase/discover/37-55) | +| [电商图片制作](https://top.aibase.com/discover/37-83) | [37-83](https://rsshub.app/aibase/discover/37-83) | +| [Ai图像转视频](https://top.aibase.com/discover/37-86) | [37-86](https://rsshub.app/aibase/discover/37-86) | + +#### 视频创作 + +| 分类 | ID | +| --------------------------------------------------- | ------------------------------------------------- | +| [视频剪辑](https://top.aibase.com/discover/38-56) | [38-56](https://rsshub.app/aibase/discover/38-56) | +| [生成视频](https://top.aibase.com/discover/38-57) | [38-57](https://rsshub.app/aibase/discover/38-57) | +| [Ai动画制作](https://top.aibase.com/discover/38-58) | [38-58](https://rsshub.app/aibase/discover/38-58) | +| [字幕生成](https://top.aibase.com/discover/38-84) | [38-84](https://rsshub.app/aibase/discover/38-84) | + +#### 效率助手 + +| 分类 | ID | +| --------------------------------------------------- | ------------------------------------------------- | +| [AI文档工具](https://top.aibase.com/discover/39-59) | [39-59](https://rsshub.app/aibase/discover/39-59) | +| [PPT](https://top.aibase.com/discover/39-60) | [39-60](https://rsshub.app/aibase/discover/39-60) | +| [思维导图](https://top.aibase.com/discover/39-61) | [39-61](https://rsshub.app/aibase/discover/39-61) | +| [表格处理](https://top.aibase.com/discover/39-62) | [39-62](https://rsshub.app/aibase/discover/39-62) | +| [Ai办公助手](https://top.aibase.com/discover/39-63) | [39-63](https://rsshub.app/aibase/discover/39-63) | + +#### 写作灵感 + +| 分类 | ID | +| ------------------------------------------------- | ------------------------------------------------- | +| [文案写作](https://top.aibase.com/discover/40-64) | [40-64](https://rsshub.app/aibase/discover/40-64) | +| [论文写作](https://top.aibase.com/discover/40-88) | [40-88](https://rsshub.app/aibase/discover/40-88) | + +#### 艺术灵感 + +| 分类 | ID | +| --------------------------------------------------- | ------------------------------------------------- | +| [音乐创作](https://top.aibase.com/discover/41-65) | [41-65](https://rsshub.app/aibase/discover/41-65) | +| [设计创作](https://top.aibase.com/discover/41-66) | [41-66](https://rsshub.app/aibase/discover/41-66) | +| [Ai图标生成](https://top.aibase.com/discover/41-67) | [41-67](https://rsshub.app/aibase/discover/41-67) | + +#### 趣味 + +| 分类 | ID | +| ----------------------------------------------------- | ------------------------------------------------- | +| [Ai名字生成器](https://top.aibase.com/discover/42-68) | [42-68](https://rsshub.app/aibase/discover/42-68) | +| [游戏娱乐](https://top.aibase.com/discover/42-71) | [42-71](https://rsshub.app/aibase/discover/42-71) | +| [其他](https://top.aibase.com/discover/42-72) | [42-72](https://rsshub.app/aibase/discover/42-72) | + +#### 开发编程 + +| 分类 | ID | +| --------------------------------------------------- | ------------------------------------------------- | +| [开发编程](https://top.aibase.com/discover/43-73) | [43-73](https://rsshub.app/aibase/discover/43-73) | +| [Ai开放平台](https://top.aibase.com/discover/43-74) | [43-74](https://rsshub.app/aibase/discover/43-74) | +| [Ai算力平台](https://top.aibase.com/discover/43-75) | [43-75](https://rsshub.app/aibase/discover/43-75) | + +#### 聊天机器人 + +| 分类 | ID | +| ------------------------------------------------- | ------------------------------------------------- | +| [智能聊天](https://top.aibase.com/discover/44-76) | [44-76](https://rsshub.app/aibase/discover/44-76) | +| [智能客服](https://top.aibase.com/discover/44-77) | [44-77](https://rsshub.app/aibase/discover/44-77) | + +#### 翻译 + +| 分类 | ID | +| --------------------------------------------- | ------------------------------------------------- | +| [翻译](https://top.aibase.com/discover/46-79) | [46-79](https://rsshub.app/aibase/discover/46-79) | + +#### 教育学习 + +| 分类 | ID | +| ------------------------------------------------- | ------------------------------------------------- | +| [教育学习](https://top.aibase.com/discover/47-80) | [47-80](https://rsshub.app/aibase/discover/47-80) | + +#### 智能营销 + +| 分类 | ID | +| ------------------------------------------------- | ------------------------------------------------- | +| [智能营销](https://top.aibase.com/discover/48-81) | [48-81](https://rsshub.app/aibase/discover/48-81) | + +#### 法律 + +| 分类 | ID | +| ----------------------------------------------- | ----------------------------------------------------- | +| [法律](https://top.aibase.com/discover/138-139) | [138-139](https://rsshub.app/aibase/discover/138-139) | +
+ `, + categories: ['new-media', 'popular'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['top.aibase.com/discover/:id'], + target: (params) => { + const id = params.id; + + return `/discover${id ? `/${id}` : ''}`; + }, + }, + { + title: '图像处理 - 图片背景移除', + source: ['top.aibase.com/discover/37-49'], + target: '/discover/37-49', + }, + { + title: '图像处理 - 图片无损放大', + source: ['top.aibase.com/discover/37-50'], + target: '/discover/37-50', + }, + { + title: '图像处理 - 图片AI修复', + source: ['top.aibase.com/discover/37-51'], + target: '/discover/37-51', + }, + { + title: '图像处理 - 图像生成', + source: ['top.aibase.com/discover/37-52'], + target: '/discover/37-52', + }, + { + title: '图像处理 - Ai图片拓展', + source: ['top.aibase.com/discover/37-53'], + target: '/discover/37-53', + }, + { + title: '图像处理 - Ai漫画生成', + source: ['top.aibase.com/discover/37-54'], + target: '/discover/37-54', + }, + { + title: '图像处理 - Ai生成写真', + source: ['top.aibase.com/discover/37-55'], + target: '/discover/37-55', + }, + { + title: '图像处理 - 电商图片制作', + source: ['top.aibase.com/discover/37-83'], + target: '/discover/37-83', + }, + { + title: '图像处理 - Ai图像转视频', + source: ['top.aibase.com/discover/37-86'], + target: '/discover/37-86', + }, + { + title: '视频创作 - 视频剪辑', + source: ['top.aibase.com/discover/38-56'], + target: '/discover/38-56', + }, + { + title: '视频创作 - 生成视频', + source: ['top.aibase.com/discover/38-57'], + target: '/discover/38-57', + }, + { + title: '视频创作 - Ai动画制作', + source: ['top.aibase.com/discover/38-58'], + target: '/discover/38-58', + }, + { + title: '视频创作 - 字幕生成', + source: ['top.aibase.com/discover/38-84'], + target: '/discover/38-84', + }, + { + title: '效率助手 - AI文档工具', + source: ['top.aibase.com/discover/39-59'], + target: '/discover/39-59', + }, + { + title: '效率助手 - PPT', + source: ['top.aibase.com/discover/39-60'], + target: '/discover/39-60', + }, + { + title: '效率助手 - 思维导图', + source: ['top.aibase.com/discover/39-61'], + target: '/discover/39-61', + }, + { + title: '效率助手 - 表格处理', + source: ['top.aibase.com/discover/39-62'], + target: '/discover/39-62', + }, + { + title: '效率助手 - Ai办公助手', + source: ['top.aibase.com/discover/39-63'], + target: '/discover/39-63', + }, + { + title: '写作灵感 - 文案写作', + source: ['top.aibase.com/discover/40-64'], + target: '/discover/40-64', + }, + { + title: '写作灵感 - 论文写作', + source: ['top.aibase.com/discover/40-88'], + target: '/discover/40-88', + }, + { + title: '艺术灵感 - 音乐创作', + source: ['top.aibase.com/discover/41-65'], + target: '/discover/41-65', + }, + { + title: '艺术灵感 - 设计创作', + source: ['top.aibase.com/discover/41-66'], + target: '/discover/41-66', + }, + { + title: '艺术灵感 - Ai图标生成', + source: ['top.aibase.com/discover/41-67'], + target: '/discover/41-67', + }, + { + title: '趣味 - Ai名字生成器', + source: ['top.aibase.com/discover/42-68'], + target: '/discover/42-68', + }, + { + title: '趣味 - 游戏娱乐', + source: ['top.aibase.com/discover/42-71'], + target: '/discover/42-71', + }, + { + title: '趣味 - 其他', + source: ['top.aibase.com/discover/42-72'], + target: '/discover/42-72', + }, + { + title: '开发编程 - 开发编程', + source: ['top.aibase.com/discover/43-73'], + target: '/discover/43-73', + }, + { + title: '开发编程 - Ai开放平台', + source: ['top.aibase.com/discover/43-74'], + target: '/discover/43-74', + }, + { + title: '开发编程 - Ai算力平台', + source: ['top.aibase.com/discover/43-75'], + target: '/discover/43-75', + }, + { + title: '聊天机器人 - 智能聊天', + source: ['top.aibase.com/discover/44-76'], + target: '/discover/44-76', + }, + { + title: '聊天机器人 - 智能客服', + source: ['top.aibase.com/discover/44-77'], + target: '/discover/44-77', + }, + { + title: '翻译 - 翻译', + source: ['top.aibase.com/discover/46-79'], + target: '/discover/46-79', + }, + { + title: '教育学习 - 教育学习', + source: ['top.aibase.com/discover/47-80'], + target: '/discover/47-80', + }, + { + title: '智能营销 - 智能营销', + source: ['top.aibase.com/discover/48-81'], + target: '/discover/48-81', + }, + { + title: '法律 - 法律', + source: ['top.aibase.com/discover/138-139'], + target: '/discover/138-139', + }, + ], +}; diff --git a/lib/routes/aibase/namespace.ts b/lib/routes/aibase/namespace.ts new file mode 100644 index 00000000000000..8969f5915e037e --- /dev/null +++ b/lib/routes/aibase/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'AIbase', + url: 'aibase.com', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/aibase/news.ts b/lib/routes/aibase/news.ts new file mode 100644 index 00000000000000..0115423b69b2aa --- /dev/null +++ b/lib/routes/aibase/news.ts @@ -0,0 +1,118 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import { rootUrl, buildApiUrl } from './util'; + +export const route: Route = { + path: '/news', + name: '资讯', + url: 'www.aibase.com', + maintainers: ['zreo0'], + handler: async (ctx) => { + // 每页数量限制 + const limit = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + // 用项目中已有的获取页面方法,获取页面以及 Token + const currentUrl = new URL('discover', rootUrl).href; + const currentHtml = await ofetch(currentUrl); + const $ = load(currentHtml); + const logoSrc = $('img.logo').prop('src'); + const image = logoSrc ? new URL(logoSrc, rootUrl).href : ''; + const author = $('title').text().split(/_/).pop(); + const { apiInfoListUrl } = await buildApiUrl($); + // 获取资讯列表,解析数据 + const data: NewsItem[] = await ofetch(apiInfoListUrl, { + headers: { + accept: 'application/json;charset=utf-8', + }, + query: { + pagesize: limit, + page: 1, + type: 1, + isen: 0, + }, + }); + const items = data.map((item) => ({ + // 文章标题 + title: item.title, + // 文章链接 + link: `https://www.aibase.com/zh/news/${item.Id}`, + // 文章正文 + description: item.summary, + // 文章发布日期 + pubDate: parseDate(item.addtime), + // 文章作者 + author: item.author || 'AI Base', + })); + + return { + title: 'AI新闻资讯', + description: 'AI新闻资讯 - 不错过全球AI革新的每一个时刻', + language: 'zh-cn', + link: 'https://www.aibase.com/zh/news', + item: items, + allowEmpty: true, + image, + author, + }; + }, + example: '/aibase/news', + description: '获取 AI 资讯列表', + categories: ['new-media', 'popular'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.aibase.com/zh/news'], + target: '/news', + }, + ], +}; + +/** API 返回的资讯结构 */ +interface NewsItem { + /** 文章 ID */ + Id: number; + /** 文章标题 */ + title: string; + /** 文章副标题 */ + subtitle: string; + /** 文章简要描述 */ + description: string; + /** 文章主图 */ + thumb: string; + classname: string; + /** 正文总结 */ + summary: string; + /** 标签,字符串,样例:[\"人工智能\",\"Hingham高中\"] */ + tags: string; + /** 可能是来源 */ + sourcename: string; + /** 作者 */ + author: string; + status: number; + url: string; + type: number; + added: number; + /** 添加时间 */ + addtime: string; + /** 更新时间 */ + upded: number; + updtime: string; + isshoulu: number; + vurl: string; + vsize: number; + weight: number; + isailog: number; + sites: string; + categrates: string; + /** 访问量 */ + pv: number; +} diff --git a/lib/routes/aibase/templates/description.art b/lib/routes/aibase/templates/description.art new file mode 100644 index 00000000000000..fae2782a3c7dfa --- /dev/null +++ b/lib/routes/aibase/templates/description.art @@ -0,0 +1,100 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
+ {{ image.alt }} +
+ {{ /if }} + {{ /each }} +{{ /if }} + +{{ if item }} + + + + + + + + + + + + + + + + {{ if item.desc }} + {{ item.desc }} + {{ else }} + 无 + {{ /if }} + + + + + + + + + + + + + + + + + + +
名称{{ item.name }}
标签 + {{ each strToArray(item.tags) t }} + {{ t }}  + {{ /each }} +
类型 + {{ if item.proctypename }} + {{ item.proctypename }} + {{ else }} + 无 + {{ /if }} +
描述
需求人群 + {{ set list = strToArray(item.use) }} + {{ if list.length === 1 }} + {{ list[0] }} + {{ else }} + {{ each list l }} +
  • {{ l }}
  • + {{ /each }} + {{ /if }} +
    使用场景示例 + {{ set list = strToArray(item.example) }} + {{ if list.length === 1 }} + {{ list[0] }} + {{ else }} + {{ each list l }} +
  • {{ l }}
  • + {{ /each }} + {{ /if }} +
    产品特色 + {{ set list = strToArray(item.functions) }} + {{ if list.length === 1 }} + {{ list[0] }} + {{ else }} + {{ each list l }} +
  • {{ l }}
  • + {{ /each }} + {{ /if }} +
    站点 + {{ if item.url }} + + {{ item.url }} + + {{ else }} + 无 + {{ /if }} +
    +{{ /if }} \ No newline at end of file diff --git a/lib/routes/aibase/topic.ts b/lib/routes/aibase/topic.ts new file mode 100644 index 00000000000000..3a9e4171791b3a --- /dev/null +++ b/lib/routes/aibase/topic.ts @@ -0,0 +1,614 @@ +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, filter = 'id' } = ctx.req.param(); + + const limit = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const currentUrl = new URL(id ? `topic/${id}` : 'discover', rootUrl).href; + + const currentHtml = await ofetch(currentUrl); + + const $ = load(currentHtml); + + const { apiTagProcUrl } = await buildApiUrl($); + + const { + data: { results: apiTagProcs }, + } = await ofetch(apiTagProcUrl, { + query: { + ...(id ? { tag: id } : {}), + page: 1, + pagesize: 20, + f: filter, + o: 'desc', + ticket: '', + }, + }); + + const items = processItems(apiTagProcs?.slice(0, limit) ?? []); + + const image = new URL($('img.logo').prop('src'), rootUrl).href; + + const author = $('title').text().split(/_/).pop(); + + return { + title: `${author}${id ? ` | ${id}` : ''}`, + description: $('meta[property="og:description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author, + }; +}; + +export const route: Route = { + path: '/topic/:id?/:filter?', + name: '标签', + url: 'top.aibase.com', + maintainers: ['nczitzk'], + handler, + example: '/aibase/topic', + parameters: { id: '标签,默认为空,即全部产品,可在对应标签页 URL 中找到', filter: '过滤器,默认为 `id` 即最新,可选 `pv` 即热门' }, + description: `::: tip + 若订阅 [AI](https://top.aibase.com/topic/AI),网址为 \`https://top.aibase.com/topic/AI\`。截取 \`https://top.aibase.com/topic\` 到末尾的部分 \`AI\` 作为参数填入,此时路由为 [\`/aibase/topic/AI\`](https://rsshub.app/aibase/topic/AI)。 +::: + +::: tip + 此处查看 [全部标签](https://top.aibase.com/topic) +::: + +
    +更多标签 + +| [AI](https://top.aibase.com/topic/AI) | [人工智能](https://top.aibase.com/topic/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD) | [图像生成](https://top.aibase.com/topic/%E5%9B%BE%E5%83%8F%E7%94%9F%E6%88%90) | [自动化](https://top.aibase.com/topic/%E8%87%AA%E5%8A%A8%E5%8C%96) | [AI 助手](https://top.aibase.com/topic/AI%E5%8A%A9%E6%89%8B) | +| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| [聊天机器人](https://top.aibase.com/topic/%E8%81%8A%E5%A4%A9%E6%9C%BA%E5%99%A8%E4%BA%BA) | [个性化](https://top.aibase.com/topic/%E4%B8%AA%E6%80%A7%E5%8C%96) | [社交媒体](https://top.aibase.com/topic/%E7%A4%BE%E4%BA%A4%E5%AA%92%E4%BD%93) | [图像处理](https://top.aibase.com/topic/%E5%9B%BE%E5%83%8F%E5%A4%84%E7%90%86) | [数据分析](https://top.aibase.com/topic/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90) | +| [自然语言处理](https://top.aibase.com/topic/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%A4%84%E7%90%86) | [聊天](https://top.aibase.com/topic/%E8%81%8A%E5%A4%A9) | [机器学习](https://top.aibase.com/topic/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0) | [教育](https://top.aibase.com/topic/%E6%95%99%E8%82%B2) | [内容创作](https://top.aibase.com/topic/%E5%86%85%E5%AE%B9%E5%88%9B%E4%BD%9C) | +| [生产力](https://top.aibase.com/topic/%E7%94%9F%E4%BA%A7%E5%8A%9B) | [设计](https://top.aibase.com/topic/%E8%AE%BE%E8%AE%A1) | [ChatGPT](https://top.aibase.com/topic/ChatGPT) | [创意](https://top.aibase.com/topic/%E5%88%9B%E6%84%8F) | [开源](https://top.aibase.com/topic/%E5%BC%80%E6%BA%90) | +| [写作](https://top.aibase.com/topic/%E5%86%99%E4%BD%9C) | [效率助手](https://top.aibase.com/topic/%E6%95%88%E7%8E%87%E5%8A%A9%E6%89%8B) | [学习](https://top.aibase.com/topic/%E5%AD%A6%E4%B9%A0) | [插件](https://top.aibase.com/topic/%E6%8F%92%E4%BB%B6) | [翻译](https://top.aibase.com/topic/%E7%BF%BB%E8%AF%91) | +| [团队协作](https://top.aibase.com/topic/%E5%9B%A2%E9%98%9F%E5%8D%8F%E4%BD%9C) | [SEO](https://top.aibase.com/topic/SEO) | [营销](https://top.aibase.com/topic/%E8%90%A5%E9%94%80) | [内容生成](https://top.aibase.com/topic/%E5%86%85%E5%AE%B9%E7%94%9F%E6%88%90) | [AI 技术](https://top.aibase.com/topic/AI%E6%8A%80%E6%9C%AF) | +| [AI 工具](https://top.aibase.com/topic/AI%E5%B7%A5%E5%85%B7) | [智能助手](https://top.aibase.com/topic/%E6%99%BA%E8%83%BD%E5%8A%A9%E6%89%8B) | [深度学习](https://top.aibase.com/topic/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0) | [多语言支持](https://top.aibase.com/topic/%E5%A4%9A%E8%AF%AD%E8%A8%80%E6%94%AF%E6%8C%81) | [视频](https://top.aibase.com/topic/%E8%A7%86%E9%A2%91) | +| [艺术](https://top.aibase.com/topic/%E8%89%BA%E6%9C%AF) | [文本生成](https://top.aibase.com/topic/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90) | [开发编程](https://top.aibase.com/topic/%E5%BC%80%E5%8F%91%E7%BC%96%E7%A8%8B) | [协作](https://top.aibase.com/topic/%E5%8D%8F%E4%BD%9C) | [语言模型](https://top.aibase.com/topic/%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B) | +| [工具](https://top.aibase.com/topic/%E5%B7%A5%E5%85%B7) | [销售](https://top.aibase.com/topic/%E9%94%80%E5%94%AE) | [生产力工具](https://top.aibase.com/topic/%E7%94%9F%E4%BA%A7%E5%8A%9B%E5%B7%A5%E5%85%B7) | [AI 写作](https://top.aibase.com/topic/AI%E5%86%99%E4%BD%9C) | [创作](https://top.aibase.com/topic/%E5%88%9B%E4%BD%9C) | +| [工作效率](https://top.aibase.com/topic/%E5%B7%A5%E4%BD%9C%E6%95%88%E7%8E%87) | [无代码](https://top.aibase.com/topic/%E6%97%A0%E4%BB%A3%E7%A0%81) | [隐私保护](https://top.aibase.com/topic/%E9%9A%90%E7%A7%81%E4%BF%9D%E6%8A%A4) | [视频编辑](https://top.aibase.com/topic/%E8%A7%86%E9%A2%91%E7%BC%96%E8%BE%91) | [摘要](https://top.aibase.com/topic/%E6%91%98%E8%A6%81) | +| [多语言](https://top.aibase.com/topic/%E5%A4%9A%E8%AF%AD%E8%A8%80) | [求职](https://top.aibase.com/topic/%E6%B1%82%E8%81%8C) | [GPT](https://top.aibase.com/topic/GPT) | [音乐](https://top.aibase.com/topic/%E9%9F%B3%E4%B9%90) | [视频创作](https://top.aibase.com/topic/%E8%A7%86%E9%A2%91%E5%88%9B%E4%BD%9C) | +| [设计工具](https://top.aibase.com/topic/%E8%AE%BE%E8%AE%A1%E5%B7%A5%E5%85%B7) | [搜索](https://top.aibase.com/topic/%E6%90%9C%E7%B4%A2) | [写作工具](https://top.aibase.com/topic/%E5%86%99%E4%BD%9C%E5%B7%A5%E5%85%B7) | [视频生成](https://top.aibase.com/topic/%E8%A7%86%E9%A2%91%E7%94%9F%E6%88%90) | [招聘](https://top.aibase.com/topic/%E6%8B%9B%E8%81%98) | +| [代码生成](https://top.aibase.com/topic/%E4%BB%A3%E7%A0%81%E7%94%9F%E6%88%90) | [大型语言模型](https://top.aibase.com/topic/%E5%A4%A7%E5%9E%8B%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B) | [语音识别](https://top.aibase.com/topic/%E8%AF%AD%E9%9F%B3%E8%AF%86%E5%88%AB) | [编程](https://top.aibase.com/topic/%E7%BC%96%E7%A8%8B) | [在线工具](https://top.aibase.com/topic/%E5%9C%A8%E7%BA%BF%E5%B7%A5%E5%85%B7) | +| [API](https://top.aibase.com/topic/API) | [趣味](https://top.aibase.com/topic/%E8%B6%A3%E5%91%B3) | [客户支持](https://top.aibase.com/topic/%E5%AE%A2%E6%88%B7%E6%94%AF%E6%8C%81) | [语音合成](https://top.aibase.com/topic/%E8%AF%AD%E9%9F%B3%E5%90%88%E6%88%90) | [图像](https://top.aibase.com/topic/%E5%9B%BE%E5%83%8F) | +| [电子商务](https://top.aibase.com/topic/%E7%94%B5%E5%AD%90%E5%95%86%E5%8A%A1) | [SEO 优化](https://top.aibase.com/topic/SEO%E4%BC%98%E5%8C%96) | [AI 辅助](https://top.aibase.com/topic/AI%E8%BE%85%E5%8A%A9) | [AI 生成](https://top.aibase.com/topic/AI%E7%94%9F%E6%88%90) | [创作工具](https://top.aibase.com/topic/%E5%88%9B%E4%BD%9C%E5%B7%A5%E5%85%B7) | +| [免费](https://top.aibase.com/topic/%E5%85%8D%E8%B4%B9) | [LinkedIn](https://top.aibase.com/topic/LinkedIn) | [博客](https://top.aibase.com/topic/%E5%8D%9A%E5%AE%A2) | [写作助手](https://top.aibase.com/topic/%E5%86%99%E4%BD%9C%E5%8A%A9%E6%89%8B) | [助手](https://top.aibase.com/topic/%E5%8A%A9%E6%89%8B) | +| [智能](https://top.aibase.com/topic/%E6%99%BA%E8%83%BD) | [健康](https://top.aibase.com/topic/%E5%81%A5%E5%BA%B7) | [多模态](https://top.aibase.com/topic/%E5%A4%9A%E6%A8%A1%E6%80%81) | [任务管理](https://top.aibase.com/topic/%E4%BB%BB%E5%8A%A1%E7%AE%A1%E7%90%86) | [电子邮件](https://top.aibase.com/topic/%E7%94%B5%E5%AD%90%E9%82%AE%E4%BB%B6) | +| [笔记](https://top.aibase.com/topic/%E7%AC%94%E8%AE%B0) | [搜索引擎](https://top.aibase.com/topic/%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E) | [计算机视觉](https://top.aibase.com/topic/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%A7%86%E8%A7%89) | [社区](https://top.aibase.com/topic/%E7%A4%BE%E5%8C%BA) | [效率](https://top.aibase.com/topic/%E6%95%88%E7%8E%87) | +| [知识管理](https://top.aibase.com/topic/%E7%9F%A5%E8%AF%86%E7%AE%A1%E7%90%86) | [LLM](https://top.aibase.com/topic/LLM) | [智能聊天](https://top.aibase.com/topic/%E6%99%BA%E8%83%BD%E8%81%8A%E5%A4%A9) | [社交](https://top.aibase.com/topic/%E7%A4%BE%E4%BA%A4) | [语言学习](https://top.aibase.com/topic/%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0) | +| [娱乐](https://top.aibase.com/topic/%E5%A8%B1%E4%B9%90) | [简历](https://top.aibase.com/topic/%E7%AE%80%E5%8E%86) | [OpenAI](https://top.aibase.com/topic/OpenAI) | [客户服务](https://top.aibase.com/topic/%E5%AE%A2%E6%88%B7%E6%9C%8D%E5%8A%A1) | [室内设计](https://top.aibase.com/topic/%E5%AE%A4%E5%86%85%E8%AE%BE%E8%AE%A1) | +
    + `, + categories: ['new-media', 'popular'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['top.aibase.com/topic/:id'], + target: (params) => { + const id = params.id; + + return `/topic${id ? `/${id}` : ''}`; + }, + }, + { + title: 'AI', + source: ['top.aibase.com/topic/AI'], + target: '/topic/AI', + }, + { + title: '人工智能', + source: ['top.aibase.com/topic/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD'], + target: '/topic/人工智能', + }, + { + title: '图像生成', + source: ['top.aibase.com/topic/%E5%9B%BE%E5%83%8F%E7%94%9F%E6%88%90'], + target: '/topic/图像生成', + }, + { + title: '自动化', + source: ['top.aibase.com/topic/%E8%87%AA%E5%8A%A8%E5%8C%96'], + target: '/topic/自动化', + }, + { + title: 'AI助手', + source: ['top.aibase.com/topic/AI%E5%8A%A9%E6%89%8B'], + target: '/topic/AI助手', + }, + { + title: '聊天机器人', + source: ['top.aibase.com/topic/%E8%81%8A%E5%A4%A9%E6%9C%BA%E5%99%A8%E4%BA%BA'], + target: '/topic/聊天机器人', + }, + { + title: '个性化', + source: ['top.aibase.com/topic/%E4%B8%AA%E6%80%A7%E5%8C%96'], + target: '/topic/个性化', + }, + { + title: '社交媒体', + source: ['top.aibase.com/topic/%E7%A4%BE%E4%BA%A4%E5%AA%92%E4%BD%93'], + target: '/topic/社交媒体', + }, + { + title: '图像处理', + source: ['top.aibase.com/topic/%E5%9B%BE%E5%83%8F%E5%A4%84%E7%90%86'], + target: '/topic/图像处理', + }, + { + title: '数据分析', + source: ['top.aibase.com/topic/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90'], + target: '/topic/数据分析', + }, + { + title: '自然语言处理', + source: ['top.aibase.com/topic/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%A4%84%E7%90%86'], + target: '/topic/自然语言处理', + }, + { + title: '聊天', + source: ['top.aibase.com/topic/%E8%81%8A%E5%A4%A9'], + target: '/topic/聊天', + }, + { + title: '机器学习', + source: ['top.aibase.com/topic/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0'], + target: '/topic/机器学习', + }, + { + title: '教育', + source: ['top.aibase.com/topic/%E6%95%99%E8%82%B2'], + target: '/topic/教育', + }, + { + title: '内容创作', + source: ['top.aibase.com/topic/%E5%86%85%E5%AE%B9%E5%88%9B%E4%BD%9C'], + target: '/topic/内容创作', + }, + { + title: '生产力', + source: ['top.aibase.com/topic/%E7%94%9F%E4%BA%A7%E5%8A%9B'], + target: '/topic/生产力', + }, + { + title: '设计', + source: ['top.aibase.com/topic/%E8%AE%BE%E8%AE%A1'], + target: '/topic/设计', + }, + { + title: 'ChatGPT', + source: ['top.aibase.com/topic/ChatGPT'], + target: '/topic/ChatGPT', + }, + { + title: '创意', + source: ['top.aibase.com/topic/%E5%88%9B%E6%84%8F'], + target: '/topic/创意', + }, + { + title: '开源', + source: ['top.aibase.com/topic/%E5%BC%80%E6%BA%90'], + target: '/topic/开源', + }, + { + title: '写作', + source: ['top.aibase.com/topic/%E5%86%99%E4%BD%9C'], + target: '/topic/写作', + }, + { + title: '效率助手', + source: ['top.aibase.com/topic/%E6%95%88%E7%8E%87%E5%8A%A9%E6%89%8B'], + target: '/topic/效率助手', + }, + { + title: '学习', + source: ['top.aibase.com/topic/%E5%AD%A6%E4%B9%A0'], + target: '/topic/学习', + }, + { + title: '插件', + source: ['top.aibase.com/topic/%E6%8F%92%E4%BB%B6'], + target: '/topic/插件', + }, + { + title: '翻译', + source: ['top.aibase.com/topic/%E7%BF%BB%E8%AF%91'], + target: '/topic/翻译', + }, + { + title: '团队协作', + source: ['top.aibase.com/topic/%E5%9B%A2%E9%98%9F%E5%8D%8F%E4%BD%9C'], + target: '/topic/团队协作', + }, + { + title: 'SEO', + source: ['top.aibase.com/topic/SEO'], + target: '/topic/SEO', + }, + { + title: '营销', + source: ['top.aibase.com/topic/%E8%90%A5%E9%94%80'], + target: '/topic/营销', + }, + { + title: '内容生成', + source: ['top.aibase.com/topic/%E5%86%85%E5%AE%B9%E7%94%9F%E6%88%90'], + target: '/topic/内容生成', + }, + { + title: 'AI技术', + source: ['top.aibase.com/topic/AI%E6%8A%80%E6%9C%AF'], + target: '/topic/AI技术', + }, + { + title: 'AI工具', + source: ['top.aibase.com/topic/AI%E5%B7%A5%E5%85%B7'], + target: '/topic/AI工具', + }, + { + title: '智能助手', + source: ['top.aibase.com/topic/%E6%99%BA%E8%83%BD%E5%8A%A9%E6%89%8B'], + target: '/topic/智能助手', + }, + { + title: '深度学习', + source: ['top.aibase.com/topic/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0'], + target: '/topic/深度学习', + }, + { + title: '多语言支持', + source: ['top.aibase.com/topic/%E5%A4%9A%E8%AF%AD%E8%A8%80%E6%94%AF%E6%8C%81'], + target: '/topic/多语言支持', + }, + { + title: '视频', + source: ['top.aibase.com/topic/%E8%A7%86%E9%A2%91'], + target: '/topic/视频', + }, + { + title: '艺术', + source: ['top.aibase.com/topic/%E8%89%BA%E6%9C%AF'], + target: '/topic/艺术', + }, + { + title: '文本生成', + source: ['top.aibase.com/topic/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90'], + target: '/topic/文本生成', + }, + { + title: '开发编程', + source: ['top.aibase.com/topic/%E5%BC%80%E5%8F%91%E7%BC%96%E7%A8%8B'], + target: '/topic/开发编程', + }, + { + title: '协作', + source: ['top.aibase.com/topic/%E5%8D%8F%E4%BD%9C'], + target: '/topic/协作', + }, + { + title: '语言模型', + source: ['top.aibase.com/topic/%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B'], + target: '/topic/语言模型', + }, + { + title: '工具', + source: ['top.aibase.com/topic/%E5%B7%A5%E5%85%B7'], + target: '/topic/工具', + }, + { + title: '销售', + source: ['top.aibase.com/topic/%E9%94%80%E5%94%AE'], + target: '/topic/销售', + }, + { + title: '生产力工具', + source: ['top.aibase.com/topic/%E7%94%9F%E4%BA%A7%E5%8A%9B%E5%B7%A5%E5%85%B7'], + target: '/topic/生产力工具', + }, + { + title: 'AI写作', + source: ['top.aibase.com/topic/AI%E5%86%99%E4%BD%9C'], + target: '/topic/AI写作', + }, + { + title: '创作', + source: ['top.aibase.com/topic/%E5%88%9B%E4%BD%9C'], + target: '/topic/创作', + }, + { + title: '工作效率', + source: ['top.aibase.com/topic/%E5%B7%A5%E4%BD%9C%E6%95%88%E7%8E%87'], + target: '/topic/工作效率', + }, + { + title: '无代码', + source: ['top.aibase.com/topic/%E6%97%A0%E4%BB%A3%E7%A0%81'], + target: '/topic/无代码', + }, + { + title: '隐私保护', + source: ['top.aibase.com/topic/%E9%9A%90%E7%A7%81%E4%BF%9D%E6%8A%A4'], + target: '/topic/隐私保护', + }, + { + title: '视频编辑', + source: ['top.aibase.com/topic/%E8%A7%86%E9%A2%91%E7%BC%96%E8%BE%91'], + target: '/topic/视频编辑', + }, + { + title: '摘要', + source: ['top.aibase.com/topic/%E6%91%98%E8%A6%81'], + target: '/topic/摘要', + }, + { + title: '多语言', + source: ['top.aibase.com/topic/%E5%A4%9A%E8%AF%AD%E8%A8%80'], + target: '/topic/多语言', + }, + { + title: '求职', + source: ['top.aibase.com/topic/%E6%B1%82%E8%81%8C'], + target: '/topic/求职', + }, + { + title: 'GPT', + source: ['top.aibase.com/topic/GPT'], + target: '/topic/GPT', + }, + { + title: '音乐', + source: ['top.aibase.com/topic/%E9%9F%B3%E4%B9%90'], + target: '/topic/音乐', + }, + { + title: '视频创作', + source: ['top.aibase.com/topic/%E8%A7%86%E9%A2%91%E5%88%9B%E4%BD%9C'], + target: '/topic/视频创作', + }, + { + title: '设计工具', + source: ['top.aibase.com/topic/%E8%AE%BE%E8%AE%A1%E5%B7%A5%E5%85%B7'], + target: '/topic/设计工具', + }, + { + title: '搜索', + source: ['top.aibase.com/topic/%E6%90%9C%E7%B4%A2'], + target: '/topic/搜索', + }, + { + title: '写作工具', + source: ['top.aibase.com/topic/%E5%86%99%E4%BD%9C%E5%B7%A5%E5%85%B7'], + target: '/topic/写作工具', + }, + { + title: '视频生成', + source: ['top.aibase.com/topic/%E8%A7%86%E9%A2%91%E7%94%9F%E6%88%90'], + target: '/topic/视频生成', + }, + { + title: '招聘', + source: ['top.aibase.com/topic/%E6%8B%9B%E8%81%98'], + target: '/topic/招聘', + }, + { + title: '代码生成', + source: ['top.aibase.com/topic/%E4%BB%A3%E7%A0%81%E7%94%9F%E6%88%90'], + target: '/topic/代码生成', + }, + { + title: '大型语言模型', + source: ['top.aibase.com/topic/%E5%A4%A7%E5%9E%8B%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B'], + target: '/topic/大型语言模型', + }, + { + title: '语音识别', + source: ['top.aibase.com/topic/%E8%AF%AD%E9%9F%B3%E8%AF%86%E5%88%AB'], + target: '/topic/语音识别', + }, + { + title: '编程', + source: ['top.aibase.com/topic/%E7%BC%96%E7%A8%8B'], + target: '/topic/编程', + }, + { + title: '在线工具', + source: ['top.aibase.com/topic/%E5%9C%A8%E7%BA%BF%E5%B7%A5%E5%85%B7'], + target: '/topic/在线工具', + }, + { + title: 'API', + source: ['top.aibase.com/topic/API'], + target: '/topic/API', + }, + { + title: '趣味', + source: ['top.aibase.com/topic/%E8%B6%A3%E5%91%B3'], + target: '/topic/趣味', + }, + { + title: '客户支持', + source: ['top.aibase.com/topic/%E5%AE%A2%E6%88%B7%E6%94%AF%E6%8C%81'], + target: '/topic/客户支持', + }, + { + title: '语音合成', + source: ['top.aibase.com/topic/%E8%AF%AD%E9%9F%B3%E5%90%88%E6%88%90'], + target: '/topic/语音合成', + }, + { + title: '图像', + source: ['top.aibase.com/topic/%E5%9B%BE%E5%83%8F'], + target: '/topic/图像', + }, + { + title: '电子商务', + source: ['top.aibase.com/topic/%E7%94%B5%E5%AD%90%E5%95%86%E5%8A%A1'], + target: '/topic/电子商务', + }, + { + title: 'SEO优化', + source: ['top.aibase.com/topic/SEO%E4%BC%98%E5%8C%96'], + target: '/topic/SEO优化', + }, + { + title: 'AI辅助', + source: ['top.aibase.com/topic/AI%E8%BE%85%E5%8A%A9'], + target: '/topic/AI辅助', + }, + { + title: 'AI生成', + source: ['top.aibase.com/topic/AI%E7%94%9F%E6%88%90'], + target: '/topic/AI生成', + }, + { + title: '创作工具', + source: ['top.aibase.com/topic/%E5%88%9B%E4%BD%9C%E5%B7%A5%E5%85%B7'], + target: '/topic/创作工具', + }, + { + title: '免费', + source: ['top.aibase.com/topic/%E5%85%8D%E8%B4%B9'], + target: '/topic/免费', + }, + { + title: 'LinkedIn', + source: ['top.aibase.com/topic/LinkedIn'], + target: '/topic/LinkedIn', + }, + { + title: '博客', + source: ['top.aibase.com/topic/%E5%8D%9A%E5%AE%A2'], + target: '/topic/博客', + }, + { + title: '写作助手', + source: ['top.aibase.com/topic/%E5%86%99%E4%BD%9C%E5%8A%A9%E6%89%8B'], + target: '/topic/写作助手', + }, + { + title: '助手', + source: ['top.aibase.com/topic/%E5%8A%A9%E6%89%8B'], + target: '/topic/助手', + }, + { + title: '智能', + source: ['top.aibase.com/topic/%E6%99%BA%E8%83%BD'], + target: '/topic/智能', + }, + { + title: '健康', + source: ['top.aibase.com/topic/%E5%81%A5%E5%BA%B7'], + target: '/topic/健康', + }, + { + title: '多模态', + source: ['top.aibase.com/topic/%E5%A4%9A%E6%A8%A1%E6%80%81'], + target: '/topic/多模态', + }, + { + title: '任务管理', + source: ['top.aibase.com/topic/%E4%BB%BB%E5%8A%A1%E7%AE%A1%E7%90%86'], + target: '/topic/任务管理', + }, + { + title: '电子邮件', + source: ['top.aibase.com/topic/%E7%94%B5%E5%AD%90%E9%82%AE%E4%BB%B6'], + target: '/topic/电子邮件', + }, + { + title: '笔记', + source: ['top.aibase.com/topic/%E7%AC%94%E8%AE%B0'], + target: '/topic/笔记', + }, + { + title: '搜索引擎', + source: ['top.aibase.com/topic/%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E'], + target: '/topic/搜索引擎', + }, + { + title: '计算机视觉', + source: ['top.aibase.com/topic/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%A7%86%E8%A7%89'], + target: '/topic/计算机视觉', + }, + { + title: '社区', + source: ['top.aibase.com/topic/%E7%A4%BE%E5%8C%BA'], + target: '/topic/社区', + }, + { + title: '效率', + source: ['top.aibase.com/topic/%E6%95%88%E7%8E%87'], + target: '/topic/效率', + }, + { + title: '知识管理', + source: ['top.aibase.com/topic/%E7%9F%A5%E8%AF%86%E7%AE%A1%E7%90%86'], + target: '/topic/知识管理', + }, + { + title: 'LLM', + source: ['top.aibase.com/topic/LLM'], + target: '/topic/LLM', + }, + { + title: '智能聊天', + source: ['top.aibase.com/topic/%E6%99%BA%E8%83%BD%E8%81%8A%E5%A4%A9'], + target: '/topic/智能聊天', + }, + { + title: '社交', + source: ['top.aibase.com/topic/%E7%A4%BE%E4%BA%A4'], + target: '/topic/社交', + }, + { + title: '语言学习', + source: ['top.aibase.com/topic/%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0'], + target: '/topic/语言学习', + }, + { + title: '娱乐', + source: ['top.aibase.com/topic/%E5%A8%B1%E4%B9%90'], + target: '/topic/娱乐', + }, + { + title: '简历', + source: ['top.aibase.com/topic/%E7%AE%80%E5%8E%86'], + target: '/topic/简历', + }, + { + title: 'OpenAI', + source: ['top.aibase.com/topic/OpenAI'], + target: '/topic/OpenAI', + }, + { + title: '客户服务', + source: ['top.aibase.com/topic/%E5%AE%A2%E6%88%B7%E6%9C%8D%E5%8A%A1'], + target: '/topic/客户服务', + }, + { + title: '室内设计', + source: ['top.aibase.com/topic/%E5%AE%A4%E5%86%85%E8%AE%BE%E8%AE%A1'], + target: '/topic/室内设计', + }, + ], +}; diff --git a/lib/routes/aibase/util.ts b/lib/routes/aibase/util.ts new file mode 100644 index 00000000000000..066473c8b85d87 --- /dev/null +++ b/lib/routes/aibase/util.ts @@ -0,0 +1,114 @@ +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import ofetch from '@/utils/ofetch'; +import { CheerioAPI } from 'cheerio'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +const defaultSrc = '_static/ee6af7e.js'; +const defaultToken = 'djflkdsoisknfoklsyhownfrlewfknoiaewf'; + +const rootUrl = 'https://top.aibase.com'; +const apiRootUrl = 'https://app.chinaz.com'; + +/** + * Converts a string to an array. + * If the string starts with '[', it is assumed to be a JSON array and is parsed accordingly. + * Otherwise, the string is wrapped in an array. + * + * @param str - The input string to convert to an array. + * @returns An array created from the input string. + */ +const strToArray = (str: string) => { + if (str.startsWith('[')) { + return JSON.parse(str); + } + return [str]; +}; + +art.defaults.imports.strToArray = strToArray; + +/** + * Retrieve a token asynchronously using a CheerioAPI instance. + * @param $ - The CheerioAPI instance. + * @returns A Promise that resolves to a string representing the token. + */ +const getToken = async ($: CheerioAPI): Promise => { + const scriptUrl = new URL($('script[src]').last()?.prop('src') ?? defaultSrc, rootUrl).href; + + const script = await ofetch(scriptUrl, { + responseType: 'text', + }); + + return script.match(/"\/(\w+)\/ai\/.*?\.aspx"/)?.[1] ?? defaultToken; +}; + +/** + * Build API URLs asynchronously using a CheerioAPI instance. + * @param $ - The CheerioAPI instance. + * @returns An object containing API URLs. + */ +const buildApiUrl = async ($: CheerioAPI) => { + const token = await getToken($); + + const apiRecommListUrl = new URL(`${token}/ai/GetAIProcRecommList.aspx`, apiRootUrl).href; + const apiRecommProcUrl = new URL(`${token}/ai/GetAIProcListByRecomm.aspx`, apiRootUrl).href; + const apiTagProcUrl = new URL(`${token}/ai/GetAiProductOfTag.aspx`, apiRootUrl).href; + // AI 资讯列表 + const apiInfoListUrl = new URL(`${token}/ai/GetAiInfoList.aspx`, apiRootUrl).href; + + return { + apiRecommListUrl, + apiRecommProcUrl, + apiTagProcUrl, + apiInfoListUrl, + }; +}; + +/** + * Process an array of items to generate a new array of processed items for RSS. + * @param items - An array of items to process. + * @returns An array of processed items. + */ +const processItems = (items: any[]): any[] => + items.map((item) => { + const title = item.name; + const image = item.imgurl; + const description = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + item, + }); + const guid = `aibase-${item.zurl}`; + + return { + title, + description, + pubDate: timezone(parseDate(item.addtime), +8), + link: new URL(`tool/${item.zurl}`, rootUrl).href, + category: [...new Set([...strToArray(item.categories), ...strToArray(item.tags), item.catname, item.procattrname, item.procformname, item.proctypename])].filter(Boolean), + guid, + id: guid, + content: { + html: description, + text: item.desc, + }, + image, + banner: image, + updated: parseDate(item.UpdTime), + enclosure_url: item.logo, + enclosure_type: item.logo ? `image/${item.logo.split(/\./).pop()}` : undefined, + enclosure_title: title, + }; + }); + +export { rootUrl, processItems, buildApiUrl }; diff --git a/lib/routes/aicaijing/namespace.ts b/lib/routes/aicaijing/namespace.ts index f338f73bd71684..d50e13febc390e 100644 --- a/lib/routes/aicaijing/namespace.ts +++ b/lib/routes/aicaijing/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AI 财经社', url: 'www.aicaijing.com', + lang: 'zh-CN', }; diff --git a/lib/routes/aiea/index.ts b/lib/routes/aiea/index.ts index 687c75a7e9614f..0d4a110969f912 100644 --- a/lib/routes/aiea/index.ts +++ b/lib/routes/aiea/index.ts @@ -18,10 +18,10 @@ export const route: Route = { maintainers: ['zxx-457'], handler, description: `| Time frame | - | ---------- | - | upcoming | - | past | - | both |`, +| ---------- | +| upcoming | +| past | +| both |`, }; async function handler(ctx) { diff --git a/lib/routes/aiea/namespace.ts b/lib/routes/aiea/namespace.ts index affb227c142339..9b1dbcfd416ca6 100644 --- a/lib/routes/aiea/namespace.ts +++ b/lib/routes/aiea/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Asian Innovation and Entrepreneurship Association', url: 'www.aiea.org', + lang: 'en', }; diff --git a/lib/routes/aijishu/index.ts b/lib/routes/aijishu/index.ts index bb2f261105f2a9..522ad7d888e949 100644 --- a/lib/routes/aijishu/index.ts +++ b/lib/routes/aijishu/index.ts @@ -20,10 +20,10 @@ export const route: Route = { maintainers: [], handler, description: `| type | 说明 | - | ------- | ---- | - | channel | 频道 | - | blog | 专栏 | - | u | 用户 |`, +| ------- | ---- | +| channel | 频道 | +| blog | 专栏 | +| u | 用户 |`, }; async function handler(ctx) { diff --git a/lib/routes/aijishu/namespace.ts b/lib/routes/aijishu/namespace.ts index 971883a85ec334..02ed7d23ce725e 100644 --- a/lib/routes/aijishu/namespace.ts +++ b/lib/routes/aijishu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '极术社区', url: 'www.aijishu', + lang: 'zh-CN', }; diff --git a/lib/routes/ainvest/namespace.ts b/lib/routes/ainvest/namespace.ts index e220878f4e2351..e6985b1cfbabce 100644 --- a/lib/routes/ainvest/namespace.ts +++ b/lib/routes/ainvest/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AInvest', url: 'ainvest.com', + lang: 'en', }; diff --git a/lib/routes/ainvest/news.ts b/lib/routes/ainvest/news.ts index cc23dc3b96272e..ed384c9e011300 100644 --- a/lib/routes/ainvest/news.ts +++ b/lib/routes/ainvest/news.ts @@ -1,13 +1,14 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; import { getHeaders, randomString, decryptAES } from './utils'; export const route: Route = { path: '/news', - categories: ['finance'], + categories: ['finance', 'popular'], example: '/ainvest/news', parameters: {}, + view: ViewType.Articles, features: { requireConfig: false, requirePuppeteer: false, diff --git a/lib/routes/aip/journal-pupp.ts b/lib/routes/aip/journal-pupp.ts index 85779d01e874cb..32f73e72bfb04f 100644 --- a/lib/routes/aip/journal-pupp.ts +++ b/lib/routes/aip/journal-pupp.ts @@ -50,7 +50,7 @@ const handler = async (ctx) => { false ); - browser.close(); + await browser.close(); return { title: jrnlName, diff --git a/lib/routes/aip/journal.ts b/lib/routes/aip/journal.ts index a5e829000b2750..688ebc7253fb38 100644 --- a/lib/routes/aip/journal.ts +++ b/lib/routes/aip/journal.ts @@ -26,9 +26,9 @@ export const route: Route = { handler, description: `Refer to the URL format \`pubs.aip.org/:pub/:jrn\` - :::tip +::: tip More jounals can be found in [AIP Publications](https://publishing.aip.org/publications/find-the-right-journal). - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/aip/namespace.ts b/lib/routes/aip/namespace.ts index df63b426968fc4..cd7f9ca97013a1 100644 --- a/lib/routes/aip/namespace.ts +++ b/lib/routes/aip/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'American Institute of Physics', url: 'pubs.aip.org', + lang: 'en', }; diff --git a/lib/routes/air-level/index.ts b/lib/routes/air-level/index.ts new file mode 100644 index 00000000000000..93ba54123b0849 --- /dev/null +++ b/lib/routes/air-level/index.ts @@ -0,0 +1,49 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 +import { load } from 'cheerio'; // 类似 jQuery 的 API HTML 解析器 + +export const route: Route = { + path: '/air/:area', + radar: [ + { + source: ['m.air-level.com/air/:area/'], + target: '/air/:area', + }, + ], + parameters: { + area: '地区', + }, + name: '空气质量', + maintainers: ['lifetraveler'], + example: '/air-level/air/xian', + handler, +}; + +async function handler(ctx) { + const area = ctx.req.param('area'); + const currentUrl = `https://m.air-level.com/air/${area}`; + const response = await ofetch(currentUrl); + const $ = load(response); + + const title = $('body > div.container > div.row.page > div:nth-child(1) > h2').text().replaceAll('[]', ''); + + const table = $('body > div.container > div.row.page > div:nth-child(1) > div:nth-child(3) > table'); + + const qt = $('body > div.container > div.row.page > div:nth-child(1) > div.aqi-dv > div > span.aqi-bg.aqi-level-2').text(); + const pubtime = $('body > div.container > div.row.page > div:nth-child(1) > div.aqi-dv > div > span.label.label-info').text(); + + const items = [ + { + title: title + ' ' + qt + ' ' + pubtime, + link: currentUrl, + description: `${table.html()}
    `, + guid: pubtime, + }, + ]; + return { + title, + item: items, + description: '订阅每个城市的天气质量', + link: currentUrl, + }; +} diff --git a/lib/routes/air-level/levelrank.ts b/lib/routes/air-level/levelrank.ts new file mode 100644 index 00000000000000..31136891aefab8 --- /dev/null +++ b/lib/routes/air-level/levelrank.ts @@ -0,0 +1,65 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 +import { load } from 'cheerio'; // 类似 jQuery 的 API HTML 解析器 + +export const route: Route = { + path: ['/rank/:status?'], + radar: [ + { + source: ['m.air-level.com/rank/:status', 'm.air-level.com/rank'], + target: '/rank/:status', + }, + ], + parameters: { + status: '地区', + }, + name: '空气质量排行', + maintainers: ['lifetraveler'], + example: '/air-level/rank/best,/air-level/rank', + handler, +}; + +async function handler(ctx) { + const status = ctx.req.param('status'); + const currentUrl = 'https://m.air-level.com/rank'; + const response = await ofetch(currentUrl); + const $ = load(response); + let table = ''; + let title = ''; + + const titleBest = $('body > div.container > div.row.page > div:nth-child(1) > div:nth-child(5) > h3').text().replaceAll('[]', ''); + const tableBest = $('body > div.container > div.row.page > div:nth-child(1) > div:nth-child(5) > table').html(); + const titleWorst = $('body > div.container > div.row.page > div:nth-child(1) > div:nth-child(3) > h3').text().replaceAll('[]', ''); + const tableWorst = $('body > div.container > div.row.page > div:nth-child(1) > div:nth-child(3) > table').html(); + + if (status) { + if (status === 'best') { + title = titleBest; + table = `${tableBest}
    `; + } + + if (status === 'worsest') { + title = titleWorst; + table = `${tableWorst}
    `; + } + } else { + title = $('body > div.container > div.row.page > div:nth-child(1) > h2').text().replaceAll('[]', ''); + table = `${titleBest}
    ${tableBest}

    ${titleWorst}
    ${tableWorst}
    `; + } + + const pubtime = $('body > div.container > div.row.page > div:nth-child(1) > h4').text(); + const items = [ + { + title, + link: currentUrl, + description: table, + guid: pubtime, + }, + ]; + return { + title, + item: items, + description: '空气质量排行', + link: currentUrl, + }; +} diff --git a/lib/routes/air-level/namespace.ts b/lib/routes/air-level/namespace.ts new file mode 100644 index 00000000000000..d85046bbd87472 --- /dev/null +++ b/lib/routes/air-level/namespace.ts @@ -0,0 +1,12 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Air-Level', + url: 'air-level.com', + description: ` + - 可以订阅每个城市的空气质量,按照拼音订阅 + - 支持订阅每天的实时排名 + `, + categories: ['forecast'], + lang: 'zh-CN', +}; diff --git a/lib/routes/airchina/namespace.ts b/lib/routes/airchina/namespace.ts index 37331fbb26b846..1941def61fce23 100644 --- a/lib/routes/airchina/namespace.ts +++ b/lib/routes/airchina/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国国际航空公司', url: 'www.airchina.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/aisixiang/namespace.ts b/lib/routes/aisixiang/namespace.ts index c74ac996551162..aa1811ab78ec58 100644 --- a/lib/routes/aisixiang/namespace.ts +++ b/lib/routes/aisixiang/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '爱思想', url: 'aisixiang.com', + lang: 'zh-CN', }; diff --git a/lib/routes/aisixiang/thinktank.ts b/lib/routes/aisixiang/thinktank.ts index 12239370e75d55..0d618a8403f536 100644 --- a/lib/routes/aisixiang/thinktank.ts +++ b/lib/routes/aisixiang/thinktank.ts @@ -23,7 +23,7 @@ export const route: Route = { maintainers: ['hoilc', 'nczitzk'], handler, description: `| 论文 | 时评 | 随笔 | 演讲 | 访谈 | 著作 | 读书 | 史论 | 译作 | 诗歌 | 书信 | 科学 | - | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |`, +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |`, }; async function handler(ctx) { diff --git a/lib/routes/aisixiang/toplist.ts b/lib/routes/aisixiang/toplist.ts index 77b123db889a76..0e9e2990e92b2c 100644 --- a/lib/routes/aisixiang/toplist.ts +++ b/lib/routes/aisixiang/toplist.ts @@ -12,8 +12,8 @@ export const route: Route = { maintainers: ['HenryQW', 'nczitzk'], handler, description: `| 文章点击排行 | 最近更新文章 | 文章推荐排行 | - | ------------ | ------------ | ------------ | - | 1 | 10 | 11 |`, +| ------------ | ------------ | ------------ | +| 1 | 10 | 11 |`, }; async function handler(ctx) { diff --git a/lib/routes/aisixiang/zhuanti.ts b/lib/routes/aisixiang/zhuanti.ts index 4dd4c21cb64147..4699f2b8c99e3a 100644 --- a/lib/routes/aisixiang/zhuanti.ts +++ b/lib/routes/aisixiang/zhuanti.ts @@ -23,9 +23,9 @@ export const route: Route = { name: '专题', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 更多专题请见 [关键词](http://www.aisixiang.com/zhuanti/) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/ajcass/namespace.ts b/lib/routes/ajcass/namespace.ts new file mode 100644 index 00000000000000..f471eed914ee46 --- /dev/null +++ b/lib/routes/ajcass/namespace.ts @@ -0,0 +1,11 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '社科期刊网', + url: 'ajcass.com', + description: '中国社会科学院学术期刊方阵', + lang: 'zh-CN', + zh: { + name: '社科期刊网', + }, +}; diff --git a/lib/routes/ajcass/shxyj.ts b/lib/routes/ajcass/shxyj.ts new file mode 100644 index 00000000000000..e4324927d16a2b --- /dev/null +++ b/lib/routes/ajcass/shxyj.ts @@ -0,0 +1,73 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/shxyj/:year?/:issue?', + categories: ['journal'], + example: '/ajcass/shxyj/2024/1', + parameters: { year: 'Year of the issue, `null` for the lastest', issue: 'Issue number, `null` for the lastest' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '社会学研究', + maintainers: ['CNYoki'], + handler, +}; + +async function handler(ctx) { + let { year, issue } = ctx.req.param(); + + if (!year) { + const response = await got('https://shxyj.ajcass.com/'); + const $ = load(response.body); + const latestIssueText = $('p.hod.pop').first().text(); + + const match = latestIssueText.match(/(\d{4}) Vol\.(\d+):/); + if (match) { + year = match[1]; + issue = match[2]; + } else { + throw new Error('无法获取最新的 year 和 issue'); + } + } + + const url = `https://shxyj.ajcass.com/Magazine/?Year=${year}&Issue=${issue}`; + const response = await got(url); + const $ = load(response.body); + + const items = $('#tab tr') + .toArray() + .map((item) => { + const $item = $(item); + const articleTitle = $item.find('a').first().text().trim(); + const articleLink = $item.find('a').first().attr('href'); + const summary = $item.find('li').eq(1).text().replace('[摘要]', '').trim(); + const authors = $item.find('li').eq(2).text().replace('作者:', '').trim(); + const pubDate = parseDate(`${year}-${Number.parseInt(issue) * 2}`); + + if (articleTitle && articleLink) { + return { + title: articleTitle, + link: `https://shxyj.ajcass.com${articleLink}`, + description: summary, + author: authors, + pubDate, + }; + } + return null; + }) + .filter((item) => item !== null); + + return { + title: `社会学研究 ${year}年第${issue}期`, + link: url, + item: items, + }; +} diff --git a/lib/routes/ajmide/index.ts b/lib/routes/ajmide/index.ts index 6c24803c7298bc..6f13ece4b23567 100644 --- a/lib/routes/ajmide/index.ts +++ b/lib/routes/ajmide/index.ts @@ -1,10 +1,11 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/:id', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Audios, example: '/ajmide/10603594', parameters: { id: '播客 id,可以从播客页面 URL 中找到' }, features: { diff --git a/lib/routes/ajmide/namespace.ts b/lib/routes/ajmide/namespace.ts index 1ec1cd92126bb2..bfb258b9ce7ed9 100644 --- a/lib/routes/ajmide/namespace.ts +++ b/lib/routes/ajmide/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '阿基米德 FM', url: 'm.ajmide.com', + lang: 'zh-CN', }; diff --git a/lib/routes/ali213/namespace.ts b/lib/routes/ali213/namespace.ts new file mode 100644 index 00000000000000..14cec63452d684 --- /dev/null +++ b/lib/routes/ali213/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '游侠网', + url: 'ali213.net', + categories: ['game'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/ali213/news.ts b/lib/routes/ali213/news.ts new file mode 100644 index 00000000000000..0d0ecba9d92e31 --- /dev/null +++ b/lib/routes/ali213/news.ts @@ -0,0 +1,269 @@ +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'; +import timezone from '@/utils/timezone'; + +const __dirname = getCurrentPath(import.meta.url); + +export const handler = async (ctx: Context): Promise => { + const { category = 'new' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const rootUrl: string = 'https://www.ali213.net'; + const targetUrl: string = new URL(`news/${category.endsWith('/') ? category : `${category}/`}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'zh-CN'; + + let items: DataItem[] = $('div.n_lone') + .slice(0, limit) + .toArray() + .map((item): DataItem => { + const $item: Cheerio = $(item); + + const aEl: Cheerio = $item.find('h2.lone_t a'); + + const title: string = aEl.prop('title') || aEl.text(); + const link: string | undefined = aEl.prop('href'); + + const imageEl: Cheerio = $item.find('img'); + const imageSrc: string | undefined = imageEl?.prop('src'); + const imageAlt: string | undefined = imageEl?.prop('alt'); + + const intro: string = $item.find('div.lone_f_r_t').text(); + + const description: string = art(path.join(__dirname, 'templates/description.art'), { + images: imageEl + ? [ + { + src: imageSrc, + alt: imageAlt, + }, + ] + : undefined, + intro, + }); + + const author: DataItem['author'] = $item.find('div.lone_f_r_f span').last().text().split(/:/).pop(); + + return { + title, + description, + pubDate: parseDate($item.find('div.lone_f_r_f span').first().text()), + link, + author, + content: { + html: description, + text: $item.find('div.lone_f_r_t').text(), + }, + image: imageSrc, + banner: imageSrc, + language, + }; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link && typeof item.link !== 'string') { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + try { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('h1.newstit').text(); + const image: string | undefined = $$('div#Content img').first().prop('src'); + + const mediaContent: Cheerio = $$('div#Content p span img'); + const media: Record> = {}; + + if (mediaContent.length) { + mediaContent.each((_, el) => { + const $$el: Cheerio = $$(el); + + const pEl: Cheerio = $$el.closest('p'); + + const mediaUrl: string | undefined = $$el.prop('src'); + const mediaType: string | undefined = mediaUrl?.split(/\./).pop(); + + if (mediaType && mediaUrl) { + media[mediaType] = { url: mediaUrl }; + + pEl.replaceWith( + art(path.join(__dirname, 'templates/description.art'), { + images: [ + { + src: mediaUrl, + }, + ], + }) + ); + } + }); + } + + const description: string = art(path.join(__dirname, 'templates/description.art'), { + description: $$('div#Content').html() ?? '', + }); + + const extraLinks = $$('div.extend_read ul li a') + .toArray() + .map((el) => { + const $$el: Cheerio = $$(el); + + return { + url: $$el.prop('href'), + type: 'related', + content_html: $$el.prop('title') || $$el.text(), + }; + }) + .filter((_): _ is { url: string; type: string; content_html: string } => true); + + return { + ...item, + title, + description, + pubDate: timezone(parseDate($$('div.newstag_l').text().split(/\s/)[0]), +8), + content: { + html: description, + text: $$('div#Content').html() ?? '', + }, + image, + banner: image, + language, + media: Object.keys(media).length > 0 ? media : undefined, + _extra: { + links: extraLinks.length > 0 ? extraLinks : undefined, + }, + }; + } catch { + return item; + } + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const author = '游侠网'; + const title = $('div.news-list-title').text(); + const feedImage = new URL('news/images/ali213_app_big.png', rootUrl).href; + + return { + title: `${author} - ${title}`, + description: title, + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/news/:category?', + name: '资讯', + url: 'www.ali213.net', + maintainers: ['nczitzk'], + handler, + example: '/ali213/news/new', + parameters: { + category: '分类,默认为 `new`,即最新资讯,可在对应分类页 URL 中找到', + }, + description: `::: tip +若订阅 [游戏资讯](https://www.ali213.net/news/game/),网址为 \`https://www.ali213.net/news/game/\`,请截取 \`https://www.ali213.net/news/\` 到末尾 \`/\` 的部分 \`game\` 作为 \`category\` 参数填入,此时目标路由为 [\`/ali213/news/game\`](https://rsshub.app/ali213/news/game)。 +::: + +| 分类名称 | 分类 ID | +| -------- | ------- | +| 最新资讯 | new | +| 评测 | pingce | +| 游戏 | game | +| 动漫 | comic | +| 影视 | movie | +| 科技 | tech | +| 电竞 | esports | +| 娱乐 | amuse | +| 手游 | mobile | +`, + categories: ['game'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.ali213.net/news/:category'], + target: (params) => { + const category = params.category; + + return `/news/${category ? `/${category}` : ''}`; + }, + }, + { + title: '最新资讯', + source: ['www.ali213.net/news/new'], + target: '/news/new', + }, + { + title: '评测', + source: ['www.ali213.net/news/pingce'], + target: '/news/pingce', + }, + { + title: '游戏', + source: ['www.ali213.net/news/game'], + target: '/news/game', + }, + { + title: '动漫', + source: ['www.ali213.net/news/comic'], + target: '/news/comic', + }, + { + title: '影视', + source: ['www.ali213.net/news/movie'], + target: '/news/movie', + }, + { + title: '科技', + source: ['www.ali213.net/news/tech'], + target: '/news/tech', + }, + { + title: '电竞', + source: ['www.ali213.net/news/esports'], + target: '/news/esports', + }, + { + title: '娱乐', + source: ['www.ali213.net/news/amuse'], + target: '/news/amuse', + }, + { + title: '手游', + source: ['www.ali213.net/news/mobile'], + target: '/news/mobile', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/ali213/templates/description.art b/lib/routes/ali213/templates/description.art new file mode 100644 index 00000000000000..249654e7e618a4 --- /dev/null +++ b/lib/routes/ali213/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
    + {{ image.alt }} +
    + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} +
    {{ 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 => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('h1.newstit').text(); + + let description: string = $$('div#Content').html() ?? ''; + + const pageLinks: string[] = []; + $$('a.currpage') + .parent() + .find('a:not(.currpage)') + .each((_, el) => { + const href = $$(el).attr('href'); + if (href) { + pageLinks.push(href); + } + }); + + const pageContents = await Promise.all( + pageLinks.map(async (link) => { + const response = await ofetch(new URL(link, item.link).href); + const $$$: CheerioAPI = load(response); + + return $$$('div#Content').html() ?? ''; + }) + ); + + description += pageContents.join(''); + + description = art(path.join(__dirname, 'templates/description.art'), { + description, + }); + + const extraLinks = $$('div.extend_read a') + .toArray() + .map((el) => { + const $$el: Cheerio = $$(el); + + return { + url: $$el.prop('href'), + type: 'related', + content_html: $$el.text(), + }; + }) + .filter((_): _ is { url: string; type: string; content_html: string } => true); + + return { + title, + description, + pubDate: item.pubDate, + category: $$('.category') + .toArray() + .map((c) => $$(c).text()), + author: item.author, + doi: $$('meta[name="citation_doi"]').prop('content') || undefined, + guid: item.guid, + id: item.guid, + content: { + html: description, + text: description, + }, + image: item.image, + banner: item.image, + language, + _extra: { + links: extraLinks.length > 0 ? extraLinks : undefined, + }, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const title: string = $('title').text(); + const feedImage: string = new URL('news/images/dxhlogo.png', rootUrl).href; + + return { + title, + description: $('meta[name="description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author: title.split(/_/).pop(), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/zl/:category?', + name: '大侠号', + url: 'www.ali213.net', + maintainers: ['nczitzk'], + handler, + example: '/ali213/zl', + parameters: { + category: '分类,默认为首页,可在对应分类页 URL 中找到', + }, + description: `::: tip +若订阅 [游戏](https://www.ali213.net/news/zl/game/),网址为 \`https://www.ali213.net/news/zl/game/\`,请截取 \`https://www.ali213.net/news/zl/\` 到末尾 \`/\` 的部分 \`game\` 作为 \`category\` 参数填入,此时目标路由为 [\`/ali213/zl/game\`](https://rsshub.app/ali213/zl/game)。 +::: + +| 首页 | 游戏 | 动漫 | 影视 | 娱乐 | +| ---------------------------------------- | -------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | +| [index](https://www.ali213.net/news/zl/) | [game](https://www.ali213.net/news/zl/game/) | [comic](https://www.ali213.net/news/zl/comic/) | [movie](https://www.ali213.net/news/zl/movie/) | [amuse](https://www.ali213.net/news/zl/amuse/) | +`, + categories: ['game'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.ali213.net/news/zl/:category'], + target: (params) => { + const category = params.category; + + return `/ali213/zl${category ? `/${category}` : ''}`; + }, + }, + { + title: '首页', + source: ['www.ali213.net/news/zl/'], + target: '/zl', + }, + { + title: '游戏', + source: ['www.ali213.net/news/zl/game/'], + target: '/zl/game', + }, + { + title: '动漫', + source: ['www.ali213.net/news/zl/comic/'], + target: '/zl/comic', + }, + { + title: '影视', + source: ['www.ali213.net/news/zl/movie/'], + target: '/zl/movie', + }, + { + title: '娱乐', + source: ['www.ali213.net/news/zl/amuse/'], + target: '/zl/amuse', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/alicesoft/infomation.ts b/lib/routes/alicesoft/infomation.ts new file mode 100644 index 00000000000000..dc855add300248 --- /dev/null +++ b/lib/routes/alicesoft/infomation.ts @@ -0,0 +1,88 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; + +const baseUrl = 'https://www.alicesoft.com'; + +export const route: Route = { + url: 'www.alicesoft.com/information', + path: '/information/:category?/:game?', + categories: ['game'], + example: '/alicesoft/information/game/cat377', + parameters: { + category: 'Category in the URL, which can be accessed under カテゴリ一覧 on the website.', + game: 'Game-specific subcategory in the URL, which can be accessed under カテゴリ一覧 on the website. In this case, the category value should be `game`.', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.alicesoft.com/information', 'www.alicesoft.com/information/:category', 'www.alicesoft.com/information/:category/:game'], + target: '/information/:category/:game', + }, + ], + name: 'ニュース', + maintainers: ['keocheung'], + handler, +}; + +async function handler(ctx) { + const { category, game } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + + let url = `${baseUrl}/information`; + if (category) { + url += `/${category}`; + if (game) { + url += `/${game}`; + } + } + + const response = await got(url); + const $ = load(response.data); + + let items = $('div.cont-main li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + return { + title: item.find('p.txt').text(), + link: item.find('a').attr('href'), + pubDate: new Date(item.find('time').attr('datetime')), + }; + }); + + items = await Promise.all( + items.map((item) => { + if (!item.link.startsWith(`${baseUrl}/information/`)) { + return item; + } + return cache.tryGet(item.link, async () => { + const contentResponse = await got(item.link); + + const content = load(contentResponse.data); + content('iframe[src^="https://www.youtube.com/"]').removeAttr('height').removeAttr('width'); + item.description = `
    ${content('div.article-detail') + .html() + ?.replaceAll(/

    (.+?)<\/p>/g, '

    $1

    ') + ?.replaceAll(/

    (.+?)<\/p>/g, '

    $1

    ')}
    `; + return item; + }); + }) + ); + + return { + title: 'ALICESOFT ' + $('article h2').clone().children().remove().end().text(), + link: url, + item: items, + language: 'ja', + }; +} diff --git a/lib/routes/alicesoft/namespace.ts b/lib/routes/alicesoft/namespace.ts new file mode 100644 index 00000000000000..ca9904aa76a3ad --- /dev/null +++ b/lib/routes/alicesoft/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'ALICESOFT', + url: 'www.alicesoft.com', + lang: 'ja', +}; diff --git a/lib/routes/alipan/files.ts b/lib/routes/alipan/files.ts new file mode 100644 index 00000000000000..ddb37302860633 --- /dev/null +++ b/lib/routes/alipan/files.ts @@ -0,0 +1,80 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { AnonymousShareInfo, ShareList, TokenResponse } from './types'; + +export const route: Route = { + path: '/files/:share_id/:parent_file_id?', + example: '/alipan/files/jjtKEgXJAtC/64a957744876479ab17941b29d1289c6ebdd71ef', + parameters: { share_id: '分享 id,可以从分享页面 URL 中找到', parent_file_id: '文件夹 id,可以从文件夹页面 URL 中找到' }, + radar: [ + { + source: ['www.alipan.com/s/:share_id/folder/:parent_file_id', 'www.alipan.com/s/:share_id'], + }, + ], + name: '文件列表', + maintainers: ['DIYgod'], + handler, + url: 'www.alipan.com/s', +}; + +async function handler(ctx) { + const { share_id, parent_file_id } = ctx.req.param(); + const url = `https://www.aliyundrive.com/s/${share_id}${parent_file_id ? `/folder/${parent_file_id}` : ''}`; + + const headers = { + referer: 'https://www.aliyundrive.com/', + origin: 'https://www.aliyundrive.com', + 'x-canary': 'client=web,app=share,version=v2.3.1', + }; + + const shareRes = await ofetch('https://api.aliyundrive.com/adrive/v3/share_link/get_share_by_anonymous', { + method: 'POST', + headers, + query: { + share_id, + }, + body: { + share_id, + }, + }); + + const tokenRes = await ofetch('https://api.aliyundrive.com/v2/share_link/get_share_token', { + method: 'POST', + headers, + body: { + share_id, + }, + }); + const shareToken = tokenRes.share_token; + + const listRes = await ofetch('https://api.aliyundrive.com/adrive/v2/file/list_by_share', { + method: 'POST', + headers: { + ...headers, + 'x-share-token': shareToken, + }, + body: { + limit: 100, + order_by: 'created_at', + order_direction: 'DESC', + parent_file_id: parent_file_id || 'root', + share_id, + }, + }); + + const result = listRes.items.map((item) => ({ + title: item.name, + description: item.name + (item.thumbnail ? `` : ''), + link: url, + pubDate: parseDate(item.created_at), + updated: parseDate(item.updated_at), + guid: item.file_id, + })); + + return { + title: `${shareRes.display_name || `${share_id}${parent_file_id ? `-${parent_file_id}` : ''}`}-阿里云盘`, + link: url, + item: result, + }; +} diff --git a/lib/routes/alipan/namespace.ts b/lib/routes/alipan/namespace.ts new file mode 100644 index 00000000000000..91c32c55173649 --- /dev/null +++ b/lib/routes/alipan/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '阿里云盘', + url: 'www.alipan.com', + categories: ['multimedia'], + lang: 'zh-CN', +}; diff --git a/lib/routes/alipan/types.ts b/lib/routes/alipan/types.ts new file mode 100644 index 00000000000000..a784c58797426d --- /dev/null +++ b/lib/routes/alipan/types.ts @@ -0,0 +1,62 @@ +interface FileInfo { + type: string; + file_id: string; + file_name: string; +} + +interface SaveButton { + text: string; + select_all_text: string; +} + +export interface AnonymousShareInfo { + file_count: number; + share_name: string; + file_infos: FileInfo[]; + creator_phone: string; + avatar: string; + display_name: string; + save_button: SaveButton; + updated_at: string; + share_title: string; + has_pwd: boolean; + creator_id: string; + creator_name: string; + expiration: string; + vip: string; +} + +export interface TokenResponse { + expire_time: string; + expires_in: number; + share_token: string; +} + +interface ImageMediaMetadata { + exif: string; +} + +interface FileDetail { + drive_id: string; + domain_id: string; + file_id: string; + share_id: string; + name: string; + type: string; + created_at: string; + updated_at: string; + file_extension: string; + mime_type: string; + mime_extension: string; + size: number; + parent_file_id: string; + thumbnail: string; + category: string; + image_media_metadata: ImageMediaMetadata; + punish_flag: number; +} + +export interface ShareList { + items: FileDetail[]; + next_marker: string; +} diff --git a/lib/routes/aliresearch/information.ts b/lib/routes/aliresearch/information.ts index 2ee2597b067e2e..ba7654cd048a3f 100644 --- a/lib/routes/aliresearch/information.ts +++ b/lib/routes/aliresearch/information.ts @@ -6,7 +6,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/information/:type?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/aliresearch/information', parameters: { type: '类型,见下表,默认为新闻' }, features: { @@ -28,7 +28,7 @@ export const route: Route = { handler, url: 'aliresearch.com/cn/information', description: `| 新闻 | 观点 | 案例 | - | ---- | ---- | ---- |`, +| ---- | ---- | ---- |`, }; async function handler(ctx) { diff --git a/lib/routes/aliresearch/namespace.ts b/lib/routes/aliresearch/namespace.ts index 94b92cc69233be..249c83f7dda055 100644 --- a/lib/routes/aliresearch/namespace.ts +++ b/lib/routes/aliresearch/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '阿里研究院', url: 'aliresearch.com', + lang: 'zh-CN', }; diff --git a/lib/routes/alistapart/index.ts b/lib/routes/alistapart/index.ts index b91a9e48f6fa85..c603423017349f 100644 --- a/lib/routes/alistapart/index.ts +++ b/lib/routes/alistapart/index.ts @@ -3,16 +3,18 @@ import { getData, getList } from './utils'; export const route: Route = { path: '/', + categories: ['programming'], radar: [ { source: ['alistapart.com/articles/'], - target: '', + target: '/', }, ], - name: 'Unknown', + name: 'Home Feed', maintainers: ['Rjnishant530'], handler, url: 'alistapart.com/articles/', + example: '/alistapart', }; async function handler() { diff --git a/lib/routes/alistapart/namespace.ts b/lib/routes/alistapart/namespace.ts index 99712be4542079..6d8f2730e318c4 100644 --- a/lib/routes/alistapart/namespace.ts +++ b/lib/routes/alistapart/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'A List Apart', url: 'alistapart.com', + lang: 'en', }; diff --git a/lib/routes/alistapart/topic.ts b/lib/routes/alistapart/topic.ts index c5c24d74ec41ed..5dbe0d1e7dec6d 100644 --- a/lib/routes/alistapart/topic.ts +++ b/lib/routes/alistapart/topic.ts @@ -17,6 +17,7 @@ export const route: Route = { radar: [ { source: ['alistapart.com/blog/topic/:topic'], + target: '/:topic', }, ], name: 'Topics', @@ -25,51 +26,51 @@ export const route: Route = { url: 'alistapart.com/articles/', description: `You have the option to utilize the main heading or use individual categories as topics for the path. - | **Code** | *code* | - | --------------------------- | ------------------------- | - | **Application Development** | *application-development* | - | **Browsers** | *browsers* | - | **CSS** | *css* | - | **HTML** | *html* | - | **JavaScript** | *javascript* | - | **The Server Side** | *the-server-side* | +| **Code** | *code* | +| --------------------------- | ------------------------- | +| **Application Development** | *application-development* | +| **Browsers** | *browsers* | +| **CSS** | *css* | +| **HTML** | *html* | +| **JavaScript** | *javascript* | +| **The Server Side** | *the-server-side* | - | **Content** | *content* | - | -------------------- | ------------------ | - | **Community** | *community* | - | **Content Strategy** | *content-strategy* | - | **Writing** | *writing* | +| **Content** | *content* | +| -------------------- | ------------------ | +| **Community** | *community* | +| **Content Strategy** | *content-strategy* | +| **Writing** | *writing* | - | **Design** | *design* | - | -------------------------- | ---------------------- | - | **Brand Identity** | *brand-identity* | - | **Graphic Design** | *graphic-design* | - | **Layout & Grids** | *layout-grids* | - | **Mobile/Multidevice** | *mobile-multidevice* | - | **Responsive Design** | *responsive-design* | - | **Typography & Web Fonts** | *typography-web-fonts* | +| **Design** | *design* | +| -------------------------- | ---------------------- | +| **Brand Identity** | *brand-identity* | +| **Graphic Design** | *graphic-design* | +| **Layout & Grids** | *layout-grids* | +| **Mobile/Multidevice** | *mobile-multidevice* | +| **Responsive Design** | *responsive-design* | +| **Typography & Web Fonts** | *typography-web-fonts* | - | **Industry & Business** | *industry-business* | - | ----------------------- | ------------------- | - | **Business** | *business* | - | **Career** | *career* | - | **Industry** | *industry* | - | **State of the Web** | *state-of-the-web* | +| **Industry & Business** | *industry-business* | +| ----------------------- | ------------------- | +| **Business** | *business* | +| **Career** | *career* | +| **Industry** | *industry* | +| **State of the Web** | *state-of-the-web* | - | **Process** | *process* | - | ---------------------- | -------------------- | - | **Creativity** | *creativity* | - | **Project Management** | *project-management* | - | **Web Strategy** | *web-strategy* | - | **Workflow & Tools** | *workflow-tools* | +| **Process** | *process* | +| ---------------------- | -------------------- | +| **Creativity** | *creativity* | +| **Project Management** | *project-management* | +| **Web Strategy** | *web-strategy* | +| **Workflow & Tools** | *workflow-tools* | - | **User Experience** | *user-experience* | - | ---------------------------- | -------------------------- | - | **Accessibility** | *accessibility* | - | **Information Architecture** | *information-architecture* | - | **Interaction Design** | *interaction-design* | - | **Usability** | *usability* | - | **User Research** | *user-research* |`, +| **User Experience** | *user-experience* | +| ---------------------------- | -------------------------- | +| **Accessibility** | *accessibility* | +| **Information Architecture** | *information-architecture* | +| **Interaction Design** | *interaction-design* | +| **Usability** | *usability* | +| **User Research** | *user-research* |`, }; async function handler(ctx) { diff --git a/lib/routes/alistapart/utils.ts b/lib/routes/alistapart/utils.ts index c4a4a29257af51..2d426e89b03ebf 100644 --- a/lib/routes/alistapart/utils.ts +++ b/lib/routes/alistapart/utils.ts @@ -1,8 +1,8 @@ -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; -const getData = (url) => got.get(url).json(); +const getData = (url) => ofetch(url); const getList = (data) => data.map((value) => { diff --git a/lib/routes/aliyun/namespace.ts b/lib/routes/aliyun/namespace.ts index 07680173282b2c..64771ea7a95dca 100644 --- a/lib/routes/aliyun/namespace.ts +++ b/lib/routes/aliyun/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '阿里云', url: 'developer.aliyun.com', + lang: 'zh-CN', }; diff --git a/lib/routes/aliyun/notice.ts b/lib/routes/aliyun/notice.ts index d897de5b18126c..a7f62130a569aa 100644 --- a/lib/routes/aliyun/notice.ts +++ b/lib/routes/aliyun/notice.ts @@ -34,12 +34,12 @@ export const route: Route = { maintainers: ['muzea'], handler, description: `| 类型 | type | - | -------- | ---- | - | 全部 | | - | 升级公告 | 1 | - | 安全公告 | 2 | - | 备案公告 | 3 | - | 其他 | 4 |`, +| -------- | ---- | +| 全部 | | +| 升级公告 | 1 | +| 安全公告 | 2 | +| 备案公告 | 3 | +| 其他 | 4 |`, }; async function handler(ctx) { diff --git a/lib/routes/aljazeera/index.ts b/lib/routes/aljazeera/index.ts index 064a9dc2101728..35c6304077b9be 100644 --- a/lib/routes/aljazeera/index.ts +++ b/lib/routes/aljazeera/index.ts @@ -7,7 +7,7 @@ import cache from '@/utils/cache'; import { load } from 'cheerio'; import { art } from '@/utils/render'; import path from 'node:path'; -import { ofetch } from 'ofetch'; +import ofetch from '@/utils/ofetch'; const languages = { arabic: { @@ -27,7 +27,7 @@ const languages = { export const route: Route = { path: '*', name: 'Unknown', - maintainers: [], + maintainers: ['nczitzk'], handler, }; diff --git a/lib/routes/aljazeera/namespace.ts b/lib/routes/aljazeera/namespace.ts index 22d73564b0a49a..097358395a4cae 100644 --- a/lib/routes/aljazeera/namespace.ts +++ b/lib/routes/aljazeera/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Aljazeera 半岛电视台', + name: 'Aljazeera', url: 'aljazeera.com', + lang: 'en', }; diff --git a/lib/routes/ally/namespace.ts b/lib/routes/ally/namespace.ts index f69681f84a78cf..888ca1c2611130 100644 --- a/lib/routes/ally/namespace.ts +++ b/lib/routes/ally/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '艾莱资讯', url: 'rail.ally.net.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ally/rail.ts b/lib/routes/ally/rail.ts index dcba0072c07bc9..8927ab8f48019f 100644 --- a/lib/routes/ally/rail.ts +++ b/lib/routes/ally/rail.ts @@ -27,9 +27,9 @@ export const route: Route = { maintainers: ['Rongronggg9'], handler, url: 'rail.ally.net.cn/', - description: `:::tip + description: `::: tip 默认抓取前 20 条,可通过 \`?limit=\` 改变。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/alpinelinux/namespace.ts b/lib/routes/alpinelinux/namespace.ts new file mode 100644 index 00000000000000..be620e436b7586 --- /dev/null +++ b/lib/routes/alpinelinux/namespace.ts @@ -0,0 +1,12 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Alpine Linux', + url: 'alpinelinux.org', + description: 'Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.', + zh: { + name: 'Alpine Linux', + description: 'Alpine Linux 是一个基于 musl libc 和 busybox 的面向安全的轻量级 Linux 发行版。', + }, + lang: 'en', +}; diff --git a/lib/routes/alpinelinux/pkgs.ts b/lib/routes/alpinelinux/pkgs.ts new file mode 100644 index 00000000000000..ea0c180d205437 --- /dev/null +++ b/lib/routes/alpinelinux/pkgs.ts @@ -0,0 +1,104 @@ +import { Data, Route } from '@/types'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import { Context } from 'hono'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; +import { config } from '@/config'; + +export const route: Route = { + name: 'Packages', + categories: ['program-update'], + maintainers: ['CaoMeiYouRen'], + path: '/pkgs/:name/:routeParams?', + parameters: { name: 'Packages name', routeParams: 'Filters of packages type. E.g. branch=edge&repo=main&arch=armv7&maintainer=Jakub%20Jirutka' }, + example: '/alpinelinux/pkgs/nodejs', + description: `Alpine Linux packages update`, + handler, + radar: [ + { + source: ['https://pkgs.alpinelinux.org/packages'], + target: (params, url) => { + const searchParams = new URL(url).searchParams; + const name = searchParams.get('name'); + searchParams.delete('name'); + const routeParams = searchParams.toString(); + return `/alpinelinux/pkgs/${name}/${routeParams}`; + }, + }, + ], + zh: { + name: '软件包', + description: 'Alpine Linux 软件包更新', + }, +}; + +type RowData = { + package: string; + packageUrl?: string; + version: string; + description?: string; + project?: string; + license: string; + branch: string; + repository: string; + architecture: string; + maintainer: string; + buildDate: string; +}; + +function parseTableToJSON(tableHTML: string) { + const $ = load(tableHTML); + const data: RowData[] = $('tbody tr') + .toArray() + .map((row) => ({ + package: $(row).find('.package a').text().trim(), + packageUrl: $(row).find('.package a').attr('href')?.trim(), + description: $(row).find('.package a').attr('aria-label')?.trim(), + version: $(row).find('.version').text().trim(), + project: $(row).find('.url a').attr('href')?.trim(), + license: $(row).find('.license').text().trim(), + branch: $(row).find('.branch').text().trim(), + repository: $(row).find('.repo a').text().trim(), + architecture: $(row).find('.arch a').text().trim(), + maintainer: $(row).find('.maintainer a').text().trim(), + buildDate: $(row).find('.bdate').text().trim(), + })); + + return data; +} + +async function handler(ctx: Context): Promise { + const { name, routeParams } = ctx.req.param(); + const query = new URLSearchParams(routeParams); + query.append('name', name); + const link = `https://pkgs.alpinelinux.org/packages?${query.toString()}`; + const key = `alpinelinux:packages:${query.toString()}`; + const rowData = (await cache.tryGet( + key, + async () => { + const response = await got({ + url: link, + }); + const html = response.data; + return parseTableToJSON(html); + }, + config.cache.routeExpire, + false + )) as RowData[]; + + const items = rowData.map((e) => ({ + title: `${e.package}@${e.version}/${e.architecture}`, + description: `Version: ${e.version}
    Project: ${e.project}
    Description: ${e.description}
    License: ${e.license}
    Branch: ${e.branch}
    Repository: ${e.repository}
    Maintainer: ${e.maintainer}
    Build Date: ${e.buildDate}`, + link: `https://pkgs.alpinelinux.org${e.packageUrl}`, + guid: `https://pkgs.alpinelinux.org${e.packageUrl}#${e.version}`, + author: e.maintainer, + pubDate: parseDate(e.buildDate), + })); + return { + title: `${name} - Alpine Linux packages`, + link, + description: 'Alpine Linux packages update', + item: items, + }; +} diff --git a/lib/routes/alternativeto/namespace.ts b/lib/routes/alternativeto/namespace.ts index 973d11869cac35..6f24e15533beac 100644 --- a/lib/routes/alternativeto/namespace.ts +++ b/lib/routes/alternativeto/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AlternativeTo', url: 'www.alternativeto.net', + lang: 'en', }; diff --git a/lib/routes/alternativeto/utils.ts b/lib/routes/alternativeto/utils.ts index 92b1fd81f97c45..c53c34b8afe6f0 100644 --- a/lib/routes/alternativeto/utils.ts +++ b/lib/routes/alternativeto/utils.ts @@ -14,7 +14,7 @@ const puppeteerGet = (url, cache) => waitUntil: 'domcontentloaded', }); const html = await page.evaluate(() => document.documentElement.innerHTML); - browser.close(); + await browser.close(); return html; }); diff --git a/lib/routes/amazon/namespace.ts b/lib/routes/amazon/namespace.ts index 5e5086e0caa6d7..7815db99cfa5bf 100644 --- a/lib/routes/amazon/namespace.ts +++ b/lib/routes/amazon/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Amazon', url: 'amazon.com', + lang: 'en', }; diff --git a/lib/routes/amz123/kx.ts b/lib/routes/amz123/kx.ts new file mode 100644 index 00000000000000..329b764b3fd7ed --- /dev/null +++ b/lib/routes/amz123/kx.ts @@ -0,0 +1,65 @@ +import { Route, ViewType } from '@/types'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/kx', + categories: ['new-media'], + example: '/amz123/kx', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['amz123.com/kx'], + target: '/kx', + }, + ], + name: 'AMZ123 快讯', + maintainers: ['defp'], + handler, + url: 'amz123.com/kx', + view: ViewType.Articles, +}; + +async function handler() { + const limit = 12; + const apiRootUrl = 'https://api.amz123.com'; + const rootUrl = 'https://www.amz123.com'; + + const { data: response } = await got.post(`${apiRootUrl}/ugc/v1/user_content/forum_list`, { + json: { + page: 1, + page_size: limit, + tag_id: 0, + fid: 4, + ban: 0, + is_new: 1, + }, + headers: { + 'content-type': 'application/json', + }, + }); + + const items = response.data.rows.map((item) => ({ + title: item.title, + description: item.description, + pubDate: parseDate(item.published_at * 1000), + link: `${rootUrl}/kx/${item.id}`, + author: item.author?.username, + category: item.tags.map((tag) => tag.name), + guid: item.resource_id, + })); + + return { + title: 'AMZ123 快讯', + link: `${rootUrl}/kx`, + item: items, + }; +} diff --git a/lib/routes/amz123/namespace.ts b/lib/routes/amz123/namespace.ts new file mode 100644 index 00000000000000..289242d2dd8894 --- /dev/null +++ b/lib/routes/amz123/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Amz123', + url: 'www.amz123.com', + categories: ['new-media'], + description: '跨境电商平台', + lang: 'zh-CN', +}; diff --git a/lib/routes/android/namespace.ts b/lib/routes/android/namespace.ts index 6e719bce7e4b28..ddcd05dcda2549 100644 --- a/lib/routes/android/namespace.ts +++ b/lib/routes/android/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Android', url: 'developer.android.com', + lang: 'en', }; diff --git a/lib/routes/anime1/anime.ts b/lib/routes/anime1/anime.ts new file mode 100644 index 00000000000000..a106a001c5d214 --- /dev/null +++ b/lib/routes/anime1/anime.ts @@ -0,0 +1,63 @@ +import { Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +export const route: Route = { + path: 'anime/:category/:name', + name: 'Anime', + url: 'anime1.me', + maintainers: ['cxheng315'], + example: '/anime1/anime/2024年夏季/神之塔-第二季', + categories: ['anime'], + parameters: { + category: 'Anime1 Category', + name: 'Anime1 Name', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['anime1.me/category/:category/:name'], + target: '/anime/:category/:name', + }, + ], + handler, +}; + +async function handler(ctx) { + const { category, name } = ctx.req.param(); + + const response = await ofetch(`https://anime1.me/category/${category}/${name}`); + + const $ = load(response); + + const title = $('.page-title').text().trim(); + + const items = $('article') + .toArray() + .map((el) => { + const $el = $(el); + const title = $el.find('.entry-title a').text().trim(); + return { + title, + link: $el.find('.entry-title a').attr('href'), + description: title, + pubDate: parseDate($el.find('time').attr('datetime') || ''), + itunes_item_image: $el.find('video').attr('poster'), + }; + }); + + return { + title, + link: `https://anime1.me/category/${category}/${name}`, + description: title, + item: items, + }; +} diff --git a/lib/routes/anime1/namespace.ts b/lib/routes/anime1/namespace.ts new file mode 100644 index 00000000000000..7d5644fadc9230 --- /dev/null +++ b/lib/routes/anime1/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Anime1', + url: 'anime1.me', + lang: 'zh-TW', +}; diff --git a/lib/routes/anime1/search.ts b/lib/routes/anime1/search.ts new file mode 100644 index 00000000000000..2e2e8fa806f6cf --- /dev/null +++ b/lib/routes/anime1/search.ts @@ -0,0 +1,57 @@ +import { Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +export const route: Route = { + path: 'search/:keyword', + name: 'Search', + url: 'anime1.me', + maintainers: ['cxheng315'], + example: '/anime1/search/神之塔', + categories: ['anime'], + parameters: { + keyword: 'Anime1 Search Keyword', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + handler, +}; + +async function handler(ctx) { + const { keyword } = ctx.req.param(); + + const response = await ofetch(`https://anime1.me/?s=${keyword}`); + + const $ = load(response); + + const title = $('page-title').text().trim(); + + const items = $('article.type-post') + .toArray() + .map((el) => { + const $el = $(el); + const title = $el.find('.entry-title a').text().trim(); + return { + title, + link: $el.find('.entry-title a').attr('href'), + description: title, + pubDate: parseDate($el.find('time').attr('datetime') || ''), + }; + }); + + return { + title, + link: `https://anime1.me/?s=${keyword}`, + description: title, + itunes_author: 'Anime1', + itunes_image: 'https://anime1.me/wp-content/uploads/2021/02/cropped-1-180x180.png', + item: items, + }; +} diff --git a/lib/routes/annualreviews/index.ts b/lib/routes/annualreviews/index.ts index bf881094836520..679f95cbc2bfa5 100644 --- a/lib/routes/annualreviews/index.ts +++ b/lib/routes/annualreviews/index.ts @@ -27,9 +27,9 @@ export const route: Route = { handler, description: `The URL of the journal [Annual Review of Analytical Chemistry](https://www.annualreviews.org/journal/anchem) is \`https://www.annualreviews.org/journal/anchem\`, where \`anchem\` is the id of the journal, so the route for this journal is \`/annualreviews/anchem\`. - :::tip +::: tip More jounals can be found in [Browse Journals](https://www.annualreviews.org/action/showPublications). - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/annualreviews/namespace.ts b/lib/routes/annualreviews/namespace.ts index bcbac131836345..9ce5576f8ea459 100644 --- a/lib/routes/annualreviews/namespace.ts +++ b/lib/routes/annualreviews/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Annual Reviews', url: 'annualreviews.org', + lang: 'en', }; diff --git a/lib/routes/anquanke/category.ts b/lib/routes/anquanke/category.ts index 1554e1d8251300..dbc4e5f7f4c1ea 100644 --- a/lib/routes/anquanke/category.ts +++ b/lib/routes/anquanke/category.ts @@ -22,8 +22,8 @@ export const route: Route = { maintainers: ['qwertyuiop6'], handler, description: `| 360 网络安全周报 | 活动 | 知识 | 资讯 | 招聘 | 工具 | - | ---------------- | -------- | --------- | ---- | ---- | ---- | - | week | activity | knowledge | news | job | tool |`, +| ---------------- | -------- | --------- | ---- | ---- | ---- | +| week | activity | knowledge | news | job | tool |`, }; async function handler(ctx) { diff --git a/lib/routes/anquanke/namespace.ts b/lib/routes/anquanke/namespace.ts index 8373e6e9d3b73a..e9ad9674f5165f 100644 --- a/lib/routes/anquanke/namespace.ts +++ b/lib/routes/anquanke/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '安全客', url: 'anquanke.com', - description: `:::tip + description: `::: tip 官方提供了混合的主页资讯 RSS: [https://api.anquanke.com/data/v1/rss](https://api.anquanke.com/data/v1/rss) :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/anthropic/namespace.ts b/lib/routes/anthropic/namespace.ts new file mode 100644 index 00000000000000..f11fc642cd2092 --- /dev/null +++ b/lib/routes/anthropic/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Anthropic', + url: 'anthropic.com', + lang: 'en', +}; diff --git a/lib/routes/anthropic/news.ts b/lib/routes/anthropic/news.ts new file mode 100644 index 00000000000000..3f6371cd9b9313 --- /dev/null +++ b/lib/routes/anthropic/news.ts @@ -0,0 +1,75 @@ +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import { Route } from '@/types'; + +export const route: Route = { + path: '/news', + categories: ['programming'], + example: '/anthropic/news', + parameters: {}, + radar: [ + { + source: ['www.anthropic.com/news', 'www.anthropic.com'], + }, + ], + name: 'News', + maintainers: ['etShaw-zh'], + handler, + url: 'www.anthropic.com/news', +}; + +async function handler() { + const link = 'https://www.anthropic.com/news'; + const response = await ofetch(link); + const $ = load(response); + + const list = $('.contentFadeUp a') + .toArray() + .map((e) => { + e = $(e); + const title = e.find('h3[class^="PostCard_post-heading__"]').text().trim(); + const href = e.attr('href'); + const pubDate = e.find('div[class^="PostList_post-date__"]').text().trim(); + const fullLink = href.startsWith('http') ? href : `https://www.anthropic.com${href}`; + return { + title, + link: fullLink, + pubDate, + }; + }); + + const out = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + + $('div[class^="PostDetail_b-social-share"]').remove(); + + const content = $('div[class*="PostDetail_post-detail__"]'); + content.find('img').each((_, e) => { + const $e = $(e); + $e.removeAttr('style srcset'); + const src = $e.attr('src'); + const params = new URLSearchParams(src); + const newSrc = params.get('/_next/image?url'); + if (newSrc) { + $e.attr('src', newSrc); + } + }); + + item.description = content.html(); + + return item; + }) + ) + ); + + return { + title: 'Anthropic News', + link, + description: 'Latest news from Anthropic', + item: out, + }; +} diff --git a/lib/routes/apache/namespace.ts b/lib/routes/apache/namespace.ts index a551844046dd24..d6b71e27ca2aca 100644 --- a/lib/routes/apache/namespace.ts +++ b/lib/routes/apache/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Apache', url: 'apisix.apache.org', + lang: 'en', }; diff --git a/lib/routes/apiseven/namespace.ts b/lib/routes/apiseven/namespace.ts index fb3d425ce3f108..d1482c49afda0c 100644 --- a/lib/routes/apiseven/namespace.ts +++ b/lib/routes/apiseven/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '支流科技', url: 'apiseven.com', + lang: 'zh-CN', }; diff --git a/lib/routes/apkpure/namespace.ts b/lib/routes/apkpure/namespace.ts index b26ba635b1d239..6bacb019896ec1 100644 --- a/lib/routes/apkpure/namespace.ts +++ b/lib/routes/apkpure/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'APKPure', url: 'apkpure.com', + lang: 'en', }; diff --git a/lib/routes/apkpure/versions.ts b/lib/routes/apkpure/versions.ts index 793db24b3ecb14..300c00931ac784 100644 --- a/lib/routes/apkpure/versions.ts +++ b/lib/routes/apkpure/versions.ts @@ -39,7 +39,7 @@ async function handler(ctx) { }); const r = await page.evaluate(() => document.documentElement.innerHTML); - browser.close(); + await browser.close(); const $ = load(r); const img = new URL($('.ver-top img').attr('src')); diff --git a/lib/routes/apnews/api.ts b/lib/routes/apnews/api.ts new file mode 100644 index 00000000000000..f0edc8582bb5e3 --- /dev/null +++ b/lib/routes/apnews/api.ts @@ -0,0 +1,77 @@ +import { Route, ViewType } from '@/types'; +import { fetchArticle } from './utils'; +import ofetch from '@/utils/ofetch'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/api/:tags?', + categories: ['traditional-media', 'popular'], + example: '/apnews/api/apf-topnews', + view: ViewType.Articles, + parameters: { + tags: { + description: 'Getting a list of articles from a public API based on tags.', + options: [ + { value: 'apf-topnews', label: 'Top News' }, + { value: 'apf-sports', label: 'Sports' }, + { value: 'apf-politics', label: 'Politics' }, + { value: 'apf-entertainment', label: 'Entertainment' }, + { value: 'apf-usnews', label: 'US News' }, + { value: 'apf-oddities', label: 'Oddities' }, + { value: 'apf-Travel', label: 'Travel' }, + { value: 'apf-technology', label: 'Technology' }, + { value: 'apf-lifestyle', label: 'Lifestyle' }, + { value: 'apf-business', label: 'Business' }, + { value: 'apf-Health', label: 'Health' }, + { value: 'apf-science', label: 'Science' }, + { value: 'apf-intlnews', label: 'International News' }, + ], + default: 'apf-topnews', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['apnews.com/'], + }, + ], + name: 'News', + maintainers: ['dzx-dzx'], + handler, +}; + +async function handler(ctx) { + const { tags = 'apf-topnews' } = ctx.req.param(); + const apiRootUrl = 'https://afs-prod.appspot.com/api/v2/feed/tag'; + const url = `${apiRootUrl}?tags=${tags}`; + const res = await ofetch(url); + + const list = res.cards + .map((e) => ({ + title: e.contents[0]?.headline, + link: e.contents[0]?.localLinkUrl, + pubDate: timezone(parseDate(e.publishedDate), 0), + category: e.tagObjs.map((tag) => tag.name), + updated: timezone(parseDate(e.contents[0]?.updated), 0), + description: e.contents[0]?.storyHTML, + author: e.contents[0]?.reporters.map((author) => ({ name: author.displayName })), + })) + .sort((a, b) => b.pubDate - a.pubDate) + .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20); + + const items = ctx.req.query('fulltext') === 'true' ? await Promise.all(list.map((item) => fetchArticle(item))) : list; + + return { + title: `${res.tagObjs[0].name} - AP News`, + item: items, + link: 'https://apnews.com', + }; +} diff --git a/lib/routes/apnews/namespace.ts b/lib/routes/apnews/namespace.ts index cdaed3fb58e25b..48e7aa1caf6e8c 100644 --- a/lib/routes/apnews/namespace.ts +++ b/lib/routes/apnews/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AP News', url: 'apnews.com', + lang: 'en', }; diff --git a/lib/routes/apnews/rss.ts b/lib/routes/apnews/rss.ts index dc600eec5235b4..c14ad46713dfb4 100644 --- a/lib/routes/apnews/rss.ts +++ b/lib/routes/apnews/rss.ts @@ -1,13 +1,19 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import parser from '@/utils/rss-parser'; import { fetchArticle } from './utils'; const HOME_PAGE = 'https://apnews.com'; export const route: Route = { - path: '/rss/:rss?', + path: '/rss/:category?', categories: ['traditional-media'], example: '/apnews/rss/business', - parameters: { rss: 'Route name from the first segment of the corresponding site, or `index` for the front page(default).' }, + view: ViewType.Articles, + parameters: { + category: { + description: 'Category from the first segment of the corresponding site, or `index` for the front page.', + default: 'index', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -22,7 +28,7 @@ export const route: Route = { target: '/rss/:rss', }, ], - name: 'RSS', + name: 'News', maintainers: ['zoenglinghou', 'mjysci', 'TonyRL'], handler, }; @@ -32,10 +38,10 @@ async function handler(ctx) { const url = `${HOME_PAGE}/${rss}.rss`; const res = await parser.parseURL(url); - const items = await Promise.all(res.items.map((item) => fetchArticle(item))); + const items = ctx.req.query('fulltext') === 'true' ? await Promise.all(res.items.map((item) => fetchArticle(item))) : res; return { - ...rss, + ...res, item: items, }; } diff --git a/lib/routes/apnews/sitemap.ts b/lib/routes/apnews/sitemap.ts new file mode 100644 index 00000000000000..655ab7f088d01f --- /dev/null +++ b/lib/routes/apnews/sitemap.ts @@ -0,0 +1,91 @@ +import { Route, ViewType } from '@/types'; +import { asyncPoolAll, fetchArticle } from './utils'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +const HOME_PAGE = 'https://apnews.com'; + +export const route: Route = { + path: '/sitemap/:route', + categories: ['traditional-media'], + example: '/apnews/sitemap/ap-sitemap-latest', + view: ViewType.Articles, + parameters: { + route: { + description: 'Route for sitemap, excluding the `.xml` extension', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['apnews.com/'], + }, + ], + name: 'Sitemap', + maintainers: ['zoenglinghou', 'mjysci', 'TonyRL', 'dzx-dzx'], + handler, +}; + +async function handler(ctx) { + const route = ctx.req.param('route'); + const url = `${HOME_PAGE}/${route}.xml`; + const response = await ofetch(url); + const $ = load(response); + + const list = $('urlset url') + .toArray() + .map((e) => { + const LANGUAGE_MAP = new Map([ + ['eng', 'en'], + ['spa', 'es'], + ]); + + const title = $(e) + .find(String.raw`news\:title`) + .text(); + const pubDate = parseDate( + $(e) + .find(String.raw`news\:publication_date`) + .text() + ); + const lastmod = timezone(parseDate($(e).find(`lastmod`).text()), -4); + const language = LANGUAGE_MAP.get( + $(e) + .find(String.raw`news\:language`) + .text() + ); + let res = { link: $(e).find('loc').text() }; + if (title) { + res = Object.assign(res, { title }); + } + if (pubDate.toString() !== 'Invalid Date') { + res = Object.assign(res, { pubDate }); + } + if (language) { + res = Object.assign(res, { language }); + } + if (lastmod.toString() !== 'Invalid Date') { + res = Object.assign(res, { lastmod }); + } + return res; + }) + .filter((e) => Boolean(e.link) && !new URL(e.link).pathname.split('/').includes('hub')) + .sort((a, b) => (a.pubDate && b.pubDate ? b.pubDate - a.pubDate : b.lastmod - a.lastmod)) + .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20); + + const items = ctx.req.query('fulltext') === 'true' ? await asyncPoolAll(20, list, (item) => fetchArticle(item)) : list; + + return { + title: `AP News sitemap:${route}`, + item: items, + link: 'https://apnews.com', + }; +} diff --git a/lib/routes/apnews/topics.ts b/lib/routes/apnews/topics.ts index 283c8e0837f5fb..a4f65f319d7c4c 100644 --- a/lib/routes/apnews/topics.ts +++ b/lib/routes/apnews/topics.ts @@ -1,14 +1,20 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { load } from 'cheerio'; -import { fetchArticle } from './utils'; +import { asyncPoolAll, fetchArticle, removeDuplicateByKey } from './utils'; const HOME_PAGE = 'https://apnews.com'; export const route: Route = { - path: '/topics/:topic?', + path: ['/topics/:topic?', '/nav/:nav{.*}?'], categories: ['traditional-media'], example: '/apnews/topics/apf-topnews', - parameters: { topic: 'Topic name, can be found in URL. For example: the topic name of AP Top News [https://apnews.com/apf-topnews](https://apnews.com/apf-topnews) is `apf-topnews`, `trending-news` by default' }, + view: ViewType.Articles, + parameters: { + topic: { + description: 'Topic name, can be found in URL. For example: the topic name of AP Top News [https://apnews.com/apf-topnews](https://apnews.com/apf-topnews) is `apf-topnews`', + default: 'trending-news', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -29,28 +35,28 @@ export const route: Route = { }; async function handler(ctx) { - const { topic = 'trending-news' } = ctx.req.param(); - const url = `${HOME_PAGE}/hub/${topic}`; + const { topic = 'trending-news', nav = '' } = ctx.req.param(); + const useNav = ctx.req.routePath === '/apnews/nav/:nav{.*}?'; + const url = useNav ? `${HOME_PAGE}/${nav}` : `${HOME_PAGE}/hub/${topic}`; const response = await got(url); const $ = load(response.data); - const items = await Promise.all( - $(':is(.PagePromo-content, .PageListStandardE-leadPromo-info) bsp-custom-headline') - .get() - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : Infinity) - .map((e) => ({ - title: $(e).find('span.PagePromoContentIcons-text').text(), - link: $(e).find('a').attr('href'), - })) - .filter((e) => typeof e.link === 'string') - .map((item) => fetchArticle(item)) - ); + const list = $(':is(.PagePromo-content, .PageListStandardE-leadPromo-info) bsp-custom-headline') + .toArray() + .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : Infinity) + .map((e) => ({ + title: $(e).find('span.PagePromoContentIcons-text').text(), + link: $(e).find('a').attr('href'), + })) + .filter((e) => typeof e.link === 'string'); + + const items = ctx.req.query('fulltext') === 'true' ? await asyncPoolAll(10, list, (item) => fetchArticle(item)) : list; return { title: $('title').text(), description: $("meta[property='og:description']").text(), link: url, - item: items, + item: removeDuplicateByKey(items, 'link'), language: $('html').attr('lang'), }; } diff --git a/lib/routes/apnews/utils.ts b/lib/routes/apnews/utils.ts index 4e8b74eba610c1..5db057510ade07 100644 --- a/lib/routes/apnews/utils.ts +++ b/lib/routes/apnews/utils.ts @@ -1,20 +1,74 @@ 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'; +import asyncPool from 'tiny-async-pool'; + +export function removeDuplicateByKey(items, key: string) { + return [...new Map(items.map((x) => [x[key], x])).values()]; +} export function fetchArticle(item) { return cache.tryGet(item.link, async () => { const data = await ofetch(item.link); const $ = load(data); - $('div.Enhancement').remove(); - return Object.assign(item, { - pubDate: timezone(parseDate($("meta[property='article:published_time']").attr('content')), 0), - updated: timezone(parseDate($("meta[property='article:modified_time']").attr('content')), 0), - description: $('div.RichTextStoryBody').html(), - category: $("meta[property='article:section']").attr('content'), - guid: $("meta[name='brightspot.contentId']").attr('content'), - }); + if ($('#link-ld-json').length === 0) { + const gtmRaw = $('meta[name="gtm-dataLayer"]').attr('content'); + if (gtmRaw) { + const gtmParsed = JSON.parse(gtmRaw); + return { + title: gtmParsed.headline, + pubDate: parseDate(gtmParsed.publication_date), + description: $('div.RichTextStoryBody').html() || $(':is(.VideoLead, .VideoPage-pageSubHeading)').html(), + category: gtmParsed.tag_array.split(','), + guid: $("meta[name='brightspot.contentId']").attr('content'), + author: gtmParsed.author, + ...item, + }; + } else { + return item; + } + } + const rawLdjson = JSON.parse($('#link-ld-json').text()); + let ldjson; + if (rawLdjson['@type'] === 'NewsArticle' || (Array.isArray(rawLdjson) && rawLdjson.some((e) => e['@type'] === 'NewsArticle'))) { + // Regular(Articles, Videos) + ldjson = Array.isArray(rawLdjson) ? rawLdjson.find((e) => e['@type'] === 'NewsArticle') : rawLdjson; + + $('div.Enhancement').remove(); + const section = $("meta[property='article:section']").attr('content'); + return { + title: ldjson.headline, + pubDate: parseDate(ldjson.datePublished), + updated: parseDate(ldjson.dateModified), + description: $('div.RichTextStoryBody').html() || $(':is(.VideoLead, .VideoPage-pageSubHeading)').html(), + category: [...(section ? [section] : []), ...(ldjson.keywords ?? [])], + guid: $("meta[name='brightspot.contentId']").attr('content'), + author: ldjson.author, + ...item, + }; + } else { + // Live + ldjson = rawLdjson; + + const url = new URL(item.link); + const description = url.hash ? $(url.hash).parent().find('.LiveBlogPost-body').html() : ldjson.description; + const pubDate = url.hash ? parseDate(Number.parseInt($(url.hash).parent().attr('data-posted-date-timestamp'), 10)) : parseDate(ldjson.coverageStartTime); + + return { + category: ldjson.keywords, + pubDate, + description, + guid: $("meta[name='brightspot.contentId']").attr('content'), + ...item, + }; + } }); } +export async function asyncPoolAll(poolLimit: number, array: readonly IN[], iteratorFn: (generator: IN) => Promise) { + const results: Awaited = []; + for await (const result of asyncPool(poolLimit, array, iteratorFn)) { + results.push(result); + } + return results; +} diff --git a/lib/routes/apnic/index.ts b/lib/routes/apnic/index.ts new file mode 100644 index 00000000000000..6570faec284b98 --- /dev/null +++ b/lib/routes/apnic/index.ts @@ -0,0 +1,61 @@ +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: '/blog', + categories: ['blog'], + example: '/apnic/blog', + url: 'blog.apnic.net', + name: 'Blog', + maintainers: ['p3psi-boo'], + handler, +}; + +async function handler() { + const baseUrl = 'https://blog.apnic.net'; + const feedUrl = `${baseUrl}/feed/`; + + const response = await got(feedUrl); + const $ = load(response.data, { xmlMode: true }); + + // 从 RSS XML 中直接提取文章信息 + const list = $('item') + .toArray() + .map((item) => { + const $item = $(item); + return { + title: $item.find('title').text(), + link: $item.find('link').text(), + author: $item.find(String.raw`dc\:creator`).text(), + category: + $item + .find('category') + .text() + .match(/>([^<]+) + cache.tryGet(item.link, async () => { + const { data: articleData } = await got(item.link); + const $article = load(articleData); + + // 获取文章正文内容 + item.description = $article('.entry-content').html(); + return item; + }) + ) + ); + + return { + title: 'APNIC Blog', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/apnic/namespace.ts b/lib/routes/apnic/namespace.ts new file mode 100644 index 00000000000000..e3e9914088b2aa --- /dev/null +++ b/lib/routes/apnic/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'APNIC', + url: 'blog.apnic.net', + description: 'Asia-Pacific Network Information Centre', + lang: 'en', +}; diff --git a/lib/routes/app-center/namespace.ts b/lib/routes/app-center/namespace.ts index e5efc03525fb9d..c3dfb838b4352e 100644 --- a/lib/routes/app-center/namespace.ts +++ b/lib/routes/app-center/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'App Center', url: 'install.appcenter.ms', + lang: 'en', }; diff --git a/lib/routes/app-center/release.ts b/lib/routes/app-center/release.ts index e4b4370dab82db..fa8fcfa277fd3c 100644 --- a/lib/routes/app-center/release.ts +++ b/lib/routes/app-center/release.ts @@ -30,9 +30,9 @@ export const route: Route = { name: 'Release', maintainers: ['Rongronggg9'], handler, - description: `:::tip + description: `::: tip The parameters can be extracted from the Release page URL: \`https://install.appcenter.ms/users/:user/apps/:app/distribution_groups/:distribution_group\` - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/apple/apps.ts b/lib/routes/apple/apps.ts index 303cb7d95c5178..69474f0a0ee420 100644 --- a/lib/routes/apple/apps.ts +++ b/lib/routes/apple/apps.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; @@ -17,9 +17,34 @@ const platforms = { export const route: Route = { path: '/apps/update/:country/:id/:platform?', - categories: ['program-update'], + categories: ['program-update', 'popular'], + view: ViewType.Notifications, example: '/apple/apps/update/us/id408709785', - parameters: { country: 'App Store Country, obtain from the app URL, see below', id: 'App id, obtain from the app URL', platform: 'App Platform, see below, all by default' }, + parameters: { + country: 'App Store Country, obtain from the app URL, see below', + id: 'App id, obtain from the app URL', + platform: { + description: 'App Platform, see below, all by default', + options: [ + { + value: 'All', + label: 'all', + }, + { + value: 'iOS', + label: 'iOS', + }, + { + value: 'macOS', + label: 'macOS', + }, + { + value: 'tvOS', + label: 'tvOS', + }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -37,13 +62,10 @@ export const route: Route = { name: 'App Update', maintainers: ['EkkoG', 'nczitzk'], handler, - description: `| All | iOS | macOS | tvOS | - | --- | --- | ----- | ---- | - | | iOS | macOS | tvOS | - - :::tip + description: ` +::: tip For example, the URL of [GarageBand](https://apps.apple.com/us/app/messages/id408709785) in the App Store is \`https://apps.apple.com/us/app/messages/id408709785\`. In this case, the \`App Store Country\` parameter for the route is \`us\`, and the \`App id\` parameter is \`id1146560473\`. So the route should be [\`/apple/apps/update/us/id408709785\`](https://rsshub.app/apple/apps/update/us/id408709785). - :::`, +:::`, }; async function handler(ctx) { @@ -52,7 +74,7 @@ async function handler(ctx) { let platformId; - if (platform) { + if (platform && platform !== 'all') { platform = platform.toLowerCase(); platformId = Object.hasOwn(platforms, platform) ? platforms[platform] : platform; } @@ -123,7 +145,7 @@ async function handler(ctx) { return { item: items, - title: `${title} - Apple App Stroe`, + title: `${title} - Apple App Store`, link: currentUrl, description: description?.replace(/\n/g, ' '), language: $('html').prop('lang'), diff --git a/lib/routes/apple/namespace.ts b/lib/routes/apple/namespace.ts index c9a524f201cf24..4f9fd18169a3fe 100644 --- a/lib/routes/apple/namespace.ts +++ b/lib/routes/apple/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Apple', url: 'apps.apple.com', + lang: 'en', }; diff --git a/lib/routes/apple/podcast.ts b/lib/routes/apple/podcast.ts index 07d1acd36e875b..6eb313987ec006 100644 --- a/lib/routes/apple/podcast.ts +++ b/lib/routes/apple/podcast.ts @@ -4,10 +4,13 @@ import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: '/podcast/:id', + path: '/podcast/:id/:region?', categories: ['multimedia'], - example: '/apple/podcast/id1559695855', - parameters: { id: '播客id,可以在 Apple 播客app 内分享的播客的 URL 中找到' }, + example: '/apple/podcast/id1559695855/cn', + parameters: { + id: '播客id,可以在 Apple 播客app 内分享的播客的 URL 中找到', + region: '地區代碼,例如 cn、us、jp,預設為 cn', + }, features: { requireConfig: false, requirePuppeteer: false, @@ -18,17 +21,18 @@ export const route: Route = { }, radar: [ { - source: ['podcasts.apple.com/cn/podcast/:id'], + source: ['podcasts.apple.com/:region/podcast/:id'], }, ], name: '播客', maintainers: ['Acring'], handler, - url: 'https://www.apple.com.cn/apple-podcasts/', + url: 'www.apple.com/apple-podcasts/', }; async function handler(ctx) { - const link = `https://podcasts.apple.com/cn/podcast/${ctx.req.param('id')}`; + const { id, region } = ctx.req.param(); + const link = `https://podcasts.apple.com/${region || `cn`}/podcast/${id}`; const response = await got({ method: 'get', url: link, @@ -36,29 +40,33 @@ async function handler(ctx) { const $ = load(response.data); - const page_data = JSON.parse($('#shoebox-media-api-cache-amp-podcasts').text()); + const serializedServerData = JSON.parse($('#serialized-server-data').text()); + + const seoEpisodes = serializedServerData[0].data.seoData.schemaContent.workExample; + const originEpisodes = serializedServerData[0].data.shelves.find((item) => item.contentType === 'episode').items; + const header = serializedServerData[0].data.shelves.find((item) => item.contentType === 'showHeaderRegular').items[0]; - const data = JSON.parse(page_data[Object.keys(page_data)[0]]).d[0]; - const attributes = data.attributes; + const episodes = originEpisodes.map((item) => { + // Try to keep line breaks in the description + const matchedSeoEpisode = seoEpisodes.find((seoEpisode) => seoEpisode.name === item.title) || null; + const episodeDescription = (matchedSeoEpisode ? matchedSeoEpisode.description : item.summary).replaceAll('\n', '
    '); - const episodes = data.relationships.episodes.data.map((item) => { - const attr = item.attributes; return { - title: attr.name, - enclosure_url: attr.assetUrl, - itunes_duration: attr.durationInMilliseconds / 1000, + title: item.title, + enclosure_url: item.playAction.episodeOffer.streamUrl, enclosure_type: 'audio/mp4', - link: attr.url, - pubDate: parseDate(attr.releaseDateTime), - description: attr.description.standard.replaceAll('\n', '
    '), + itunes_duration: item.duration, + link: item.playAction.episodeOffer.storeUrl, + pubDate: parseDate(item.releaseDate), + description: episodeDescription, }; }); return { - title: attributes.name, - link: attributes.url, - itunes_author: attributes.artistName, + title: header.title, + link: header.contextAction.podcastOffer.storeUrl, + itunes_author: header.contextAction.podcastOffer.author, item: episodes, - description: attributes.description.standard, + description: header.description.replaceAll('\n', ' '), }; } diff --git a/lib/routes/appleinsider/index.ts b/lib/routes/appleinsider/index.ts index e61c774cda0bba..5fb118e3487b84 100644 --- a/lib/routes/appleinsider/index.ts +++ b/lib/routes/appleinsider/index.ts @@ -6,7 +6,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/:category?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/appleinsider', parameters: { category: 'Category, see below, News by default' }, features: { @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| News | Reviews | How-tos | - | ---- | ------- | ------- | - | | reviews | how-to |`, +| ---- | ------- | ------- | +| | reviews | how-to |`, }; async function handler(ctx) { diff --git a/lib/routes/appleinsider/namespace.ts b/lib/routes/appleinsider/namespace.ts index b4ac79aa69b185..b2712ee6b3a6aa 100644 --- a/lib/routes/appleinsider/namespace.ts +++ b/lib/routes/appleinsider/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AppleInsider', url: 'appleinsider.com', + lang: 'en', }; diff --git a/lib/routes/appstare/comments.ts b/lib/routes/appstare/comments.ts new file mode 100644 index 00000000000000..8db1faf2f7f449 --- /dev/null +++ b/lib/routes/appstare/comments.ts @@ -0,0 +1,57 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; + +export const handler = async (ctx) => { + const country = ctx.req.param('country'); + const appid = ctx.req.param('appid'); + const url = `https://monitor.appstare.net/spider/appComments?country=${country}&appId=${appid}`; + const data = await ofetch(url); + + const items = data.map((item) => ({ + title: item.title, + description: ` +
    ${'⭐️'.repeat(Math.floor(item.rating))}
    +

    ${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 = $(el); + const $aEl: Cheerio = $el.find('div.storyDetails h3 a'); + + const title: string = $aEl.text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + images: + $el.find('a.image img').length > 0 + ? $el + .find('a.image img') + .toArray() + .map((imgEl) => { + const $imgEl: Cheerio = $(imgEl); + + return { + src: $imgEl.attr('src'), + alt: $imgEl.attr('alt'), + }; + }) + : undefined, + }); + const pubDateStr: string | undefined = $el.find('span.date').text(); + const linkUrl: string | undefined = $aEl.attr('href'); + const image: string | undefined = $el.find('a.image img').attr('src'); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.story_title h1').text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.storytext').html(), + }); + const pubDateStr: string | undefined = $$('span.date').first().text().split(/:/).pop(); + const categories: string[] = + $$('meta[name="keywords"]') + .attr('content') + ?.split(/,/) + .map((c) => c.trim()) ?? []; + const authors: DataItem['author'] = $$('span.author').first().text(); + const upDatedStr: string | undefined = pubDateStr; + + let processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + const extraLinkEls: Element[] = $$('div.extrasStory ul li').toArray(); + const extraLinks = extraLinkEls + .map((extraLinkEl) => { + const $$extraLinkEl: Cheerio = $$(extraLinkEl); + + return { + url: $$extraLinkEl.find('a').attr('href'), + type: 'related', + content_html: $$extraLinkEl.html(), + }; + }) + .filter((_): _ is { url: string; type: string; content_html: string } => true); + + if (extraLinks) { + processedItem = { + ...processedItem, + _extra: { + links: extraLinks, + }, + }; + } + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const title: string = $('title').text().trim(); + + return { + title, + description: $('meta[name="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('img.logo').attr('src'), + author: title.split(/-/).pop(), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/categories/:category?', + name: '果蔬品项', + url: 'asiafruitchina.net', + maintainers: ['nczitzk'], + handler, + example: '/asiafruitchina/categories/all', + parameters: { + category: { + description: '分类,默认为 `all`,即全部,可在对应分类页 URL 中找到', + options: [ + { + label: '全部', + value: 'all', + }, + { + label: '橙', + value: 'chengzi', + }, + { + label: '百香果', + value: 'baixiangguo', + }, + { + label: '菠萝/凤梨', + value: 'boluo', + }, + { + label: '菠萝蜜', + value: 'boluomi', + }, + { + label: '草莓', + value: 'caomei', + }, + { + label: '番荔枝/释迦', + value: 'fanlizhi', + }, + { + label: '番茄', + value: 'fanqie', + }, + { + label: '柑橘', + value: 'ganju', + }, + { + label: '哈密瓜', + value: 'hamigua', + }, + { + label: '核果', + value: 'heguo', + }, + { + label: '红毛丹', + value: 'hongmaodan', + }, + { + label: '火龙果', + value: 'huolongguo', + }, + { + label: '浆果', + value: 'jiangguo', + }, + { + label: '桔子', + value: 'juzi', + }, + { + label: '蓝莓', + value: 'lanmei', + }, + { + label: '梨', + value: 'li', + }, + { + label: '荔枝', + value: 'lizhi', + }, + { + label: '李子', + value: 'lizi', + }, + { + label: '榴莲', + value: 'liulian', + }, + { + label: '龙眼', + value: 'lognyan', + }, + { + label: '芦笋', + value: 'lusun', + }, + { + label: '蔓越莓', + value: 'manyuemei', + }, + { + label: '芒果', + value: 'mangguo', + }, + { + label: '猕猴桃/奇异果', + value: 'mihoutao', + }, + { + label: '柠檬', + value: 'ningmeng', + }, + { + label: '牛油果', + value: 'niuyouguo', + }, + { + label: '苹果', + value: 'pingguo', + }, + { + label: '葡萄/提子', + value: 'putao', + }, + { + label: '其他', + value: 'qita', + }, + { + label: '奇异莓', + value: 'qiyimei', + }, + { + label: '热带水果', + value: 'redaishuiguo', + }, + { + label: '山竹', + value: 'shanzhu', + }, + { + label: '石榴', + value: 'shiliu', + }, + { + label: '蔬菜', + value: 'shucai', + }, + { + label: '树莓', + value: 'shumei', + }, + { + label: '桃', + value: 'tao', + }, + { + label: '甜瓜', + value: 'tiangua', + }, + { + label: '甜椒', + value: 'tianjiao', + }, + { + label: '甜柿', + value: 'tianshi', + }, + { + label: '香蕉', + value: 'xiangjiao', + }, + { + label: '西瓜', + value: 'xigua', + }, + { + label: '西梅', + value: 'ximei', + }, + { + label: '杏', + value: 'xing', + }, + { + label: '椰子', + value: 'yezi', + }, + { + label: '杨梅', + value: 'yangmei', + }, + { + label: '樱桃', + value: 'yintao', + }, + { + label: '油桃', + value: 'youtao', + }, + { + label: '柚子', + value: 'youzi', + }, + ], + }, + }, + description: `:::tip +若订阅 [橙](https://asiafruitchina.net/categories?gspx=chengzi),网址为 \`https://asiafruitchina.net/categories?gspx=chengzi\`,请截取 \`https://asiafruitchina.net/categories?gspx=\` 到末尾的部分 \`chengzi\` 作为 \`category\` 参数填入,此时目标路由为 [\`/asiafruitchina/categories/chengzi\`](https://rsshub.app/asiafruitchina/categories/chengzi)。 +::: + +
    + 更多分类 + + | [全部](https://asiafruitchina.net/categories?gspx=all) | [橙](https://asiafruitchina.net/categories?gspx=chengzi) | [百香果](https://asiafruitchina.net/categories?gspx=baixiangguo) | [菠萝/凤梨](https://asiafruitchina.net/categories?gspx=boluo) | [菠萝蜜](https://asiafruitchina.net/categories?gspx=boluomi) | + | ------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------- | + | [all](https://rsshub.app/asiafruitchina/categories/all) | [chengzi](https://rsshub.app/asiafruitchina/categories/chengzi) | [baixiangguo](https://rsshub.app/asiafruitchina/categories/baixiangguo) | [boluo](https://rsshub.app/asiafruitchina/categories/boluo) | [boluomi](https://rsshub.app/asiafruitchina/categories/boluomi) | + + | [草莓](https://asiafruitchina.net/categories?gspx=caomei) | [番荔枝/释迦](https://asiafruitchina.net/categories?gspx=fanlizhi) | [番茄](https://asiafruitchina.net/categories?gspx=fanqie) | [柑橘](https://asiafruitchina.net/categories?gspx=ganju) | [哈密瓜](https://asiafruitchina.net/categories?gspx=hamigua) | + | ------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------- | + | [caomei](https://rsshub.app/asiafruitchina/categories/caomei) | [fanlizhi](https://rsshub.app/asiafruitchina/categories/fanlizhi) | [fanqie](https://rsshub.app/asiafruitchina/categories/fanqie) | [ganju](https://rsshub.app/asiafruitchina/categories/ganju) | [hamigua](https://rsshub.app/asiafruitchina/categories/hamigua) | + + | [核果](https://asiafruitchina.net/categories?gspx=heguo) | [红毛丹](https://asiafruitchina.net/categories?gspx=hongmaodan) | [火龙果](https://asiafruitchina.net/categories?gspx=huolongguo) | [浆果](https://asiafruitchina.net/categories?gspx=jiangguo) | [桔子](https://asiafruitchina.net/categories?gspx=juzi) | + | ----------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------- | + | [heguo](https://rsshub.app/asiafruitchina/categories/heguo) | [hongmaodan](https://rsshub.app/asiafruitchina/categories/hongmaodan) | [huolongguo](https://rsshub.app/asiafruitchina/categories/huolongguo) | [jiangguo](https://rsshub.app/asiafruitchina/categories/jiangguo) | [juzi](https://rsshub.app/asiafruitchina/categories/juzi) | + + | [蓝莓](https://asiafruitchina.net/categories?gspx=lanmei) | [梨](https://asiafruitchina.net/categories?gspx=li) | [荔枝](https://asiafruitchina.net/categories?gspx=lizhi) | [李子](https://asiafruitchina.net/categories?gspx=lizi) | [榴莲](https://asiafruitchina.net/categories?gspx=liulian) | + | ------------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------- | + | [lanmei](https://rsshub.app/asiafruitchina/categories/lanmei) | [li](https://rsshub.app/asiafruitchina/categories/li) | [lizhi](https://rsshub.app/asiafruitchina/categories/lizhi) | [lizi](https://rsshub.app/asiafruitchina/categories/lizi) | [liulian](https://rsshub.app/asiafruitchina/categories/liulian) | + + | [龙眼](https://asiafruitchina.net/categories?gspx=lognyan) | [芦笋](https://asiafruitchina.net/categories?gspx=lusun) | [蔓越莓](https://asiafruitchina.net/categories?gspx=manyuemei) | [芒果](https://asiafruitchina.net/categories?gspx=mangguo) | [猕猴桃/奇异果](https://asiafruitchina.net/categories?gspx=mihoutao) | + | --------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------- | + | [lognyan](https://rsshub.app/asiafruitchina/categories/lognyan) | [lusun](https://rsshub.app/asiafruitchina/categories/lusun) | [manyuemei](https://rsshub.app/asiafruitchina/categories/manyuemei) | [mangguo](https://rsshub.app/asiafruitchina/categories/mangguo) | [mihoutao](https://rsshub.app/asiafruitchina/categories/mihoutao) | + + | [柠檬](https://asiafruitchina.net/categories?gspx=ningmeng) | [牛油果](https://asiafruitchina.net/categories?gspx=niuyouguo) | [苹果](https://asiafruitchina.net/categories?gspx=pingguo) | [葡萄/提子](https://asiafruitchina.net/categories?gspx=putao) | [其他](https://asiafruitchina.net/categories?gspx=qita) | + | ----------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------- | + | [ningmeng](https://rsshub.app/asiafruitchina/categories/ningmeng) | [niuyouguo](https://rsshub.app/asiafruitchina/categories/niuyouguo) | [pingguo](https://rsshub.app/asiafruitchina/categories/pingguo) | [putao](https://rsshub.app/asiafruitchina/categories/putao) | [qita](https://rsshub.app/asiafruitchina/categories/qita) | + + | [奇异莓](https://asiafruitchina.net/categories?gspx=qiyimei) | [热带水果](https://asiafruitchina.net/categories?gspx=redaishuiguo) | [山竹](https://asiafruitchina.net/categories?gspx=shanzhu) | [石榴](https://asiafruitchina.net/categories?gspx=shiliu) | [蔬菜](https://asiafruitchina.net/categories?gspx=shucai) | + | --------------------------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | + | [qiyimei](https://rsshub.app/asiafruitchina/categories/qiyimei) | [redaishuiguo](https://rsshub.app/asiafruitchina/categories/redaishuiguo) | [shanzhu](https://rsshub.app/asiafruitchina/categories/shanzhu) | [shiliu](https://rsshub.app/asiafruitchina/categories/shiliu) | [shucai](https://rsshub.app/asiafruitchina/categories/shucai) | + + | [树莓](https://asiafruitchina.net/categories?gspx=shumei) | [桃](https://asiafruitchina.net/categories?gspx=tao) | [甜瓜](https://asiafruitchina.net/categories?gspx=tiangua) | [甜椒](https://asiafruitchina.net/categories?gspx=tianjiao) | [甜柿](https://asiafruitchina.net/categories?gspx=tianshi) | + | ------------------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------- | + | [shumei](https://rsshub.app/asiafruitchina/categories/shumei) | [tao](https://rsshub.app/asiafruitchina/categories/tao) | [tiangua](https://rsshub.app/asiafruitchina/categories/tiangua) | [tianjiao](https://rsshub.app/asiafruitchina/categories/tianjiao) | [tianshi](https://rsshub.app/asiafruitchina/categories/tianshi) | + + | [香蕉](https://asiafruitchina.net/categories?gspx=xiangjiao) | [西瓜](https://asiafruitchina.net/categories?gspx=xigua) | [西梅](https://asiafruitchina.net/categories?gspx=ximei) | [杏](https://asiafruitchina.net/categories?gspx=xing) | [椰子](https://asiafruitchina.net/categories?gspx=yezi) | + | ------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | + | [xiangjiao](https://rsshub.app/asiafruitchina/categories/xiangjiao) | [xigua](https://rsshub.app/asiafruitchina/categories/xigua) | [ximei](https://rsshub.app/asiafruitchina/categories/ximei) | [xing](https://rsshub.app/asiafruitchina/categories/xing) | [yezi](https://rsshub.app/asiafruitchina/categories/yezi) | + + | [杨梅](https://asiafruitchina.net/categories?gspx=yangmei) | [樱桃](https://asiafruitchina.net/categories?gspx=yintao) | [油桃](https://asiafruitchina.net/categories?gspx=youtao) | [柚子](https://asiafruitchina.net/categories?gspx=youzi) | + | --------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------- | + | [yangmei](https://rsshub.app/asiafruitchina/categories/yangmei) | [yintao](https://rsshub.app/asiafruitchina/categories/yintao) | [youtao](https://rsshub.app/asiafruitchina/categories/youtao) | [youzi](https://rsshub.app/asiafruitchina/categories/youzi) | + +
    +`, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['asiafruitchina.net/categories'], + target: (_, url) => { + const urlObj: URL = new URL(url); + const category: string | undefined = urlObj.searchParams.get('id') ?? undefined; + + return `/asiafruitchina/categories${category ? `/${category}` : ''}`; + }, + }, + { + title: '全部', + source: ['asiafruitchina.net/categories'], + target: '/categories/all', + }, + { + title: '橙', + source: ['asiafruitchina.net/categories'], + target: '/categories/chengzi', + }, + { + title: '百香果', + source: ['asiafruitchina.net/categories'], + target: '/categories/baixiangguo', + }, + { + title: '菠萝/凤梨', + source: ['asiafruitchina.net/categories'], + target: '/categories/boluo', + }, + { + title: '菠萝蜜', + source: ['asiafruitchina.net/categories'], + target: '/categories/boluomi', + }, + { + title: '草莓', + source: ['asiafruitchina.net/categories'], + target: '/categories/caomei', + }, + { + title: '番荔枝/释迦', + source: ['asiafruitchina.net/categories'], + target: '/categories/fanlizhi', + }, + { + title: '番茄', + source: ['asiafruitchina.net/categories'], + target: '/categories/fanqie', + }, + { + title: '柑橘', + source: ['asiafruitchina.net/categories'], + target: '/categories/ganju', + }, + { + title: '哈密瓜', + source: ['asiafruitchina.net/categories'], + target: '/categories/hamigua', + }, + { + title: '核果', + source: ['asiafruitchina.net/categories'], + target: '/categories/heguo', + }, + { + title: '红毛丹', + source: ['asiafruitchina.net/categories'], + target: '/categories/hongmaodan', + }, + { + title: '火龙果', + source: ['asiafruitchina.net/categories'], + target: '/categories/huolongguo', + }, + { + title: '浆果', + source: ['asiafruitchina.net/categories'], + target: '/categories/jiangguo', + }, + { + title: '桔子', + source: ['asiafruitchina.net/categories'], + target: '/categories/juzi', + }, + { + title: '蓝莓', + source: ['asiafruitchina.net/categories'], + target: '/categories/lanmei', + }, + { + title: '梨', + source: ['asiafruitchina.net/categories'], + target: '/categories/li', + }, + { + title: '荔枝', + source: ['asiafruitchina.net/categories'], + target: '/categories/lizhi', + }, + { + title: '李子', + source: ['asiafruitchina.net/categories'], + target: '/categories/lizi', + }, + { + title: '榴莲', + source: ['asiafruitchina.net/categories'], + target: '/categories/liulian', + }, + { + title: '龙眼', + source: ['asiafruitchina.net/categories'], + target: '/categories/lognyan', + }, + { + title: '芦笋', + source: ['asiafruitchina.net/categories'], + target: '/categories/lusun', + }, + { + title: '蔓越莓', + source: ['asiafruitchina.net/categories'], + target: '/categories/manyuemei', + }, + { + title: '芒果', + source: ['asiafruitchina.net/categories'], + target: '/categories/mangguo', + }, + { + title: '猕猴桃/奇异果', + source: ['asiafruitchina.net/categories'], + target: '/categories/mihoutao', + }, + { + title: '柠檬', + source: ['asiafruitchina.net/categories'], + target: '/categories/ningmeng', + }, + { + title: '牛油果', + source: ['asiafruitchina.net/categories'], + target: '/categories/niuyouguo', + }, + { + title: '苹果', + source: ['asiafruitchina.net/categories'], + target: '/categories/pingguo', + }, + { + title: '葡萄/提子', + source: ['asiafruitchina.net/categories'], + target: '/categories/putao', + }, + { + title: '其他', + source: ['asiafruitchina.net/categories'], + target: '/categories/qita', + }, + { + title: '奇异莓', + source: ['asiafruitchina.net/categories'], + target: '/categories/qiyimei', + }, + { + title: '热带水果', + source: ['asiafruitchina.net/categories'], + target: '/categories/redaishuiguo', + }, + { + title: '山竹', + source: ['asiafruitchina.net/categories'], + target: '/categories/shanzhu', + }, + { + title: '石榴', + source: ['asiafruitchina.net/categories'], + target: '/categories/shiliu', + }, + { + title: '蔬菜', + source: ['asiafruitchina.net/categories'], + target: '/categories/shucai', + }, + { + title: '树莓', + source: ['asiafruitchina.net/categories'], + target: '/categories/shumei', + }, + { + title: '桃', + source: ['asiafruitchina.net/categories'], + target: '/categories/tao', + }, + { + title: '甜瓜', + source: ['asiafruitchina.net/categories'], + target: '/categories/tiangua', + }, + { + title: '甜椒', + source: ['asiafruitchina.net/categories'], + target: '/categories/tianjiao', + }, + { + title: '甜柿', + source: ['asiafruitchina.net/categories'], + target: '/categories/tianshi', + }, + { + title: '香蕉', + source: ['asiafruitchina.net/categories'], + target: '/categories/xiangjiao', + }, + { + title: '西瓜', + source: ['asiafruitchina.net/categories'], + target: '/categories/xigua', + }, + { + title: '西梅', + source: ['asiafruitchina.net/categories'], + target: '/categories/ximei', + }, + { + title: '杏', + source: ['asiafruitchina.net/categories'], + target: '/categories/xing', + }, + { + title: '椰子', + source: ['asiafruitchina.net/categories'], + target: '/categories/yezi', + }, + { + title: '杨梅', + source: ['asiafruitchina.net/categories'], + target: '/categories/yangmei', + }, + { + title: '樱桃', + source: ['asiafruitchina.net/categories'], + target: '/categories/yintao', + }, + { + title: '油桃', + source: ['asiafruitchina.net/categories'], + target: '/categories/youtao', + }, + { + title: '柚子', + source: ['asiafruitchina.net/categories'], + target: '/categories/youzi', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/asiafruitchina/namespace.ts b/lib/routes/asiafruitchina/namespace.ts new file mode 100644 index 00000000000000..48c11815ea05f9 --- /dev/null +++ b/lib/routes/asiafruitchina/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '亚洲水果', + url: 'asiafruitchina.net', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/asiafruitchina/news.ts b/lib/routes/asiafruitchina/news.ts new file mode 100644 index 00000000000000..8cb1dcc994a8b6 --- /dev/null +++ b/lib/routes/asiafruitchina/news.ts @@ -0,0 +1,184 @@ +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 limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'https://asiafruitchina.net'; + const targetUrl: string = new URL('category/news', 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 = $(el); + const $aEl: Cheerio = $el.find('div.storyDetails h3 a'); + + const title: string = $aEl.text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + images: + $el.find('a.image img').length > 0 + ? $el + .find('a.image img') + .toArray() + .map((imgEl) => { + const $imgEl: Cheerio = $(imgEl); + + return { + src: $imgEl.attr('src'), + alt: $imgEl.attr('alt'), + }; + }) + : undefined, + }); + const pubDateStr: string | undefined = $el.find('span.date').text(); + const linkUrl: string | undefined = $aEl.attr('href'); + const image: string | undefined = $el.find('a.image img').attr('src'); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.story_title h1').text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.storytext').html(), + }); + const pubDateStr: string | undefined = $$('span.date').first().text().split(/:/).pop(); + const categories: string[] = + $$('meta[name="keywords"]') + .attr('content') + ?.split(/,/) + .map((c) => c.trim()) ?? []; + const authors: DataItem['author'] = $$('span.author').first().text(); + const upDatedStr: string | undefined = pubDateStr; + + let processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + const extraLinkEls: Element[] = $$('div.extrasStory ul li').toArray(); + const extraLinks = extraLinkEls + .map((extraLinkEl) => { + const $$extraLinkEl: Cheerio = $$(extraLinkEl); + + return { + url: $$extraLinkEl.find('a').attr('href'), + type: 'related', + content_html: $$extraLinkEl.html(), + }; + }) + .filter((_): _ is { url: string; type: string; content_html: string } => true); + + if (extraLinks) { + processedItem = { + ...processedItem, + _extra: { + links: extraLinks, + }, + }; + } + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const title: string = $('title').text().trim(); + + return { + title, + description: $('meta[name="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('img.logo').attr('src'), + author: title.split(/-/).pop(), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/news', + name: '行业资讯', + url: 'asiafruitchina.net', + maintainers: ['nczitzk'], + handler, + example: '/asiafruitchina/news', + parameters: undefined, + description: undefined, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['asiafruitchina.net/category/news'], + target: '/asiafruitchina/news', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/asiafruitchina/templates/description.art b/lib/routes/asiafruitchina/templates/description.art new file mode 100644 index 00000000000000..dfab19230c1108 --- /dev/null +++ b/lib/routes/asiafruitchina/templates/description.art @@ -0,0 +1,17 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
    + {{ image.alt }} +
    + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/asianfanfics/namespace.ts b/lib/routes/asianfanfics/namespace.ts new file mode 100644 index 00000000000000..7903b878628da4 --- /dev/null +++ b/lib/routes/asianfanfics/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Asianfanfics', + url: 'asianfanfics.com', + lang: 'en', +}; diff --git a/lib/routes/asianfanfics/tag.ts b/lib/routes/asianfanfics/tag.ts new file mode 100644 index 00000000000000..e83463092829d1 --- /dev/null +++ b/lib/routes/asianfanfics/tag.ts @@ -0,0 +1,91 @@ +import { DataItem, Route } from '@/types'; + +import { config } from '@/config'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; + +// test url http://localhost:1200/asianfanfics/tag/milklove/N + +export const route: Route = { + path: '/tag/:tag/:type', + categories: ['reading'], + example: '/asianfanfics/tag/milklove/N', + parameters: { + tag: '标签', + type: '排序类型', + }, + name: '标签', + maintainers: ['KazooTTT'], + radar: [ + { + source: ['www.asianfanfics.com/browse/tag/:tag/:type'], + target: '/tag/:tag/:type', + }, + ], + description: `匹配asianfanfics标签,支持排序类型: +- L: Latest 最近更新 +- N: Newest 最近发布 +- O: Oldest 最早发布 +- C: Completed 已完成 +- OS: One Shots 短篇 +`, + handler, +}; + +type Type = 'L' | 'N' | 'O' | 'C' | 'OS'; + +const typeToText = { + L: '最近更新', + N: '最近发布', + O: '最早发布', + C: '已完成', + OS: '短篇', +}; + +async function handler(ctx) { + const tag = ctx.req.param('tag'); + const type = ctx.req.param('type') as Type; + + if (!type || !['L', 'N', 'O', 'C', 'OS'].includes(type)) { + throw new Error('无效的排序类型'); + } + const link = `https://www.asianfanfics.com/browse/tag/${tag}/${type}`; + + const response = await ofetch(link, { + headers: { + 'user-agent': config.trueUA, + Referer: 'https://www.asianfanfics.com/', + }, + }); + const $ = load(response); + + const items: DataItem[] = $('.primary-container .excerpt') + .toArray() + .filter((element) => { + const $element = $(element); + return $element.find('.excerpt__title a').length > 0; + }) + .map((element) => { + const $element = $(element); + const title = $element.find('.excerpt__title a').text(); + const link = 'https://www.asianfanfics.com' + $element.find('.excerpt__title a').attr('href'); + const author = $element.find('.excerpt__meta__name a').text().trim(); + const pubDate = parseDate($element.find('time').attr('datetime') || ''); + const description = $element.find('.excerpt__text').html(); + + return { + title, + link, + author, + pubDate, + description, + }; + }); + + return { + title: `Asianfanfics - 标签:${tag} - ${typeToText[type]}`, + link, + item: items, + }; +} diff --git a/lib/routes/asianfanfics/text-search.ts b/lib/routes/asianfanfics/text-search.ts new file mode 100644 index 00000000000000..d6b8fb0041a7f7 --- /dev/null +++ b/lib/routes/asianfanfics/text-search.ts @@ -0,0 +1,70 @@ +import { config } from '@/config'; +import { DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; + +// test url http://localhost:1200/asianfanfics/text-search/milklove + +export const route: Route = { + path: '/text-search/:keyword', + categories: ['reading'], + example: '/asianfanfics/text-search/milklove', + parameters: { + keyword: '关键词', + }, + name: '关键词', + maintainers: ['KazooTTT'], + radar: [ + { + source: ['www.asianfanfics.com/browse/text_search?q=:keyword'], + target: '/text-search/:keyword', + }, + ], + description: '匹配asianfanfics搜索关键词', + handler, +}; + +async function handler(ctx) { + const keyword = ctx.req.param('keyword'); + if (keyword.trim() === '') { + throw new Error('关键词不能为空'); + } + const link = `https://www.asianfanfics.com/browse/text_search?q=${keyword}+`; + + const response = await ofetch(link, { + headers: { + 'user-agent': config.trueUA, + }, + }); + const $ = load(response); + + const items: DataItem[] = $('.primary-container .excerpt') + .toArray() + .filter((element) => { + const $element = $(element); + return $element.find('.excerpt__title a').length > 0; + }) + .map((element) => { + const $element = $(element); + const title = $element.find('.excerpt__title a').text(); + const link = 'https://www.asianfanfics.com' + $element.find('.excerpt__title a').attr('href'); + const author = $element.find('.excerpt__meta__name a').text().trim(); + const pubDate = parseDate($element.find('time').attr('datetime') || ''); + const description = $element.find('.excerpt__text').html(); + + return { + title, + link, + author, + pubDate, + description, + }; + }); + + return { + title: `Asianfanfics - 关键词:${keyword}`, + link, + item: items, + }; +} diff --git a/lib/routes/asiantolick/namespace.ts b/lib/routes/asiantolick/namespace.ts index 277a3e0829dd54..21d7619ddb8cc9 100644 --- a/lib/routes/asiantolick/namespace.ts +++ b/lib/routes/asiantolick/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Asian to lick', url: 'asiantolick.com', + lang: 'zh-CN', }; diff --git a/lib/routes/asmr-200/index.ts b/lib/routes/asmr-200/index.ts new file mode 100644 index 00000000000000..a26b6762c45910 --- /dev/null +++ b/lib/routes/asmr-200/index.ts @@ -0,0 +1,66 @@ +import { Result, Work } from '@/routes/asmr-200/type'; +import { DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import path from 'node:path'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import timezone from '@/utils/timezone'; +import { getCurrentPath } from '@/utils/helpers'; + +const render = (work: Work, link: string) => art(path.join(getCurrentPath(import.meta.url), 'templates', 'work.art'), { work, link }); + +export const route: Route = { + path: '/works/:order?/:subtitle?/:sort?', + categories: ['multimedia'], + example: '/asmr-200/works', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + parameters: { + order: '排序字段,默认按照资源的收录日期来排序,详见下表', + sort: '排序方式,可选 `asc` 和 `desc` ,默认倒序', + subtitle: '筛选带字幕音频,可选 `0` 和 `1` ,默认关闭', + }, + radar: [ + { + source: ['asmr-200.com'], + target: 'asmr-200/works', + }, + ], + name: '最新收录', + maintainers: ['hualiong'], + url: 'asmr-200.com', + description: `| 发售日期 | 收录日期 | 销量 | 价格 | 评价 | 随机 | RJ号 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| release | create_date | dl_count | price | rate_average_2dp | random | id |`, + handler: async (ctx) => { + const { order = 'create_date', sort = 'desc', subtitle = '0' } = ctx.req.param(); + const res = await ofetch('https://api.asmr-200.com/api/works', { query: { order, sort, page: 1, subtitle } }); + + const items: DataItem[] = res.works.map((each) => { + const category = each.tags.map((tag) => tag.name); + each.category = category.join(','); + each.cv = each.vas.map((cv) => cv.name).join(','); + return { + title: each.title, + image: each.mainCoverUrl, + author: each.name, + link: `https://asmr-200.com/work/${each.source_id}`, + pubDate: timezone(parseDate(each.release, 'YYYY-MM-DD'), +8), + category, + description: render(each, `https://asmr-200.com/work/${each.source_id}`), + }; + }); + + return { + title: '最新收录 - ASMR Online', + link: 'https://asmr-200.com/', + item: items, + }; + }, +}; diff --git a/lib/routes/asmr-200/namespace.ts b/lib/routes/asmr-200/namespace.ts new file mode 100644 index 00000000000000..b447efa8abc1d4 --- /dev/null +++ b/lib/routes/asmr-200/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'ASMR Online', + url: 'asmr-200.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/asmr-200/templates/work.art b/lib/routes/asmr-200/templates/work.art new file mode 100644 index 00000000000000..759ad749f63a95 --- /dev/null +++ b/lib/routes/asmr-200/templates/work.art @@ -0,0 +1,7 @@ +{{ work.title }} +

    {{ work.title }} {{ work.source_id }}

    +

    发布者:{{ 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 + '

    ' + fullText : summary, + author: $('.author a', elem).text(), + }; }); - const summary = 'Reply: ' + $('.posts', elem).text(); - return { - link, - title: $('.subject a', elem).attr('title'), - pubDate: parseDate($('.lastpost .time', elem).text()), - description: fullText ? summary + '

    ' + fullText : summary, - author: $('.author a', elem).text(), - }; }) ); diff --git a/lib/routes/bangumi.tv/namespace.ts b/lib/routes/bangumi.tv/namespace.ts new file mode 100644 index 00000000000000..b978132087e1e7 --- /dev/null +++ b/lib/routes/bangumi.tv/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Bangumi 番组计划', + url: 'bangumi.tv', + lang: 'zh-CN', +}; diff --git a/lib/routes/bangumi.tv/other/followrank.ts b/lib/routes/bangumi.tv/other/followrank.ts new file mode 100644 index 00000000000000..08e6e0f2dd1a57 --- /dev/null +++ b/lib/routes/bangumi.tv/other/followrank.ts @@ -0,0 +1,69 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/:type/followrank', + categories: ['anime'], + example: '/bangumi.tv/anime/followrank', + parameters: { type: '类型:anime - 动画,book - 图书,music - 音乐,game - 游戏,real - 三次元' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['bgm.tv/:type'], + target: '/:type/followrank', + }, + ], + name: '成员关注榜', + maintainers: ['honue', 'zhoukuncheng', 'NekoAria'], + handler, +}; + +async function handler(ctx) { + const type = ctx.req.param('type'); + const url = `https://bgm.tv/${type}`; + + const response = await ofetch(url); + + const $ = load(response); + + const items = $('.featuredItems .mainItem') + .toArray() + .map((item) => { + const $item = $(item); + const link = 'https://bgm.tv' + $item.find('a').first().attr('href'); + const imageUrl = $item + .find('.image') + .attr('style') + ?.match(/url\((.*?)\)/)?.[1]; + const info = $item.find('small.grey').text(); + return { + title: $item.find('.title').text().trim(), + link, + description: `
    ${info}`, + }; + }); + + const RANK_TYPES = { + tv: '动画', + anime: '动画', + book: '图书', + music: '音乐', + game: '游戏', + real: '三次元', + }; + + return { + title: `BangumiTV 成员关注${RANK_TYPES[type]}榜`, + link: url, + item: items, + description: `BangumiTV 首页 - 成员关注${RANK_TYPES[type]}榜`, + }; +} diff --git a/lib/routes/bangumi/tv/person/index.ts b/lib/routes/bangumi.tv/person/index.ts similarity index 92% rename from lib/routes/bangumi/tv/person/index.ts rename to lib/routes/bangumi.tv/person/index.ts index 58cc88fef36a4f..d4e566f81e071a 100644 --- a/lib/routes/bangumi/tv/person/index.ts +++ b/lib/routes/bangumi.tv/person/index.ts @@ -1,12 +1,12 @@ 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: '/tv/person/:id', + path: '/person/:id', categories: ['anime'], - example: '/bangumi/tv/person/32943', + example: '/bangumi.tv/person/32943', parameters: { id: '人物 id, 在人物页面的地址栏查看' }, features: { requireConfig: false, @@ -30,7 +30,7 @@ async function handler(ctx) { // bangumi.tv未提供获取“人物信息”的API,因此仍需要通过抓取网页来获取 const personID = ctx.req.param('id'); const link = `https://bgm.tv/person/${personID}/works?sort=date`; - const { data: html } = await got(link); + const html = await ofetch(link); const $ = load(html); const personName = $('.nameSingle a').text(); const works = $('.item') diff --git a/lib/routes/bangumi/tv/subject/comments.ts b/lib/routes/bangumi.tv/subject/comments.ts similarity index 95% rename from lib/routes/bangumi/tv/subject/comments.ts rename to lib/routes/bangumi.tv/subject/comments.ts index 685a907c7c2d28..4f52ee191b469d 100644 --- a/lib/routes/bangumi/tv/subject/comments.ts +++ b/lib/routes/bangumi.tv/subject/comments.ts @@ -1,11 +1,11 @@ -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import { parseDate, parseRelativeDate } from '@/utils/parse-date'; const getComments = async (subjectID, minLength) => { // bangumi.tv未提供获取“吐槽(comments)”的API,因此仍需要通过抓取网页来获取 const link = `https://bgm.tv/subject/${subjectID}/comments`; - const { data: html } = await got(link); + const html = await ofetch(link); const $ = load(html); const title = $('.nameSingle').find('a').text(); const comments = $('.item') diff --git a/lib/routes/bangumi/tv/subject/ep.ts b/lib/routes/bangumi.tv/subject/ep.ts similarity index 73% rename from lib/routes/bangumi/tv/subject/ep.ts rename to lib/routes/bangumi.tv/subject/ep.ts index dcebbc3d12ff52..04d41847bc09b6 100644 --- a/lib/routes/bangumi/tv/subject/ep.ts +++ b/lib/routes/bangumi.tv/subject/ep.ts @@ -1,7 +1,7 @@ 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'; @@ -9,14 +9,8 @@ import { getLocalName } from './utils'; const getEps = async (subjectID, showOriginalName) => { const url = `https://api.bgm.tv/subject/${subjectID}?responseGroup=large`; - const { data: epsInfo } = await got(url); - const activeEps = []; - - for (const e of epsInfo.eps) { - if (e.status === 'Air') { - activeEps.push(e); - } - } + const epsInfo = await ofetch(url); + const activeEps = epsInfo.eps.filter((e) => e.status === 'Air'); return { title: getLocalName(epsInfo, showOriginalName), @@ -24,7 +18,7 @@ const getEps = async (subjectID, showOriginalName) => { description: epsInfo.summary, item: activeEps.map((e) => ({ title: `ep.${e.sort} ${getLocalName(e, showOriginalName)}`, - description: art(path.resolve(__dirname, '../../templates/tv/ep.art'), { + description: art(path.join(__dirname, '../templates/ep.art'), { e, epsInfo, }), diff --git a/lib/routes/bangumi/tv/subject/index.ts b/lib/routes/bangumi.tv/subject/index.ts similarity index 93% rename from lib/routes/bangumi/tv/subject/index.ts rename to lib/routes/bangumi.tv/subject/index.ts index 83416b2d83528e..9af2260bd05e8a 100644 --- a/lib/routes/bangumi/tv/subject/index.ts +++ b/lib/routes/bangumi.tv/subject/index.ts @@ -6,9 +6,9 @@ import { queryToBoolean } from '@/utils/readable-social'; import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { - path: '/tv/subject/:id/:type?/:showOriginalName?', + path: '/subject/:id/:type?/:showOriginalName?', categories: ['anime'], - example: '/bangumi/tv/subject/328609/ep/true', + example: '/bangumi.tv/subject/328609/ep/true', parameters: { id: '条目 id, 在条目页面的地址栏查看', type: '条目类型,可选值为 `ep`, `comments`, `blogs`, `topics`,默认为 `ep`', showOriginalName: '显示番剧标题原名,可选值 0/1/false/true,默认为 false' }, features: { requireConfig: false, @@ -27,9 +27,9 @@ export const route: Route = { name: '条目的通用路由格式', maintainers: ['JimenezLi'], handler, - description: `:::warning + description: `::: warning 此通用路由仅用于对路由参数的描述,具体信息请查看下方与条目相关的路由 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/bangumi/tv/subject/offcial-subject-api.ts b/lib/routes/bangumi.tv/subject/offcial-subject-api.ts similarity index 93% rename from lib/routes/bangumi/tv/subject/offcial-subject-api.ts rename to lib/routes/bangumi.tv/subject/offcial-subject-api.ts index be2b992ae3a1e9..cefadca81aa41c 100644 --- a/lib/routes/bangumi/tv/subject/offcial-subject-api.ts +++ b/lib/routes/bangumi.tv/subject/offcial-subject-api.ts @@ -1,4 +1,4 @@ -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import { getLocalName } from './utils'; @@ -17,7 +17,7 @@ const getFromAPI = (type) => { return async (subjectID, showOriginalName) => { // 官方提供的条目API文档见 https://github.com/bangumi/api/blob/3f3fa6390c468816f9883d24be488e41f8946159/docs-raw/Subject-API.md const url = `https://api.bgm.tv/subject/${subjectID}?responseGroup=large`; - const { data: subjectInfo } = await got(url); + const subjectInfo = await ofetch(url); return { title: `${getLocalName(subjectInfo, showOriginalName)}的 Bangumi ${mapping[type].cn}`, link: `https://bgm.tv/subject/${subjectInfo.id}/${mapping[type].en}`, diff --git a/lib/routes/bangumi/tv/subject/utils.ts b/lib/routes/bangumi.tv/subject/utils.ts similarity index 100% rename from lib/routes/bangumi/tv/subject/utils.ts rename to lib/routes/bangumi.tv/subject/utils.ts diff --git a/lib/routes/bangumi/templates/tv/ep.art b/lib/routes/bangumi.tv/templates/ep.art similarity index 100% rename from lib/routes/bangumi/templates/tv/ep.art rename to lib/routes/bangumi.tv/templates/ep.art diff --git a/lib/routes/bangumi.tv/templates/subject.art b/lib/routes/bangumi.tv/templates/subject.art new file mode 100644 index 00000000000000..089bf11f11286a --- /dev/null +++ b/lib/routes/bangumi.tv/templates/subject.art @@ -0,0 +1,6 @@ +{{ if routeSubjectType === 'all' }}类型:{{ subjectTypeName }}
    {{ /if }} +{{ if subjectType === 2 }}看到:{{ epStatus }} / {{ subjectEps ? subjectEps: '???' }}
    {{ /if }} +{{ if subjectType === 1 }}读到:{{ epStatus }} / {{ subjectEps ? subjectEps: '???' }}
    {{ /if }} +评分:{{ score }}
    +放送时间:{{ date ? date : '未知' }}
    + diff --git a/lib/routes/bangumi/templates/tv/today.art b/lib/routes/bangumi.tv/templates/today.art similarity index 100% rename from lib/routes/bangumi/templates/tv/today.art rename to lib/routes/bangumi.tv/templates/today.art diff --git a/lib/routes/bangumi/tv/user/blog.ts b/lib/routes/bangumi.tv/user/blog.ts similarity index 81% rename from lib/routes/bangumi/tv/user/blog.ts rename to lib/routes/bangumi.tv/user/blog.ts index b005291e1f12e4..da55b03efb58a8 100644 --- a/lib/routes/bangumi/tv/user/blog.ts +++ b/lib/routes/bangumi.tv/user/blog.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'; import timezone from '@/utils/timezone'; export const route: Route = { - path: '/tv/user/blog/:id', + path: '/user/blog/:id', categories: ['anime'], - example: '/bangumi/tv/user/blog/sai', + example: '/bangumi.tv/user/blog/sai', parameters: { id: '用户 id, 在用户页面地址栏查看' }, features: { requireConfig: false, @@ -22,6 +22,9 @@ export const route: Route = { { source: ['bgm.tv/user/:id'], }, + { + source: ['bangumi.tv/user/:id'], + }, ], name: '用户日志', maintainers: ['nczitzk'], @@ -30,11 +33,8 @@ export const route: Route = { async function handler(ctx) { const currentUrl = `https://bgm.tv/user/${ctx.req.param('id')}/blog`; - const response = await got({ - method: 'get', - url: currentUrl, - }); - const $ = load(response.data); + const response = await ofetch(currentUrl); + const $ = load(response); const list = $('#entry_list div.item') .find('h2.title') .toArray() @@ -51,8 +51,8 @@ async function handler(ctx) { const items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { - const res = await got({ method: 'get', url: item.link }); - const content = load(res.data); + const res = await ofetch(item.link); + const content = load(res); item.description = content('#entry_content').html(); return item; diff --git a/lib/routes/bangumi.tv/user/collections.ts b/lib/routes/bangumi.tv/user/collections.ts new file mode 100644 index 00000000000000..dc4676445161b4 --- /dev/null +++ b/lib/routes/bangumi.tv/user/collections.ts @@ -0,0 +1,186 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; +import { config } from '@/config'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +// 合并不同 subjectType 的 type 映射 +const getTypeNames = (subjectType) => { + const commonTypeNames = { + 1: '想看', + 2: '看过', + 3: '在看', + 4: '搁置', + 5: '抛弃', + }; + + switch (subjectType) { + case '1': // 书籍 + return { + 1: '想读', + 2: '读过', + 3: '在读', + 4: '搁置', + 5: '抛弃', + }; + case '2': // 动画 + case '6': // 三次元 + return commonTypeNames; + case '3': // 音乐 + return { + 1: '想听', + 2: '听过', + 3: '在听', + 4: '搁置', + 5: '抛弃', + }; + case '4': // 游戏 + return { + 1: '想玩', + 2: '玩过', + 3: '在玩', + 4: '搁置', + 5: '抛弃', + }; + default: + return commonTypeNames; // 默认使用通用的类型 + } +}; + +export const route: Route = { + path: '/user/collections/:id/:subjectType/:type', + categories: ['anime'], + example: '/bangumi.tv/user/collections/sai/1/1', + parameters: { + id: '用户 id, 在用户页面地址栏查看', + subjectType: { + description: '全部类别: `空`、book: `1`、anime: `2`、music: `3`、game: `4`、real: `6`', + options: [ + { value: 'ALL', label: 'all' }, + { value: 'book', label: '1' }, + { value: 'anime', label: '2' }, + { value: 'music', label: '3' }, + { value: 'game', label: '4' }, + { value: 'real', label: '6' }, + ], + }, + type: { + description: '全部类别: `空`、想看: `1`、看过: `2`、在看: `3`、搁置: `4`、抛弃: `5`', + options: [ + { value: 'ALL', label: 'all' }, + { value: '想看', label: '1' }, + { value: '看过', label: '2' }, + { value: '在看', label: '3' }, + { value: '搁置', label: '4' }, + { value: '抛弃', label: '5' }, + ], + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['bgm.tv/anime/list/:id'], + target: '/bangumi.tv/user/collections/:id/all/all', + }, + { + source: ['bangumi.tv/anime/list/:id'], + target: '/bangumi.tv/user/collections/:id/all/all', + }, + { + source: ['bgm.tv/anime/list/:id/wish'], + target: '/bangumi.tv/user/collections/:id/2/1', + }, + { + source: ['bangumi.tv/anime/list/:id/wish'], + target: '/bangumi.tv/user/collections/:id/2/1', + }, + ], + name: 'Bangumi 用户收藏列表', + maintainers: ['youyou-sudo', 'honue'], + handler, +}; + +async function handler(ctx) { + const userId = ctx.req.param('id'); + const subjectType = ctx.req.param('subjectType') || ''; + const type = ctx.req.param('type') || ''; + + const subjectTypeNames = { + 1: '书籍', + 2: '动画', + 3: '音乐', + 4: '游戏', + 6: '三次元', + }; + + const typeNames = getTypeNames(subjectType); + const typeName = typeNames[type] || ''; + const subjectTypeName = subjectTypeNames[subjectType] || ''; + + let descriptionFields = ''; + + if (typeName && subjectTypeName) { + descriptionFields = `${typeName}的${subjectTypeName}列表`; + } else if (typeName) { + descriptionFields = `${typeName}的列表`; + } else if (subjectTypeName) { + descriptionFields = `收藏的${subjectTypeName}列表`; + } else { + descriptionFields = '的Bangumi收藏列表'; + } + + const userDataUrl = `https://api.bgm.tv/v0/users/${userId}`; + const userData = await ofetch(userDataUrl, { + headers: { + 'User-Agent': config.trueUA, + }, + }); + + const collectionDataUrl = `https://api.bgm.tv/v0/users/${userId}/collections?${subjectType && subjectType !== 'all' ? `subject_type=${subjectType}` : ''}${type && type !== 'all' ? `&type=${type}` : ''}`; + const collectionData = await ofetch(collectionDataUrl, { + headers: { + 'User-Agent': config.trueUA, + }, + }); + + const userNickname = userData.nickname; + const items = collectionData.data.map((item) => { + const titles = item.subject.name_cn || item.subject.name; + const updateTime = item.updated_at; + const subjectId = item.subject_id; + + return { + title: `${type === 'all' ? `${getTypeNames(item.subject_type)[item.type]}:` : ''}${titles}`, + description: art(path.join(__dirname, '../templates/subject.art'), { + routeSubjectType: subjectType, + subjectTypeName: subjectTypeNames[item.subject_type], + subjectType: item.subject_type, + subjectEps: item.subject.eps, + epStatus: item.ep_status, + score: item.subject.score, + date: item.subject.date, + picUrl: item.subject.images.large, + }), + link: `https://bgm.tv/subject/${subjectId}`, + pubDate: timezone(parseDate(updateTime), 0), + }; + }); + return { + title: `${userNickname}${descriptionFields}`, + link: `https://bgm.tv/user/${userId}/collections`, + item: items, + description: `${userNickname}${descriptionFields}`, + }; +} diff --git a/lib/routes/bangumi/tv/other/followrank.ts b/lib/routes/bangumi/tv/other/followrank.ts deleted file mode 100644 index 5175ca9957b090..00000000000000 --- a/lib/routes/bangumi/tv/other/followrank.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import { config } from '@/config'; -export const route: Route = { - path: '/tv/followrank', - categories: ['anime'], - example: '/bangumi/tv/followrank', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['bgm.tv/anime'], - }, - ], - name: '成员关注动画榜', - maintainers: ['honue'], - handler, - url: 'bgm.tv/anime', -}; - -async function handler() { - const url = 'https://bgm.tv/anime'; - const response = await got({ - url, - method: 'get', - headers: { - 'User-Agent': config.trueUA, - }, - }); - - const $ = load(response.body); - - const items = [ - ...$('#columnB > div:nth-child(4) > table > tbody') - .find('tr') - .toArray() - .map((item) => { - const aTag = $(item).children('td').next().find('a'); - return { - title: aTag.html(), - link: 'https://bgm.tv' + aTag.attr('href'), - }; - }), - ...$('#chl_subitem > ul') - .find('li') - .toArray() - .map((item) => ({ - title: $(item).children('a').attr('title'), - link: 'https://bgm.tv' + $(item).children('a').attr('href'), - })), - ]; - - return { - title: 'Bangumi 成员关注动画榜', - link: url, - item: items, - description: `Bangumi 首页-成员关注动画榜`, - }; -} diff --git a/lib/routes/bangumi/tv/user/wish.ts b/lib/routes/bangumi/tv/user/wish.ts deleted file mode 100644 index 355f017d6892d7..00000000000000 --- a/lib/routes/bangumi/tv/user/wish.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; -import { config } from '@/config'; -export const route: Route = { - path: '/tv/user/wish/:id', - categories: ['anime'], - example: '/bangumi/tv/user/wish/sai', - parameters: { id: '用户 id, 在用户页面地址栏查看' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['bgm.tv/anime/list/:id/wish'], - }, - ], - name: '用户想看', - maintainers: ['honue'], - handler, -}; - -async function handler(ctx) { - const userid = ctx.req.param('id'); - const url = `https://bgm.tv/anime/list/${userid}/wish`; - const response = await got({ - url, - method: 'get', - headers: { - 'User-Agent': config.trueUA, - }, - }); - const $ = load(response.body); - - const username = $('.name').find('a').html(); - const items = $('#browserItemList') - .find('li') - .toArray() - .map((item) => { - const aTag = $(item).find('h3').children('a'); - const jdate = $(item).find('.collectInfo').find('span').html(); - return { - title: aTag.html(), - link: 'https://bgm.tv' + aTag.attr('href'), - pubDate: timezone(parseDate(jdate), 0), - }; - }); - - return { - title: `${username}想看的动画`, - link: url, - item: items, - description: `${username}想看的动画列表`, - }; -} diff --git a/lib/routes/baoyu/index.ts b/lib/routes/baoyu/index.ts new file mode 100644 index 00000000000000..5b101120889598 --- /dev/null +++ b/lib/routes/baoyu/index.ts @@ -0,0 +1,57 @@ +import { Route, DataItem } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import parser from '@/utils/rss-parser'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/blog', + categories: ['blog'], + example: '/baoyu/blog', + radar: [ + { + source: ['baoyu.io/'], + }, + ], + url: 'baoyu.io/', + name: 'Blog', + maintainers: ['liyaozhong'], + handler, + description: '宝玉 - 博客文章', +}; + +async function handler() { + const rootUrl = 'https://baoyu.io'; + const feedUrl = `${rootUrl}/feed.xml`; + + const feed = await parser.parseURL(feedUrl); + + const items = await Promise.all( + feed.items.map((item) => { + const link = item.link; + + return cache.tryGet(link as string, async () => { + const response = await got(link); + const $ = load(response.data); + + const container = $('.container'); + const content = container.find('.prose').html() || ''; + + return { + title: item.title, + description: content, + link, + pubDate: item.pubDate ? parseDate(item.pubDate) : undefined, + author: item.creator || '宝玉', + } as DataItem; + }); + }) + ); + + return { + title: '宝玉的博客', + link: rootUrl, + item: items, + }; +} diff --git a/lib/routes/baoyu/namespace.ts b/lib/routes/baoyu/namespace.ts new file mode 100644 index 00000000000000..6bc9da081013b2 --- /dev/null +++ b/lib/routes/baoyu/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '宝玉', + url: 'baoyu.io', + description: '宝玉的博客', + lang: 'zh-CN', +}; diff --git a/lib/routes/baozimh/namespace.ts b/lib/routes/baozimh/namespace.ts index bc361ba5decbda..e02b690b284249 100644 --- a/lib/routes/baozimh/namespace.ts +++ b/lib/routes/baozimh/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '包子漫画', url: 'www.baozimh.com', + lang: 'zh-CN', }; diff --git a/lib/routes/barronschina/index.ts b/lib/routes/barronschina/index.ts index 3050a00a9440e0..70339fbf7ceef8 100644 --- a/lib/routes/barronschina/index.ts +++ b/lib/routes/barronschina/index.ts @@ -28,9 +28,9 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'barronschina.com.cn/', - description: `:::tip + description: `::: tip 栏目 id 留空则返回快讯,在对应页地址栏 \`columnId=\` 后可以看到。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/barronschina/namespace.ts b/lib/routes/barronschina/namespace.ts index 9b5e286a6f5715..479ae25e61c868 100644 --- a/lib/routes/barronschina/namespace.ts +++ b/lib/routes/barronschina/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '巴伦周刊中文版', url: 'barronschina.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/bast/namespace.ts b/lib/routes/bast/namespace.ts index 4bc5c4878ab616..06cb4bbaa8df96 100644 --- a/lib/routes/bast/namespace.ts +++ b/lib/routes/bast/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京市科学技术协会', url: 'bast.net.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/bbc/index.ts b/lib/routes/bbc/index.ts index 88ce4dd780eb0b..5eff388ef49a13 100644 --- a/lib/routes/bbc/index.ts +++ b/lib/routes/bbc/index.ts @@ -4,11 +4,10 @@ import parser from '@/utils/rss-parser'; import { load } from 'cheerio'; import utils from './utils'; import ofetch from '@/utils/ofetch'; - export const route: Route = { path: '/:site?/:channel?', name: 'News', - maintainers: ['HenryQW', 'DIYgod'], + maintainers: ['HenryQW', 'DIYgod', 'pseudoyu'], handler, example: '/bbc/world-asia', parameters: { @@ -59,30 +58,51 @@ async function handler(ctx) { } const items = await Promise.all( - feed.items.map((item) => - cache.tryGet(item.link, async () => { - const response = await ofetch(item.link); - - const $ = load(response); - - const description = new URL(item.link).pathname.startsWith('/news/av') ? item.content : utils.ProcessFeed($); - - let section = 'sport'; - const urlSplit = item.link.split('/'); - const sectionSplit = urlSplit.at(-1).split('-'); - if (sectionSplit.length > 1) { - section = sectionSplit[0]; - } - section = section[0].toUpperCase() + section.slice(1); - - return { - title: `[${section}] ${item.title}`, - description, - pubDate: item.pubDate, - link: item.link, - }; - }) - ) + feed.items + .filter((item) => item && item.link) + .map((item) => + cache.tryGet(item.link, async () => { + try { + const linkURL = new URL(item.link); + if (linkURL.hostname === 'www.bbc.com') { + linkURL.hostname = 'www.bbc.co.uk'; + } + + const response = await ofetch(linkURL.href, { + retryStatusCodes: [403], + }); + + const $ = load(response); + + const path = linkURL.pathname; + + let description; + + switch (true) { + case path.startsWith('/sport'): + description = item.content; + break; + case path.startsWith('/sounds/play'): + description = item.content; + break; + case path.startsWith('/news/live'): + description = item.content; + break; + default: + description = utils.ProcessFeed($); + } + + return { + title: item.title || '', + description: description || '', + pubDate: item.pubDate || new Date().toUTCString(), + link: item.link, + }; + } catch { + return {} as Record; + } + }) + ) ); return { @@ -90,6 +110,6 @@ async function handler(ctx) { link, image: 'https://www.bbc.com/favicon.ico', description: title, - item: items, + item: items.filter((item) => Object.keys(item).length > 0), }; } diff --git a/lib/routes/bbc/namespace.ts b/lib/routes/bbc/namespace.ts index 59f6de83e38b74..4feae697921ae8 100644 --- a/lib/routes/bbc/namespace.ts +++ b/lib/routes/bbc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'BBC', url: 'bbc.com', + lang: 'en', }; diff --git a/lib/routes/bbcnewslabs/namespace.ts b/lib/routes/bbcnewslabs/namespace.ts index 5bb42bfa8686bb..97d970ec8f00e9 100644 --- a/lib/routes/bbcnewslabs/namespace.ts +++ b/lib/routes/bbcnewslabs/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'BBC News Labs', url: 'bbcnewslabs.co.uk', + lang: 'en', }; diff --git a/lib/routes/bc3ts/list.ts b/lib/routes/bc3ts/list.ts new file mode 100644 index 00000000000000..5b497e4c99a25d --- /dev/null +++ b/lib/routes/bc3ts/list.ts @@ -0,0 +1,72 @@ +import { Route } from '@/types'; + +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { Media, PostResponse } from './types'; +import { config } from '@/config'; + +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +export const route: Route = { + path: '/post/list/:sort?', + example: '/bc3ts/post/list', + parameters: { + sort: '排序方式,`1` 為最新,`2` 為熱門,默认為 `1`', + }, + features: { + antiCrawler: true, + }, + radar: [ + { + source: ['web.bc3ts.net'], + }, + ], + name: '動態', + maintainers: ['TonyRL'], + handler, +}; + +const baseUrl = 'https://web.bc3ts.net'; + +const renderMedia = (media: Media[]) => art(path.join(__dirname, 'templates', 'media.art'), { media }); + +async function handler(ctx) { + const { sort = '1' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + + const response = await ofetch('https://app.bc3ts.net/post/list/v2', { + headers: { + apikey: 'zlF+kaPfem%23we$2@90irpE*_RGjdw', + app_version: '3.0.28', + version: '2.0.0', + 'User-Agent': config.trueUA, + }, + query: { + limits: limit, + sort_type: sort, + }, + }); + + const items = response.data.map((p) => ({ + title: p.title ?? p.content.split('\n')[0], + description: p.content.replaceAll('\n', '
    ') + (p.media.length && renderMedia(p.media)), + link: `${baseUrl}/post/${p.id}`, + author: p.user.name, + pubDate: parseDate(p.created_time, 'x'), + category: p.group.name, + upvotes: p.like_count, + comments: p.comment_count, + })); + + return { + title: `爆料公社${sort === '1' ? '最新' : '熱門'}動態`, + link: baseUrl, + language: 'zh-TW', + image: 'https://img.bc3ts.net/image/web/main/logo-white-new-2023.png', + icon: 'https://img.bc3ts.net/image/web/main/logo/logo_icon_6th_2024_192x192.png', + item: items, + }; +} diff --git a/lib/routes/bc3ts/namespace.ts b/lib/routes/bc3ts/namespace.ts new file mode 100644 index 00000000000000..bbae0f8afa272f --- /dev/null +++ b/lib/routes/bc3ts/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '爆料公社', + url: 'web.bc3ts.net', + categories: ['new-media'], + lang: 'zh-CN', +}; diff --git a/lib/routes/bc3ts/templates/media.art b/lib/routes/bc3ts/templates/media.art new file mode 100644 index 00000000000000..a0e2992fd8f0a0 --- /dev/null +++ b/lib/routes/bc3ts/templates/media.art @@ -0,0 +1,10 @@ +
    +{{ each media m }} + {{ if m.type === 0 }} + {{ m.name }} + {{ else if m.type === 3 }} + + {{ /if }} +{{ /each }} diff --git a/lib/routes/bc3ts/types.ts b/lib/routes/bc3ts/types.ts new file mode 100644 index 00000000000000..ff20611f2d2f5e --- /dev/null +++ b/lib/routes/bc3ts/types.ts @@ -0,0 +1,118 @@ +interface Medal { + id: number; + name: string; + image: string; +} + +interface HeadFrame { + id: number; + image: string; +} + +interface UsingItem { + medal: Medal | null; + head_frame: HeadFrame | null; +} + +interface Relationship { + status: number; + that_status: number; +} + +interface User { + id: string; + name: string; + badge: any[]; + level: number; + using_item: UsingItem; + relationship: Relationship; + boom_verified: number; +} + +interface SafeSearch { + racy: string; + adult: string; + spoof: string; + medical: string; + violence: string; +} + +export interface Media { + id: number; + type: number; + name: string; + width: number; + height: number; + cover: string | null; + status: string; + safe_search: SafeSearch; + unlock_item: null; + media_url: string; +} + +interface Group { + id: number; + name: string; + type: number; + god_id: string; + status: number; + privacy: number; + layout_type: number; + share_status: number; + post_can_use_anonymous: boolean; + approve_user_permission_status: number[]; +} + +interface Reward { + money: number; + diamond: number; +} + +interface Post { + id: number; + user: User; + title: string | null; + content: string; + appendix: object; + created_time: number; + expired_time: number; + media: Media[]; + like: number; + like_count: number; + collection: number; + top: number; + reference_reply_count: number; + reference_share_count: number; + comment_count: number; + group: Group; + block_user: any[]; + tag_user_index: any[]; + reward: Reward; + reference: null; + latitude: number | null; + longitude: number | null; + unique_like_count: number; + unique_exposure_count: number; + unique_priority_point: number; + unique_priority_time: string | null; + safe_search: number; + unlock_type: null; + unlock_item: object; + is_unlocked: null; + visibility: number; + is_anonymous: number; + is_me: number; + comment_can_use_anonymous: number; + who_can_read: number; + poll: null; + activity: null; + poll_status: null; + is_cache: boolean; + is_editor: boolean; + theme_id: null; +} + +export interface PostResponse { + code: number; + data: Post[]; +} diff --git a/lib/routes/bdys/index.ts b/lib/routes/bdys/index.ts index 5c541d877cc8ab..4b0aa0106bf8f6 100644 --- a/lib/routes/bdys/index.ts +++ b/lib/routes/bdys/index.ts @@ -40,63 +40,63 @@ export const route: Route = { handler, description: `#### 资源分类 - | 不限 | 电影 | 电视剧 | - | ---- | ---- | ------ | - | all | 0 | 1 | +| 不限 | 电影 | 电视剧 | +| ---- | ---- | ------ | +| all | 0 | 1 | - #### 影视类型 +#### 影视类型 - | 不限 | 动作 | 爱情 | 喜剧 | 科幻 | 恐怖 | - | ---- | ------- | ------ | ---- | ------ | ------ | - | all | dongzuo | aiqing | xiju | kehuan | kongbu | +| 不限 | 动作 | 爱情 | 喜剧 | 科幻 | 恐怖 | +| ---- | ------- | ------ | ---- | ------ | ------ | +| all | dongzuo | aiqing | xiju | kehuan | kongbu | - | 战争 | 武侠 | 魔幻 | 剧情 | 动画 | 惊悚 | - | --------- | ----- | ------ | ------ | ------- | -------- | - | zhanzheng | wuxia | mohuan | juqing | donghua | jingsong | +| 战争 | 武侠 | 魔幻 | 剧情 | 动画 | 惊悚 | +| --------- | ----- | ------ | ------ | ------- | -------- | +| zhanzheng | wuxia | mohuan | juqing | donghua | jingsong | - | 3D | 灾难 | 悬疑 | 警匪 | 文艺 | 青春 | - | -- | ------ | ------ | ------- | ----- | -------- | - | 3D | zainan | xuanyi | jingfei | wenyi | qingchun | +| 3D | 灾难 | 悬疑 | 警匪 | 文艺 | 青春 | +| -- | ------ | ------ | ------- | ----- | -------- | +| 3D | zainan | xuanyi | jingfei | wenyi | qingchun | - | 冒险 | 犯罪 | 纪录 | 古装 | 奇幻 | 国语 | - | ------- | ------ | ---- | -------- | ------ | ----- | - | maoxian | fanzui | jilu | guzhuang | qihuan | guoyu | +| 冒险 | 犯罪 | 纪录 | 古装 | 奇幻 | 国语 | +| ------- | ------ | ---- | -------- | ------ | ----- | +| maoxian | fanzui | jilu | guzhuang | qihuan | guoyu | - | 综艺 | 历史 | 运动 | 原创压制 | - | ------ | ----- | ------- | ---------- | - | zongyi | lishi | yundong | yuanchuang | +| 综艺 | 历史 | 运动 | 原创压制 | +| ------ | ----- | ------- | ---------- | +| zongyi | lishi | yundong | yuanchuang | - | 美剧 | 韩剧 | 国产电视剧 | 日剧 | 英剧 | 德剧 | - | ----- | ----- | ---------- | ---- | ------ | ---- | - | meiju | hanju | guoju | riju | yingju | deju | +| 美剧 | 韩剧 | 国产电视剧 | 日剧 | 英剧 | 德剧 | +| ----- | ----- | ---------- | ---- | ------ | ---- | +| meiju | hanju | guoju | riju | yingju | deju | - | 俄剧 | 巴剧 | 加剧 | 西剧 | 意大利剧 | 泰剧 | - | ---- | ---- | ----- | ------- | -------- | ----- | - | eju | baju | jiaju | spanish | yidaliju | taiju | +| 俄剧 | 巴剧 | 加剧 | 西剧 | 意大利剧 | 泰剧 | +| ---- | ---- | ----- | ------- | -------- | ----- | +| eju | baju | jiaju | spanish | yidaliju | taiju | - | 港台剧 | 法剧 | 澳剧 | - | --------- | ---- | ---- | - | gangtaiju | faju | aoju | +| 港台剧 | 法剧 | 澳剧 | +| --------- | ---- | ---- | +| gangtaiju | faju | aoju | - #### 制片地区 +#### 制片地区 - | 大陆 | 中国香港 | 中国台湾 | - | ---- | -------- | -------- | +| 大陆 | 中国香港 | 中国台湾 | +| ---- | -------- | -------- | - | 美国 | 英国 | 日本 | 韩国 | 法国 | - | ---- | ---- | ---- | ---- | ---- | +| 美国 | 英国 | 日本 | 韩国 | 法国 | +| ---- | ---- | ---- | ---- | ---- | - | 印度 | 德国 | 西班牙 | 意大利 | 澳大利亚 | - | ---- | ---- | ------ | ------ | -------- | +| 印度 | 德国 | 西班牙 | 意大利 | 澳大利亚 | +| ---- | ---- | ------ | ------ | -------- | - | 比利时 | 瑞典 | 荷兰 | 丹麦 | 加拿大 | 俄罗斯 | - | ------ | ---- | ---- | ---- | ------ | ------ | +| 比利时 | 瑞典 | 荷兰 | 丹麦 | 加拿大 | 俄罗斯 | +| ------ | ---- | ---- | ---- | ------ | ------ | - #### 影视排序 +#### 影视排序 - | 更新时间 | 豆瓣评分 | - | -------- | -------- | - | 0 | 1 |`, +| 更新时间 | 豆瓣评分 | +| -------- | -------- | +| 0 | 1 |`, }; async function handler(ctx) { diff --git a/lib/routes/bdys/namespace.ts b/lib/routes/bdys/namespace.ts index d810d855cac1e5..78e9a01b3cdb1c 100644 --- a/lib/routes/bdys/namespace.ts +++ b/lib/routes/bdys/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '哔嘀影视', url: '52bdys.com', - description: `:::tip + description: `::: tip 哔嘀影视有多个备用域名,路由默认使用域名 \`https://bdys01.com\`。若该域名无法访问,可以通过在路由最后加上 \`?domain=<域名>\` 指定路由访问的域名。如指定备用域名为 \`https://bde4.icu\`,则在所有哔嘀影视路由最后加上 \`?domain=bde4.icu\` 即可,此时路由为 [\`/bdys?domain=bde4.icu\`](https://rsshub.app/bdys?domain=bde4.icu) :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/behance/namespace.ts b/lib/routes/behance/namespace.ts index 07fcc852a74427..f17e9309c66332 100644 --- a/lib/routes/behance/namespace.ts +++ b/lib/routes/behance/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Behance', url: 'www.behance.net', + lang: 'en', }; diff --git a/lib/routes/behance/queries.ts b/lib/routes/behance/queries.ts new file mode 100644 index 00000000000000..d8101162c0e3cc --- /dev/null +++ b/lib/routes/behance/queries.ts @@ -0,0 +1,1015 @@ +export const getProfileProjectsAndSelectionsQuery = `query GetProfileProjectsAndSections($username: String, $after: String) { + user(username: $username) { + hasPortfolio + profileSections { + ...profileSectionFields + } + profileProjects(first: 12, after: $after) { + pageInfo { + endCursor + hasNextPage + } + nodes { + __typename + adminFlags { + mature_lock + privacy_lock + dmca_lock + flagged_lock + privacy_violation_lock + trademark_lock + spam_lock + eu_ip_lock + } + canBeBoosted + colors { + r + g + b + } + covers { + size_202 { + url + } + size_404 { + url + } + size_808 { + url + } + } + features { + url + name + featuredOn + ribbon { + image + image2x + image3x + } + } + fields { + id + label + slug + url + } + hasMatureContent + id + isBoosted + isFeatured + isHiddenFromWorkTab + isMatureReviewSubmitted + isMonaReported + isOwner + isFounder + isPinnedToSubscriptionOverview + isPrivate + sourceFiles { + ...sourceFileWithCoverFields + } + matureAccess + modifiedOn + name + owners { + ...OwnerFields + images { + size_50 { + url + } + } + } + premium + publishedOn + privacyLevel + profileSectionId + stats { + appreciations { + all + } + views { + all + } + comments { + all + } + } + slug + url + } + } + } + viewer { + flags { + hasClickedOnAddProfileSectionButton + hasSeenProfilePortfolioUpsellModal + hasSeenCreatorProIntroModal + lastSeenMarketingPopupTimestamp + onboardedAsHirer + } + } + } + + fragment sourceFileWithCoverFields on SourceFile { + __typename + sourceFileId + projectId + userId + title + assetId + renditionUrl + mimeType + size + category + licenseType + unitAmount + currency + tier + hidden + extension + hasUserPurchased + description + cover { + coverUrl + coverX + coverY + coverScale + } + } + + fragment OwnerFields on User { + displayName + hasPremiumAccess + id + isFollowing + isProfileOwner + location + locationUrl + url + username + isMessageButtonVisible + availabilityInfo { + availabilityTimeline + isAvailableFullTime + isAvailableFreelance + hiringTimeline { + key + label + } + } + creatorPro { + isActive + initialSubscriptionDate + } + } + + fragment profileSectionFields on ProfileSection { + id + isDefault + name + order + projectCount + userId + }`; + +export const getAppreciatedQuery = `query GetAppreciatedProjects($username: String, $after: String) { + user(username: $username) { + appreciatedProjects(first: 24, after: $after) { + nodes { + __typename + colors { + r + g + b + } + covers { + size_202 { + url + } + size_404 { + url + } + size_808 { + url + } + } + slug + id + name + url + owners { + ...OwnerFields + images { + size_50 { + url + } + } + } + stats { + appreciations { + all + } + views { + all + } + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + } + fragment OwnerFields on User { + displayName + hasPremiumAccess + id + isFollowing + isProfileOwner + location + locationUrl + url + username + isMessageButtonVisible + availabilityInfo { + availabilityTimeline + isAvailableFullTime + isAvailableFreelance + hiringTimeline { + key + label + } + } + creatorPro { + isActive + initialSubscriptionDate + } + }`; + +export const getProjectPageQuery = `query ProjectPage($projectId: ProjectId!, $projectPassword: String) { + viewer { + ...Project_Viewer + } + project(id: $projectId) { + id + slug + premium + isPrivate + isOwner + canvasWidth + embedTag + url + stylesInline + ...Project_Project + ...EmbedShareModal_Project + creator { + hasPremiumAccess + } + owners { + id + username + displayName + } + allModules(projectPassword: $projectPassword) { + __typename + } + } + } + fragment Avatar_UserImageSizes on UserImageSizes { + ...AvatarImage_UserImageSizes + } + fragment Avatar_User on User { + id + url + images { + ...Avatar_UserImageSizes + } + creatorPro { + isActive + } + ...CreatorProBadge_User + } + fragment AvatarImage_UserImageSizes on UserImageSizes { + allAvailable { + url + width + type + } + } + fragment CreatorProBadge_User on User { + creatorPro { + initialSubscriptionDate + } + } + fragment EmbedShareModal_Project on Project { + ...EmbedShareProjectCover_Project + } + fragment Feature_ProjectFeature on ProjectFeature { + url + name + featuredOn + ribbon { + image + image2x + } + } + fragment HireOverlay_User on User { + id + firstName + isResponsiveToHiring + isMessageButtonVisible + ...Avatar_User + ...WideMessageButton_User + availabilityInfo { + hiringTimeline { + key + } + } + } + fragment ProjectInfoBox_User on User { + id + displayName + url + isFollowing + ...MultipleOwners_User + } + fragment ProjectInfoBox_Project on Project { + id + name + url + isOwner + covers { + allAvailable { + url + width + type + } + } + owners { + ...ProjectInfoBox_User + } + } + fragment SourceAssetsPane_SourceFile on SourceFile { + ...SourceFileRowsContainer_SourceFile + } + fragment SourceFileRowsContainer_SourceFile on SourceFile { + tier + hasUserPurchased + ...SourceFileRow_SourceFile + } + fragment UserInfo_User on User { + displayName + id + location + locationUrl + url + country + isProfileOwner + city + state + creatorPro { + isActive + } + ...Avatar_User + } + fragment UsersTooltip_User on User { + displayName + id + isFollowing + isProfileOwner + availabilityInfo { + isAvailableFullTime + isAvailableFreelance + } + ...UserInfo_User + } + fragment DominantColor_Colors on Colors { + r + g + b + } + fragment Tools_Tool on Tool { + id + title + url + backgroundImage { + size_original { + url + } + } + backgroundColor + synonym { + tagId + name + title + downloadUrl + iconUrl + } + } + fragment HireMeForm_UserAvailabilityInfo on UserAvailabilityInfo { + isAvailableFreelance + isAvailableFullTime + budgetMin + currency + compensationMin + hiringTimeline { + key + } + } + fragment MessageDialogManager_User on User { + id + displayName + username + images { + ...AvatarImage_UserImageSizes + } + creatorPro { + isActive + } + ...SendRegularMessagePane_User + ...ServicesPane_User + } + fragment SendRegularMessagePane_User on User { + displayName + availabilityInfo { + ...HireMeForm_UserAvailabilityInfo + } + } + fragment ServicesPane_User on User { + id + displayName + ...InquireServiceModal_User + ...ViewServiceInfoModal_User + } + fragment MessageManager_User on User { + id + isMessageButtonVisible + displayName + username + ...MessageDialogManager_User + availabilityInfo { + availabilityTimeline + isAvailableFullTime + isAvailableFreelance + hiringTimeline { + key + } + } + } + fragment WideMessageButton_User on User { + firstName + ...MessageManager_User + } + fragment AppreciationNotification_Viewer on Viewer { + url + } + fragment Avatar_Project_User on User { + id + ...Avatar_User + } + fragment ImageElement_ImageModule on ImageModule { + id + altText + height + width + fullBleed + imageSizes { + allAvailable(type: [JPG, WEBP]) { + url + width + height + type + } + size_max_158 { + url + } + } + } + fragment Module_ProjectModule on ProjectModule { + ...Embed_ProjectModule + ... on AudioModule { + id + caption + captionAlignment + } + ... on VideoModule { + id + caption + captionAlignment + } + ... on EmbedModule { + id + caption + captionAlignment + } + ... on ImageModule { + id + ...ThreeD_ImageModule + ...SingleImage_ImageModule + ...Image_ImageModule + } + ... on MediaCollectionModule { + id + caption: captionPlain + captionAlignment + components { + ...Actions_MediaCollectionComponent + filename + flexHeight + flexWidth + height + width + id + imageSizes { + allAvailable(type: [JPG, WEBP]) { + url + width + height + type + } + } + } + fullBleed + } + ... on TextModule { + id + text + alignment + } + } + fragment MultipleOwners_User on User { + ...UsersTooltip_User + } + fragment MultipleOwners_Project on Project { + id + name + } + fragment Project_Owners_User on User { + id + displayName + username + firstName + url + isFollowing + isMessageButtonVisible + isCreatorPro + creatorPro { + isActive + } + ...UsersTooltip_User + ...MultipleOwners_User + ...Avatar_Project_User + ...Avatar_User + ...HireOverlay_User + } + fragment Project_Creator_User on User { + id + isProfileOwner + isFollowing + hasPremiumAccess + hasAllowEmbeds + url + ...PaneHeader_User + } + fragment Project_ProjectCoverImageSizes on ProjectCoverImageSizes { + allAvailable { + url + width + type + } + } + fragment Project_Tool on Tool { + ...Tools_Tool + } + fragment Project_SourceFile on SourceFile { + ...SourceFileRowsContainer_SourceFile + ...SourceFilesProjectOverlay_SourceFile + ...ProjectExtras_SourceFile + } + fragment Project_ProjectFeature on ProjectFeature { + ...Feature_ProjectFeature + } + fragment Project_Project on Project { + id + slug + name + url + description + tags { + id + title + } + privacyLevel + matureAccess + canvasWidth + isOwner + hasMatureContent + isPrivate + publishedOn + canBeAddedToMoodboard + isMonaReported + isAppreciated + projectCTA { + ctaType + link { + description + url + title + } + isDefaultCTA + } + ...MultipleOwners_Project + ...ProjectInfoBox_Project + ...ProjectLightbox_Project + ...ProjectExtras_Project + covers { + ...Project_ProjectCoverImageSizes + } + sourceFiles { + ...Project_SourceFile + } + creator { + ...Project_Creator_User + } + owners { + ...Project_Owners_User + } + tools { + ...Project_Tool + } + allModules(projectPassword: $projectPassword) { + ... on AudioModule { + id + fullBleed + caption + } + ... on EmbedModule { + id + caption + } + ... on ImageModule { + id + fullBleed + caption + imageSizes { + size_disp { + url + } + size_disp_still { + url + } + } + ...Actions_ImageModule + } + ... on MediaCollectionModule { + id + fullBleed + caption: captionPlain + components { + id + imageSizes { + size_disp { + url + } + size_disp_still { + url + } + } + ...Actions_MediaCollectionComponent + } + } + ... on TextModule { + id + fullBleed + } + ... on VideoModule { + id + fullBleed + caption + } + ...Module_ProjectModule + } + aeroData { + externalUrl + } + adminNotices { + title + body + isReviewable + } + license { + license + } + features { + ...Project_ProjectFeature + } + stats { + appreciations { + all + } + views { + all + } + } + styles { + spacing { + projectTopMargin + } + } + colors { + r + g + b + } + } + fragment Project_Viewer on Viewer { + stats { + appreciations + } + pulsePoints { + displayFollow + displayAppreciate + } + flags { + hasSeenCreatorProIntroModal + onboardedAsHirer + } + creatorPro { + isActive + } + createdOn + ...AppreciationNotification_Viewer + ...ProjectExtras_Viewer + } + fragment ProjectComments_Viewer on Viewer { + ...ProjectCommentInput_Viewer + id + } + fragment ProjectCommentInput_Viewer on Viewer { + url + images { + size_50 { + url + } + } + } + fragment ProjectExtras_SourceFile on SourceFile { + ...ProjectInfo_SourceFile + } + fragment ProjectExtras_Project on Project { + isCommentingAllowed + } + fragment ProjectExtras_Viewer on Viewer { + ...ProjectInfo_Viewer + } + fragment ProjectInfo_SourceFile on SourceFile { + ...SourceAssetsPane_SourceFile + } + fragment ProjectInfo_Viewer on Viewer { + id + ...ProjectComments_Viewer + } + fragment ProjectLightbox_Project on Project { + id + slug + isOwner + ...ProjectInfoBox_Project + } + fragment SourceFilesProjectOverlay_SourceFile on SourceFile { + hasUserPurchased + ...SourceFileRow_SourceFile + } + fragment Video_VideoDisplay on VideoModule { + captionPlain + embed + height + id + width + } + fragment Actions_ImageModule on ImageModule { + id + hasCaiData + projectId + src + width + projectId + exifData { + lens { + ...Actions_exifDataValue + } + software { + ...Actions_exifDataValue + } + makeAndModel { + ...Actions_exifDataValue + } + focalLength { + ...Actions_exifDataValue + } + iso { + ...Actions_exifDataValue + } + location { + ...Actions_exifDataValue + } + flash { + ...Actions_exifDataValue + } + exposureMode { + ...Actions_exifDataValue + } + shutterSpeed { + ...Actions_exifDataValue + } + aperture { + ...Actions_exifDataValue + } + } + } + fragment Actions_exifDataValue on exifDataValue { + id + searchValue + label + value + } + fragment Actions_MediaCollectionComponent on MediaCollectionComponent { + id + } + fragment Audio_AudioModule on AudioModule { + captionPlain + embed + fullBleed + id + } + fragment Embed_ProjectModule on ProjectModule { + ... on EmbedModule { + ...ExternalEmbed_EmbedModule + } + ... on AudioModule { + isDoneProcessing + ...Audio_AudioModule + } + ... on VideoModule { + isDoneProcessing + ...Video_VideoModule + } + } + fragment ExternalEmbed_EmbedModule on EmbedModule { + captionPlain + fluidEmbed + embedModuleFullBleed: fullBleed + height + id + originalEmbed + originalHeight + originalWidth + width + widthUnit + } + fragment Image_ImageModule on ImageModule { + id + caption + captionAlignment + fullBleed + height + width + altText + ...Actions_ImageModule + ...ImageElement_ImageModule + } + fragment SingleImage_ImageModule on ImageModule { + id + caption + captionAlignment + fullBleed + height + width + ...Actions_ImageModule + ...ImageElement_ImageModule + } + fragment ThreeD_ImageModule on ImageModule { + id + altText + threeDData { + iframeUrl + } + } + fragment Video_VideoModule on VideoModule { + fullBleed + id + ...Video_VideoDisplay + } + fragment Avatar_EmbedFragment on User { + id + images { + allAvailable { + url + width + } + } + displayName + } + fragment EmbedShareProjectCover_SourceFile on SourceFile { + ...PaidAndFreeAssetsCountBadge_Embed_SourceFile + } + fragment EmbedShareProjectCover_User on User { + ...Avatar_EmbedFragment + } + fragment EmbedShareProjectCover_Project on Project { + id + isPrivate + isPublished + hasMatureContent + creator { + hasAllowEmbeds + } + colors { + ...DominantColor_Colors + } + sourceFiles { + ...EmbedShareProjectCover_SourceFile + } + name + url + covers { + allAvailable { + url + width + type + } + size_original_webp { + url + width + type + } + } + owners { + url + ...EmbedShareProjectCover_User + } + } + fragment PaidAndFreeAssetsCountBadge_Embed_SourceFile on SourceFile { + unitAmount + tier + hidden + } + fragment ViewServiceInfoModal_User on User { + id + displayName + url + images { + size_50 { + url + } + } + creatorPro { + isActive + } + ...CreatorProBadge_User + } + fragment InquireServiceModal_User on User { + id + displayName + images { + size_50 { + url + } + } + availabilityInfo { + hiringTimeline { + key + } + } + } + fragment SourceFileRow_SourceFile on SourceFile { + assetId + cover { + coverUrl + } + title + extension + currency + unitAmount + renditionUrl + hasUserPurchased + tier + sourceFileId + projectId + mimeType + category + licenseType + size + } + fragment PaneHeader_User on User { + id + url + displayName + images { + allAvailable { + width + url + type + } + } + }`; diff --git a/lib/routes/behance/templates/description.art b/lib/routes/behance/templates/description.art new file mode 100644 index 00000000000000..698a6ff6b4c254 --- /dev/null +++ b/lib/routes/behance/templates/description.art @@ -0,0 +1,23 @@ +{{ if description.length }} + {{ description }}
    +{{ /if }} + +{{ each modules module }} + {{ if module.__typename === 'ImageModule' }} +
    + {{ module.altText }} + {{ if module.caption.length }}
    {{ module.caption }}
    {{ /if }} +
    + {{ else if module.__typename === 'TextModule' }} + {{@ module.text }} + {{ else if module.__typename === 'MediaCollectionModule' }} + {{ each module.components comp }} + + {{ /each }} + {{ else if module.__typename === 'EmbedModule' }} + {{@ module.fluidEmbed || module.originalEmbed }} + {{ else }} + UNHANDLED MODULE: {{ module.__typename }} + {{ /if }} +
    +{{ /each }} diff --git a/lib/routes/behance/user.ts b/lib/routes/behance/user.ts index 35e61a4dc4dca7..f8eb132eb0f9b2 100644 --- a/lib/routes/behance/user.ts +++ b/lib/routes/behance/user.ts @@ -1,14 +1,30 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import crypto from 'node:crypto'; +import path from 'node:path'; +import { art } from '@/utils/render'; +import { getCurrentPath } from '@/utils/helpers'; +import { getAppreciatedQuery, getProfileProjectsAndSelectionsQuery, getProjectPageQuery } from './queries'; +const __dirname = getCurrentPath(import.meta.url); export const route: Route = { path: '/:user/:type?', - categories: ['design'], + categories: ['design', 'popular'], + view: ViewType.Pictures, example: '/behance/mishapetrick', - parameters: { user: 'username', type: 'type, `projects` or `appreciated`, `projects` by default' }, + parameters: { + user: 'username', + type: { + description: 'type', + options: [ + { value: 'projects', label: 'projects' }, + { value: 'appreciated', label: 'appreciated' }, + ], + default: 'projects', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -23,66 +39,84 @@ export const route: Route = { description: `Behance user's profile URL, like [https://www.behance.net/mishapetrick](https://www.behance.net/mishapetrick) the username will be \`mishapetrick\`。`, }; +const getUserProfile = async (nodes, user) => + (await cache.tryGet(`behance:profile:${user}`, () => { + const profile = nodes.flatMap((item) => item.owners).find((owner) => owner.username === user); + + return Promise.resolve({ + displayName: profile.displayName, + id: profile.id, + link: profile.url, + image: profile.images.size_50.url.replace('/user/50/', '/user/source/'), + }); + })) as { displayName: string; id: string; link: string; image: string }; + async function handler(ctx) { - const user = ctx.req.param('user') ?? ''; - const type = ctx.req.param('type') ?? 'projects'; + const { user, type = 'projects' } = ctx.req.param(); + + const uuid = crypto.randomUUID(); + const headers = { + Cookie: `gk_suid=${Math.random().toString().substring(2, 10)}, gki=; originalReferrer=; bcp=${uuid}`, + 'X-BCP': uuid, + 'X-Requested-With': 'XMLHttpRequest', + }; - const response = await got({ - method: 'get', - url: `https://www.behance.net/${user}/${type}`, // 接口只获取12个项目 - headers: { - 'X-Requested-With': 'XMLHttpRequest', + const response = await ofetch('https://www.behance.net/v3/graphql', { + method: 'POST', + headers, + body: { + query: type === 'projects' ? getProfileProjectsAndSelectionsQuery : getAppreciatedQuery, + variables: { + username: user, + after: '', + }, }, }); - const data = response.data; - let list; - if (type === 'projects') { - list = data.profile.activeSection.work.projects.slice(0, 12); - } - if (type === 'appreciated') { - list = data.profile.activeSection.appreciations.appreciations.slice(0, 12); - } - const articledata = await Promise.all( - list.map(async (item) => { - if (type === 'appreciated') { - item = item.project; - } - const url = `${item.url}?ilo0=1`; - const description = await cache.tryGet(url, async () => { - const response2 = await got({ - method: 'get', - url, + + const nodes = type === 'projects' ? response.data.user.profileProjects.nodes : response.data.user.appreciatedProjects.nodes; + const list = nodes.map((item) => ({ + title: item.name, + link: item.url, + author: item.owners.map((owner) => owner.displayName).join(', '), + image: item.covers.size_202.url.replace('/202/', '/source/'), + pubDate: item.publishedOn ? parseDate(item.publishedOn, 'X') : undefined, + category: item.fields?.map((field) => field.label.toLowerCase()), + projectId: item.id, + })); + + const profile = await getUserProfile(nodes, user); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch('https://www.behance.net/v3/graphql', { + method: 'POST', + headers, + body: { + query: getProjectPageQuery, + variables: { + projectId: item.projectId, + }, + }, }); - const articleHtml = response2.data; - const $2 = load(articleHtml); - $2('.ImageElement-root-kir').remove(); - $2('.embed-dimensions').remove(); - $2('script.js-lightbox-slide-content').each((_, elem) => { - elem = $2(elem); - elem.replaceWith(elem.html()); + const project = response.data.project; + + item.description = art(path.join(__dirname, 'templates/description.art'), { + description: project.description, + modules: project.allModules, }); - const content = $2('div.project-styles').html(); - const single = { - content, - }; - return single; - }); - return description; - }) + item.category = [...new Set([...(item.category || []), ...(project.tags?.map((tag) => tag.title.toLowerCase()) || [])])]; + item.pubDate = item.pubDate || (project.publishedOn ? parseDate(project.publishedOn, 'X') : undefined); + + return item; + }) + ) ); + return { - title: `${data.profile.owner.first_name} ${data.profile.owner.last_name}'s ${type}`, - link: data.profile.owner.url, - item: list.map((item, index) => { - if (type === 'appreciated') { - item = item.project; - } - return { - title: item.name, - description: articledata[index].content, - link: item.url, - pubDate: parseDate(item.published_on * 1000), - }; - }), + title: `${profile.displayName}'s ${type}`, + link: `https://www.behance.net/${user}/${type}`, + image: profile.image, + item: items, }; } diff --git a/lib/routes/beijingprice/index.ts b/lib/routes/beijingprice/index.ts new file mode 100644 index 00000000000000..8004d50b567c60 --- /dev/null +++ b/lib/routes/beijingprice/index.ts @@ -0,0 +1,188 @@ +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 = 'jgzx/xwzx' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + + const rootUrl = 'https://www.beijingprice.cn'; + const currentUrl = new URL(category.endsWith('/') ? category : `${category}/`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('div.jgzx.rightcontent ul li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const a = item.find('a'); + const link = a.prop('href'); + const msg = a.prop('msg'); + + const title = a.text()?.trim() ?? a.prop('title'); + + let enclosureUrl; + let enclosureType; + + if (msg) { + const parsedMsg = JSON.parse(msg); + enclosureUrl = new URL(`${parsedMsg.path}${parsedMsg.fileName}`, rootUrl).href; + enclosureType = `application/${parsedMsg.suffix}`; + } + + return { + title, + pubDate: parseDate(item.contents().last().text()), + link: enclosureUrl ?? (link.startsWith('http') ? link : new URL(link, rootUrl).href), + language, + enclosure_url: enclosureUrl, + enclosure_type: enclosureType, + enclosure_title: enclosureUrl ? title : undefined, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + if (!item.link.includes('www.beijingprice.cn') || item.link.endsWith('.pdf')) { + return item; + } + + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const title = $$('p.title').text().trim(); + const description = $$('div.news-content').html(); + const fromSplits = $$('p.from') + .text() + .split(/发布时间:/); + + item.title = title; + item.description = description; + item.pubDate = fromSplits?.length === 0 ? item.pubDate : parseDate(fromSplits?.pop() ?? '', 'YYYY年MM月DD日'); + item.category = $$('div.map a') + .toArray() + .map((c) => $$(c).text()) + .slice(1); + item.author = fromSplits?.[0]?.replace(/来源:/, '') ?? undefined; + item.content = { + html: description, + text: $$('div.news-content').text(), + }; + item.language = language; + + return item; + }) + ) + ); + + const image = new URL($('a.header-logo img').prop('src'), rootUrl).href; + + return { + title: $('title').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: 'beijingprice.cn', + maintainers: ['nczitzk'], + handler, + example: '/beijingprice/jgzx/xwzx', + parameters: { category: '分类,默认为 `jgzx/xwzx` 即新闻资讯,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [新闻资讯](https://www.beijingprice.cn/jgzx/xwzx/),网址为 \`https://www.beijingprice.cn/jgzx/xwzx/\`。截取 \`https://beijingprice.cn/\` 到末尾 \`/\` 的部分 \`jgzx/xwzx\` 作为参数填入,此时路由为 [\`/beijingprice/jgzx/xwzx\`](https://rsshub.app/beijingprice/jgzx/xwzx)。 +::: + +#### [价格资讯](https://www.beijingprice.cn/jgzx/xwzx/) + +| [新闻资讯](https://www.beijingprice.cn/jgzx/xwzx/) | [工作动态](https://www.beijingprice.cn/jgzx/gzdt/) | [各区动态](https://www.beijingprice.cn/jgzx/gqdt/) | [通知公告](https://www.beijingprice.cn/jgzx/tzgg/) | [价格早报](https://www.beijingprice.cn/jgzx/jgzb/) | +| ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | +| [jgzx/xwzx](https://rsshub.app/beijingprice/jgzx/xwzx) | [jgzx/gzdt](https://rsshub.app/beijingprice/jgzx/gzdt) | [jgzx/gqdt](https://rsshub.app/beijingprice/jgzx/gqdt) | [jgzx/tzgg](https://rsshub.app/beijingprice/jgzx/tzgg) | [jgzx/jgzb](https://rsshub.app/beijingprice/jgzx/jgzb) | + +#### [综合信息](https://www.beijingprice.cn/zhxx/cbjs/) + +| [价格听证](https://www.beijingprice.cn/zhxx/jgtz/) | [价格监测定点单位名单](https://www.beijingprice.cn/zhxx/jgjcdddwmd/) | [部门预算决算](https://www.beijingprice.cn/bmys/) | +| ------------------------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------- | +| [zhxx/jgtz](https://rsshub.app/beijingprice/zhxx/jgtz) | [zhxx/jgjcdddwmd](https://rsshub.app/beijingprice/zhxx/jgjcdddwmd) | [bmys](https://rsshub.app/beijingprice/bmys) | + `, + categories: ['government'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['beijingprice.cn/:category?'], + target: (params) => { + const category = params.category; + + return `/beijingprice${category ? `/${category}` : ''}`; + }, + }, + { + title: '价格资讯 - 新闻资讯', + source: ['beijingprice.cn/jgzx/xwzx/'], + target: '/jgzx/xwzx', + }, + { + title: '价格资讯 - 工作动态', + source: ['beijingprice.cn/jgzx/gzdt/'], + target: '/jgzx/gzdt', + }, + { + title: '价格资讯 - 各区动态', + source: ['beijingprice.cn/jgzx/gqdt/'], + target: '/jgzx/gqdt', + }, + { + title: '价格资讯 - 通知公告', + source: ['beijingprice.cn/jgzx/tzgg/'], + target: '/jgzx/tzgg', + }, + { + title: '价格资讯 - 价格早报', + source: ['beijingprice.cn/jgzx/jgzb/'], + target: '/jgzx/jgzb', + }, + { + title: '综合信息 - 价格听证', + source: ['beijingprice.cn/zhxx/jgtz/'], + target: '/zhxx/jgtz', + }, + { + title: '综合信息 - 价格监测定点单位名单', + source: ['beijingprice.cn/zhxx/jgjcdddwmd/'], + target: '/zhxx/jgjcdddwmd', + }, + { + title: '综合信息 - 部门预算决算', + source: ['beijingprice.cn/bmys/'], + target: '/bmys', + }, + ], +}; diff --git a/lib/routes/beijingprice/namespace.ts b/lib/routes/beijingprice/namespace.ts new file mode 100644 index 00000000000000..60a18476ab8541 --- /dev/null +++ b/lib/routes/beijingprice/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '北京价格', + url: 'beijingprice.cn', + categories: ['government'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/bellroy/namespace.ts b/lib/routes/bellroy/namespace.ts index de98f41da2a80d..3833468b3e8b0a 100644 --- a/lib/routes/bellroy/namespace.ts +++ b/lib/routes/bellroy/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Bellroy', url: 'bellroy.com', + lang: 'zh-CN', }; diff --git a/lib/routes/bendibao/namespace.ts b/lib/routes/bendibao/namespace.ts index 62a7d9153c93b8..808d8f7abb7222 100644 --- a/lib/routes/bendibao/namespace.ts +++ b/lib/routes/bendibao/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '本地宝', url: 'bendibao.com', + lang: 'zh-CN', }; diff --git a/lib/routes/bendibao/news.ts b/lib/routes/bendibao/news.ts index bca4a37467f281..0f5404bcf99df7 100644 --- a/lib/routes/bendibao/news.ts +++ b/lib/routes/bendibao/news.ts @@ -30,11 +30,11 @@ export const route: Route = { handler, url: 'bendibao.com/', description: `| 城市名 | 缩写 | - | ------ | ---- | - | 北京 | bj | - | 上海 | sh | - | 广州 | gz | - | 深圳 | sz | +| ------ | ---- | +| 北京 | bj | +| 上海 | sh | +| 广州 | gz | +| 深圳 | sz | 更多城市请参见 [这里](http://www.bendibao.com/city.htm) diff --git a/lib/routes/bestblogs/feeds.ts b/lib/routes/bestblogs/feeds.ts new file mode 100644 index 00000000000000..d4509e646bccf3 --- /dev/null +++ b/lib/routes/bestblogs/feeds.ts @@ -0,0 +1,107 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/feeds/:category?', + categories: ['programming'], + example: '/bestblogs/feeds/featured', + parameters: { category: 'the category of articles. Can be `programming`, `ai`, `product`, `business` or `featured`. Default is `featured`' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '文章列表', + maintainers: ['zhenlohuang'], + handler, +}; + +class APIRequest { + keyword?: string; + qualifiedFilter: string; + sourceId?: string; + category?: string; + timeFilter: string; + language: string; + userLanguage: string; + sortType: string; + currentPage: number; + pageSize: number; + + constructor({ keyword = '', qualifiedFilter = 'true', sourceId = '', category = '', timeFilter = '1w', language = 'all', userLanguage = 'zh', sortType = 'default', currentPage = 1, pageSize = 10 } = {}) { + this.keyword = keyword; + this.qualifiedFilter = qualifiedFilter; + this.sourceId = sourceId; + this.category = category; + this.timeFilter = timeFilter; + this.language = language; + this.userLanguage = userLanguage; + this.sortType = sortType; + this.currentPage = currentPage; + this.pageSize = pageSize; + } + + toJson(): string { + const requestBody = { + keyword: this.keyword, + qualifiedFilter: this.qualifiedFilter, + sourceId: this.sourceId, + category: this.category, + timeFilter: this.timeFilter, + language: this.language, + userLanguage: this.userLanguage, + sortType: this.sortType, + currentPage: this.currentPage, + pageSize: this.pageSize, + }; + + return JSON.stringify(requestBody); + } +} + +async function handler(ctx) { + const defaultPageSize = 100; + const defaultTimeFilter = '1w'; + const { category = 'featured' } = ctx.req.param(); + + const apiRequest = new APIRequest({ + category, + pageSize: defaultPageSize, + qualifiedFilter: category === 'featured' ? 'true' : 'false', + timeFilter: defaultTimeFilter, + }); + + const apiUrl = 'https://api.bestblogs.dev/api/resource/list'; + const response = await ofetch(apiUrl, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: apiRequest.toJson(), + }); + + if (!response || !response.data || !response.data.dataList) { + throw new Error('Invalid API response: ' + JSON.stringify(response)); + } + + const articles = response.data.dataList; + + const items = articles.map((article) => ({ + title: article.title, + link: article.url, + description: article.summary, + pubDate: parseDate(article.publishDateTimeStr), + author: Array.isArray(article.authors) ? article.authors.map((author) => ({ name: author })) : [{ name: article.authors }], + category: article.category, + })); + + return { + title: `Bestblogs.dev`, + link: `https://www.bestblogs.dev/feeds`, + item: items, + }; +} diff --git a/lib/routes/bestblogs/namespace.ts b/lib/routes/bestblogs/namespace.ts new file mode 100644 index 00000000000000..0b54f592a5533a --- /dev/null +++ b/lib/routes/bestblogs/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'bestblogs.dev', + url: 'www.bestblogs.dev', + lang: 'zh-CN', +}; diff --git a/lib/routes/bgmlist/namespace.ts b/lib/routes/bgmlist/namespace.ts index 306f9a9c6f3896..ebbdd5c6af3409 100644 --- a/lib/routes/bgmlist/namespace.ts +++ b/lib/routes/bgmlist/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '番组放送', url: 'bgmlist.com', + lang: 'zh-CN', }; diff --git a/lib/routes/bigquant/collections.ts b/lib/routes/bigquant/collections.ts index 2dc716dec1d80b..f0a009959861c7 100644 --- a/lib/routes/bigquant/collections.ts +++ b/lib/routes/bigquant/collections.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; import MarkdownIt from 'markdown-it'; @@ -8,7 +8,8 @@ const md = MarkdownIt({ export const route: Route = { path: '/collections', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/bigquant/collections', parameters: {}, features: { diff --git a/lib/routes/bigquant/namespace.ts b/lib/routes/bigquant/namespace.ts index ededdee2d71c67..7176b5aa86d711 100644 --- a/lib/routes/bigquant/namespace.ts +++ b/lib/routes/bigquant/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'BigQuant', url: 'bigquant.com', + lang: 'zh-CN', }; diff --git a/lib/routes/bilibili/api-interface.d.ts b/lib/routes/bilibili/api-interface.d.ts index eae9f0e4084471..89b2038ee55741 100644 --- a/lib/routes/bilibili/api-interface.d.ts +++ b/lib/routes/bilibili/api-interface.d.ts @@ -28,6 +28,7 @@ interface Generalspec { render_spec: Renderspec; size_spec: Containersize; } +/* eslint-disable-next-line @typescript-eslint/no-empty-object-type */ interface AVATARLAYER {} interface Webcssstyle { borderRadius: string; @@ -186,7 +187,15 @@ interface Richtextnode { type: string; jump_url?: string; emoji?: Emoji; + pics?: Pic2[]; } +interface Pic2 { + height: number; + size: number; + src: string; + width: number; +} + interface Desc { rich_text_nodes: Richtextnode[]; text: string; diff --git a/lib/routes/bilibili/article.ts b/lib/routes/bilibili/article.ts index 6bed24998b030e..1c9980d0e1b406 100644 --- a/lib/routes/bilibili/article.ts +++ b/lib/routes/bilibili/article.ts @@ -1,8 +1,10 @@ import { Route } from '@/types'; import got from '@/utils/got'; import cache from './cache'; - +import cacheGeneral from '@/utils/cache'; +import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; + export const route: Route = { path: '/user/article/:uid', categories: ['social-media'], @@ -21,8 +23,8 @@ export const route: Route = { source: ['space.bilibili.com/:uid'], }, ], - name: 'UP 主专栏', - maintainers: ['lengthmin', 'Qixingchen'], + name: 'UP 主图文', + maintainers: ['lengthmin', 'Qixingchen', 'hyoban'], handler, }; @@ -31,24 +33,45 @@ async function handler(ctx) { const name = await cache.getUsernameFromUID(uid); const response = await got({ method: 'get', - url: `https://api.bilibili.com/x/space/article?mid=${uid}&pn=1&ps=10&sort=publish_time&jsonp=jsonp`, + url: `https://api.bilibili.com/x/polymer/web-dynamic/v1/opus/feed/space?host_mid=${uid}`, headers: { - Referer: `https://space.bilibili.com/${uid}/`, + Referer: `https://space.bilibili.com/${uid}/article`, }, }); const data = response.data.data; - const title = `${name} 的 bilibili 专栏`; + const title = `${name} 的 bilibili 图文`; const link = `https://space.bilibili.com/${uid}/article`; - const description = `${name} 的 bilibili 专栏`; + const description = `${name} 的 bilibili 图文`; + const cookie = await cache.getCookie(); + const item = await Promise.all( - data.articles.map(async (item) => { - const { url: art_url, description: eDescription } = await cache.getArticleDataFromCvid(item.id, uid); - const publishDate = parseDate(item.publish_time * 1000); + data.items.map(async (item) => { + const link = 'https:' + item.jump_url; + const data = await cacheGeneral.tryGet( + link, + async () => + ( + await got({ + method: 'get', + url: link, + headers: { + Referer: `https://space.bilibili.com/${uid}/article`, + Cookie: cookie, + }, + }) + ).data + ); + + const $ = load(data as string); + const description = $('.opus-module-content').html(); + const pubDate = $('.opus-module-author__pub__text').text().replace('编辑于 ', ''); + const single = { - title: item.title, - link: art_url, - description: eDescription, - pubDate: publishDate, + title: item.content, + link, + description: description || item.content, + // 2019年11月11日 08:50 + pubDate: pubDate ? parseDate(pubDate, 'YYYY年MM月DD日 HH:mm') : undefined, }; return single; }) diff --git a/lib/routes/bilibili/bangumi.ts b/lib/routes/bilibili/bangumi.ts index 1f23bdd42cf801..55deb289635469 100644 --- a/lib/routes/bilibili/bangumi.ts +++ b/lib/routes/bilibili/bangumi.ts @@ -1,65 +1,68 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; +import { Data, DataItem, Route, ViewType } from '@/types'; +import cache from '@/utils/cache'; +import { EpisodeResult } from './types'; +import utils from './utils'; export const route: Route = { - path: '/bangumi/media/:mediaid', + path: '/bangumi/media/:mediaid/:embed?', name: '番剧', parameters: { mediaid: '番剧媒体 id, 番剧主页 URL 中获取', + embed: '默认为开启内嵌视频, 任意值为关闭', }, example: '/bilibili/bangumi/media/9192', - categories: ['social-media'], - maintainers: ['DIYgod'], + categories: ['social-media', 'popular'], + view: ViewType.Videos, + maintainers: ['DIYgod', 'nuomi1'], handler, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportRadar: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, }; async function handler(ctx) { - let seasonid = ctx.req.param('seasonid'); - const mediaid = ctx.req.param('mediaid'); + const mediaId = ctx.req.param('mediaid'); + const embed = !ctx.req.param('embed'); - let mediaData; - if (mediaid) { - const response = await got({ - method: 'get', - url: `https://www.bilibili.com/bangumi/media/md${mediaid}`, - }); - mediaData = JSON.parse(response.data.match(/window\.__INITIAL_STATE__=([\S\s]+);\(function\(\)/)[1]) || {}; - seasonid = mediaData.mediaInfo.season_id; - } - const { data } = await got.get(`https://api.bilibili.com/pgc/web/season/section?season_id=${seasonid}`); + const mediaData = await utils.getBangumi(mediaId, cache); + const seasonId = String(mediaData.season_id); + const seasonData = await utils.getBangumiItems(seasonId, cache); + + const episodes: DataItem[] = []; + + const getEpisode = (item: EpisodeResult, title: string) => + ({ + title, + description: utils.renderOGVDescription(embed, item.cover, item.long_title, seasonId, String(item.id)), + link: item.share_url, + image: item.cover.replace('http://', 'https://'), + language: 'zh-cn', + }) as DataItem; - let episodes = []; - if (data.result.main_section && data.result.main_section.episodes) { - episodes = [ - ...episodes, - ...data.result.main_section.episodes.map((item) => ({ - title: `第${item.title}话 ${item.long_title}`, - description: ``, - link: `https://www.bilibili.com/bangumi/play/ep${item.id}`, - })), - ]; + for (const item of seasonData.main_section.episodes) { + const episode = getEpisode(item, `第${item.title}话 ${item.long_title}`); + episodes.push(episode); } - if (data.result.section) { - for (const section of data.result.section) { - if (section.episodes) { - episodes = [ - ...episodes, - ...section.episodes.map((item) => ({ - title: `${item.title} ${item.long_title}`, - description: ``, - link: `https://www.bilibili.com/bangumi/play/ep${item.id}`, - })), - ]; - } + for (const section of seasonData.section) { + for (const item of section.episodes) { + const episode = getEpisode(item, `${item.title} ${item.long_title}`); + episodes.push(episode); } } return { - title: mediaData?.mediaInfo.title, - link: `https://www.bilibili.com/bangumi/media/md${mediaData?.mediaInfo.media_id}/`, - image: mediaData?.mediaInfo.cover, - description: mediaData?.mediaInfo.evaluate, + title: mediaData.title, + description: mediaData.evaluate, + link: mediaData.share_url, item: episodes, - }; + image: mediaData.cover.replace('http://', 'https://'), + language: 'zh-cn', + } as Data; } diff --git a/lib/routes/bilibili/bilibili-recommend.ts b/lib/routes/bilibili/bilibili-recommend.ts new file mode 100644 index 00000000000000..c6c93f0bfcd940 --- /dev/null +++ b/lib/routes/bilibili/bilibili-recommend.ts @@ -0,0 +1,42 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import utils from './utils'; + +export const route: Route = { + path: '/precious/:embed?', + categories: ['social-media'], + example: '/bilibili/precious', + parameters: { embed: '默认为开启内嵌视频, 任意值为关闭' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '入站必刷', + maintainers: ['liuyuhe666'], + handler, +}; + +async function handler(ctx) { + const embed = !ctx.req.param('embed'); + const response = await got({ + method: 'get', + url: 'https://api.bilibili.com/x/web-interface/popular/precious', + headers: { + Referer: 'https://www.bilibili.com/v/popular/history', + }, + }); + const data = response.data.data.list; + return { + title: '哔哩哔哩入站必刷', + link: 'https://www.bilibili.com/v/popular/history', + item: data.map((item) => ({ + title: item.title, + description: utils.renderUGCDescription(embed, item.pic, item.desc || item.title, item.aid, undefined, item.bvid), + link: item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, + })), + }; +} diff --git a/lib/routes/bilibili/cache.ts b/lib/routes/bilibili/cache.ts index 01f58714b6b1e1..0bd54c3bac9ac7 100644 --- a/lib/routes/bilibili/cache.ts +++ b/lib/routes/bilibili/cache.ts @@ -5,21 +5,36 @@ import { load } from 'cheerio'; import { config } from '@/config'; import logger from '@/utils/logger'; import puppeteer from '@/utils/puppeteer'; +import { JSDOM } from 'jsdom'; + +const disableConfigCookie = false; -let disableConfigCookie = false; const getCookie = () => { if (!disableConfigCookie && Object.keys(config.bilibili.cookies).length > 0) { + // Update b_lsid in cookies + for (const key of Object.keys(config.bilibili.cookies)) { + const cookie = config.bilibili.cookies[key]; + if (cookie) { + const updatedCookie = cookie.replace(/b_lsid=[0-9A-F]+_[0-9A-F]+/, `b_lsid=${utils.lsid()}`); + config.bilibili.cookies[key] = updatedCookie; + } + } + return config.bilibili.cookies[Object.keys(config.bilibili.cookies)[Math.floor(Math.random() * Object.keys(config.bilibili.cookies).length)]]; } const key = 'bili-cookie'; return cache.tryGet(key, async () => { - const browser = await puppeteer(); + const browser = await puppeteer({ + stealth: true, + }); const page = await browser.newPage(); const waitForRequest = new Promise((resolve) => { page.on('requestfinished', async (request) => { if (request.url() === 'https://api.bilibili.com/x/internal/gaia-gateway/ExClimbWuzhi') { const cookies = await page.cookies(); - const cookieString = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; '); + let cookieString = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; '); + + cookieString = cookieString.replace(/b_lsid=[0-9A-F]+_[0-9A-F]+/, `b_lsid=${utils.lsid()}`); resolve(cookieString); } }); @@ -32,9 +47,24 @@ const getCookie = () => { }); }; -const clearCookie = () => { - cache.set('bili-cookie'); - disableConfigCookie = true; +const getRenderData = (uid) => { + const key = 'bili-web-render-data'; + return cache.tryGet(key, async () => { + const cookie = await getCookie(); + const { data: response } = await got(`https://space.bilibili.com/${uid}`, { + headers: { + Referer: 'https://www.bilibili.com/', + Cookie: cookie, + }, + }); + const dom = new JSDOM(response); + const document = dom.window.document; + const scriptElement = document.querySelector('#__RENDER_DATA__'); + const innerText = scriptElement ? scriptElement.textContent || '{}' : '{}'; + const renderData = JSON.parse(decodeURIComponent(innerText)); + const accessId = renderData.access_id; + return accessId; + }); }; const getWbiVerifyString = () => { @@ -106,13 +136,9 @@ const getUsernameAndFaceFromUID = async (uid) => { if (!name || !face) { const cookie = await getCookie(); const wbiVerifyString = await getWbiVerifyString(); - // await got(`https://space.bilibili.com/${uid}/`, { - // headers: { - // Referer: `https://www.bilibili.com/`, - // Cookie: cookie, - // }, - // }); - const params = utils.addWbiVerifyInfo(`mid=${uid}&token=&platform=web&web_location=1550101`, wbiVerifyString); + const dmImgList = utils.getDmImgList(); + const renderData = await getRenderData(uid); + const params = utils.addWbiVerifyInfo(utils.addRenderData(utils.addDmVerifyInfo(`mid=${uid}&token=&platform=web&web_location=1550101`, dmImgList), renderData), wbiVerifyString); const { data: nameResponse } = await got(`https://api.bilibili.com/x/space/wbi/acc/info?${params}`, { headers: { Referer: `https://space.bilibili.com/${uid}/`, @@ -143,15 +169,15 @@ const getLiveIDFromShortID = (shortID) => { }); }; -const getUsernameFromLiveID = (liveID) => { - const key = `bili-username-from-liveID-${liveID}`; +const getUserInfoFromLiveID = (liveID) => { + const key = `bili-userinfo-from-liveID-${liveID}`; return cache.tryGet(key, async () => { const { data: nameResponse } = await got(`https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room?roomid=${liveID}`, { headers: { Referer: `https://live.bilibili.com/${liveID}`, }, }); - return nameResponse.data.info.uname; + return nameResponse.data.info; }); }; @@ -250,14 +276,14 @@ const getArticleDataFromCvid = async (cvid, uid) => { export default { getCookie, - clearCookie, getWbiVerifyString, getUsernameFromUID, getUsernameAndFaceFromUID, getLiveIDFromShortID, - getUsernameFromLiveID, + getUserInfoFromLiveID, getVideoNameFromId, getCidFromId, getAidFromBvid, getArticleDataFromCvid, + getRenderData, }; diff --git a/lib/routes/bilibili/coin.ts b/lib/routes/bilibili/coin.ts index 1c4c77a9b78190..e2a19b82b092f4 100644 --- a/lib/routes/bilibili/coin.ts +++ b/lib/routes/bilibili/coin.ts @@ -5,10 +5,10 @@ import utils from './utils'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: '/user/coin/:uid/:disableEmbed?', + path: '/user/coin/:uid/:embed?', categories: ['social-media'], example: '/bilibili/user/coin/208259', - parameters: { uid: '用户 id, 可在 UP 主主页中找到', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { uid: '用户 id, 可在 UP 主主页中找到', embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: false, requirePuppeteer: false, @@ -30,7 +30,7 @@ export const route: Route = { async function handler(ctx) { const uid = ctx.req.param('uid'); - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const name = await cache.getUsernameFromUID(uid); @@ -51,7 +51,7 @@ async function handler(ctx) { description: `${name} 的 bilibili 投币视频`, item: data.map((item) => ({ title: item.title, - description: `${item.desc}${disableEmbed ? '' : `

    ${utils.iframe(item.aid)}`}
    `, + description: utils.renderUGCDescription(embed, item.pic, item.desc, item.aid, undefined, item.bvid), pubDate: parseDate(item.time * 1000), link: item.time > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, author: item.owner.name, diff --git a/lib/routes/bilibili/danmaku.ts b/lib/routes/bilibili/danmaku.ts index 997e54dafa2aa0..b89be0d48f7fc4 100644 --- a/lib/routes/bilibili/danmaku.ts +++ b/lib/routes/bilibili/danmaku.ts @@ -54,7 +54,7 @@ async function handler(ctx) { let danmakuText = danmakuResponse.body; - danmakuText = await ((danmakuText[0] & 0x0f) === 0x08 ? zlib.inflateSync(danmakuText) : zlib.inflateRawSync(danmakuText)); + danmakuText = await ((danmakuText[0] & 0x0F) === 0x08 ? zlib.inflateSync(danmakuText) : zlib.inflateRawSync(danmakuText)); let danmakuList = []; const $ = load(danmakuText, { xmlMode: true }); diff --git a/lib/routes/bilibili/dynamic.ts b/lib/routes/bilibili/dynamic.ts index 52bbb9ad52f9a3..e936325219f564 100644 --- a/lib/routes/bilibili/dynamic.ts +++ b/lib/routes/bilibili/dynamic.ts @@ -1,8 +1,8 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; import JSONbig from 'json-bigint'; -import utils from './utils'; +import utils, { getLiveUrl, getVideoUrl } from './utils'; import { parseDate } from '@/utils/parse-date'; import { fallback, queryToBoolean } from '@/utils/readable-social'; import cacheIn from './cache'; @@ -10,9 +10,23 @@ import { BilibiliWebDynamicResponse, Item2, Modules } from './api-interface'; export const route: Route = { path: '/user/dynamic/:uid/:routeParams?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/bilibili/user/dynamic/2267573', - parameters: { uid: '用户 id, 可在 UP 主主页中找到', routeParams: '额外参数;请参阅以下说明和表格' }, + parameters: { + uid: '用户 id, 可在 UP 主主页中找到', + routeParams: ` +| 键 | 含义 | 接受的值 | 默认值 | +| ---------- | --------------------------------- | -------------- | ------ | +| showEmoji | 显示或隐藏表情图片 | 0/1/true/false | false | +| embed | 默认开启内嵌视频 | 0/1/true/false | true | +| useAvid | 视频链接使用 AV 号 (默认为 BV 号) | 0/1/true/false | false | +| directLink | 使用内容直链 | 0/1/true/false | false | +| hideGoods | 隐藏带货动态 | 0/1/true/false | false | +| offset | 偏移状态 | string | "" | + +用例:\`/bilibili/user/dynamic/2267573/showEmoji=1&embed=0&useAvid=1\``, + }, features: { requireConfig: [ { @@ -26,7 +40,7 @@ export const route: Route = { }, ], requirePuppeteer: false, - antiCrawler: true, + antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, @@ -40,20 +54,6 @@ export const route: Route = { name: 'UP 主动态', maintainers: ['DIYgod', 'zytomorrow', 'CaoMeiYouRen', 'JimenezLi'], handler, - description: `| 键 | 含义 | 接受的值 | 默认值 | - | ------------ | --------------------------------- | -------------- | ------ | - | showEmoji | 显示或隐藏表情图片 | 0/1/true/false | false | - | disableEmbed | 关闭内嵌视频 | 0/1/true/false | false | - | useAvid | 视频链接使用 AV 号 (默认为 BV 号) | 0/1/true/false | false | - | directLink | 使用内容直链 | 0/1/true/false | false | - - 用例:\`/bilibili/user/dynamic/2267573/showEmoji=1&disableEmbed=1&useAvid=1\` - - :::tip 动态的专栏显示全文 - 动态的专栏显示全文请使用通用参数里的 \`mode=fulltext\` - - 举例: bilibili 专栏全文输出 /bilibili/user/dynamic/2267573/?mode=fulltext - :::`, }; const getTitle = (data: Modules): string => { @@ -109,16 +109,17 @@ const getDes = (data: Modules): string => { const getOriginTitle = (data?: Modules) => data && getTitle(data); const getOriginDes = (data?: Modules) => data && getDes(data); const getOriginName = (data?: Modules) => data?.module_author?.name; -const getIframe = (data?: Modules, disableEmbed: boolean = false) => { - if (disableEmbed) { +const getIframe = (data?: Modules, embed: boolean = true) => { + if (!embed) { return ''; } const aid = data?.module_dynamic?.major?.archive?.aid; const bvid = data?.module_dynamic?.major?.archive?.bvid; - if (!aid) { + if (aid === undefined && bvid === undefined) { return ''; } - return utils.iframe(aid, null, bvid); + // 不通过 utils.renderUGCDescription 渲染 img/description 以兼容其他格式的动态 + return utils.renderUGCDescription(embed, '', '', aid, undefined, bvid); }; const getImgs = (data?: Modules) => { @@ -147,7 +148,10 @@ const getImgs = (data?: Modules) => { if (major[type]?.cover) { imgUrls.push(major[type].cover); } - return imgUrls.map((url) => ``).join(''); + return imgUrls + .filter(Boolean) + .map((url) => ``) + .join(''); }; const getUrl = (item?: Item2, useAvid = false) => { @@ -157,6 +161,7 @@ const getUrl = (item?: Item2, useAvid = false) => { } let url = ''; let text = ''; + let videoPageUrl; const major = data.module_dynamic?.major; if (!major) { return null; @@ -175,6 +180,7 @@ const getUrl = (item?: Item2, useAvid = false) => { const id = useAvid ? `av${archive?.aid}` : archive?.bvid; url = `https://www.bilibili.com/video/${id}`; text = `视频地址:${url}`; + videoPageUrl = getVideoUrl(archive?.bvid); break; } case 'MAJOR_TYPE_COMMON': @@ -211,6 +217,7 @@ const getUrl = (item?: Item2, useAvid = false) => { case 'MAJOR_TYPE_LIVE_RCMD': { const live_play_info = JSON.parse(major.live_rcmd?.content || '{}')?.live_play_info; url = `https://live.bilibili.com/${live_play_info?.room_id}`; + videoPageUrl = getLiveUrl(live_play_info?.room_id); text = `直播间地址:${url}`; break; } @@ -220,6 +227,7 @@ const getUrl = (item?: Item2, useAvid = false) => { return { url, text, + videoPageUrl, }; }; @@ -227,21 +235,17 @@ async function handler(ctx) { const uid = ctx.req.param('uid'); const routeParams = Object.fromEntries(new URLSearchParams(ctx.req.param('routeParams'))); const showEmoji = fallback(undefined, queryToBoolean(routeParams.showEmoji), false); - const disableEmbed = fallback(undefined, queryToBoolean(routeParams.disableEmbed), false); + const embed = fallback(undefined, queryToBoolean(routeParams.embed), true); const displayArticle = ctx.req.query('mode') === 'fulltext'; + const offset = fallback(undefined, routeParams.offset, ''); const useAvid = fallback(undefined, queryToBoolean(routeParams.useAvid), false); const directLink = fallback(undefined, queryToBoolean(routeParams.directLink), false); + const hideGoods = fallback(undefined, queryToBoolean(routeParams.hideGoods), false); const cookie = await cacheIn.getCookie(); - const response = await got({ - method: 'get', - url: `https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space`, - searchParams: { - host_mid: uid, - platform: 'web', - features: 'itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote', - }, + const params = utils.addDmVerifyInfo(`offset=${offset}&host_mid=${uid}&platform=web&features=itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote`, utils.getDmImgList()); + const response = await got(`https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?${params}`, { headers: { Referer: `https://space.bilibili.com/${uid}/`, Cookie: cookie, @@ -249,8 +253,7 @@ async function handler(ctx) { }); const body = JSONbig.parse(response.body); if (body?.code === -352) { - cacheIn.clearCookie(); - throw new Error('The cookie has expired, please try again.'); + throw new Error('Request failed, please try again.'); } const items = (body as BilibiliWebDynamicResponse)?.data?.items; @@ -261,86 +264,134 @@ async function handler(ctx) { cache.set(`bili-userface-from-uid-${uid}`, face); const rssItems = await Promise.all( - items.map(async (item) => { - // const parsed = JSONbig.parse(item.card); - - const data = item.modules; - const origin = item?.orig?.modules; + items + .filter((item) => { + if (hideGoods) { + return item.modules.module_dynamic?.additional?.type !== 'ADDITIONAL_TYPE_GOODS'; + } + return true; + }) + .map(async (item) => { + // const parsed = JSONbig.parse(item.card); - // link - let link = ''; - if (item.id_str) { - link = `https://t.bilibili.com/${item.id_str}`; - } + const data = item.modules; + const origin = item?.orig?.modules; - let description = getDes(data) || ''; - const title = getTitle(data) || description; // 没有 title 的时候使用 desc 填充 + // link + let link = ''; + if (item.id_str) { + link = `https://t.bilibili.com/${item.id_str}`; + } - // emoji - if (data.module_dynamic?.desc?.rich_text_nodes?.length && showEmoji) { - const nodes = data.module_dynamic?.desc?.rich_text_nodes; - for (const node of nodes) { - if (node?.emoji) { - const emoji = node.emoji; - description = description.replaceAll( - emoji.text, - `${emoji.text}` - ); + let description = getDes(data) || ''; + const title = getTitle(data) || description; // 没有 title 的时候使用 desc 填充 + const category: string[] = []; + // emoji + if (data.module_dynamic?.desc?.rich_text_nodes?.length) { + const nodes = data.module_dynamic.desc.rich_text_nodes; + for (const node of nodes) { + // 处理 emoji 的情况 + if (showEmoji && node?.emoji) { + const emoji = node.emoji; + description = description.replaceAll( + emoji.text, + `${emoji.text}` + ); + } + // 处理转发带图评论的情况 + if (node?.pics?.length) { + const { pics, text } = node; + description = description.replaceAll( + text, + pics + .map( + (pic) => + `${text}` + ) + .join('
    ') + ); + } + if (node?.type === 'RICH_TEXT_NODE_TYPE_TOPIC') { + // 将话题作为 category + category.push(node.text.match(/#(\S+)#/)?.[1] || ''); + } } } - } - if (item.type === 'DYNAMIC_TYPE_ARTICLE' && displayArticle) { - // 抓取专栏全文 - const cvid = data.module_dynamic?.major?.opus?.jump_url?.match?.(/cv(\d+)/)?.[0]; - if (cvid) { - description = (await cacheIn.getArticleDataFromCvid(cvid, uid)).description || ''; + if (data.module_dynamic?.major?.opus?.summary?.rich_text_nodes?.length) { + const nodes = data.module_dynamic.major.opus.summary.rich_text_nodes; + for (const node of nodes) { + if (node?.type === 'RICH_TEXT_NODE_TYPE_TOPIC') { + // 将话题作为 category + category.push(node.text.match(/#(\S+)#/)?.[1] || ''); + } + } + } + if (data.module_dynamic?.topic?.name) { + // 将话题作为 category + category.push(data.module_dynamic.topic.name); } - } - const urlResult = getUrl(item, useAvid); - const urlText = urlResult?.text; - if (urlResult && directLink) { - link = urlResult.url; - } + if (item.type === 'DYNAMIC_TYPE_ARTICLE' && displayArticle) { + // 抓取专栏全文 + const cvid = data.module_dynamic?.major?.opus?.jump_url?.match?.(/cv(\d+)/)?.[0]; + if (cvid) { + description = (await cacheIn.getArticleDataFromCvid(cvid, uid)).description || ''; + } + } - const originUrlResult = getUrl(item?.orig, useAvid); - const originUrlText = originUrlResult?.text; - if (originUrlResult && directLink) { - link = originUrlResult.url; - } + const urlResult = getUrl(item, useAvid); + const urlText = urlResult?.text; + if (urlResult && directLink) { + link = urlResult.url; + } - let originDescription = ''; - const originName = getOriginName(origin); - const originTitle = getOriginTitle(origin); - const originDes = getOriginDes(origin); - if (originName) { - originDescription += `//转发自: @${getOriginName(origin)}: `; - } - if (originTitle) { - originDescription += originTitle; - } - if (originDes) { - originDescription += `
    ${originDes}`; - } + const originUrlResult = getUrl(item?.orig, useAvid); + const originUrlText = originUrlResult?.text; + if (originUrlResult && directLink) { + link = originUrlResult.url; + } - // 换行处理 - description = description.replaceAll('\r\n', '
    ').replaceAll('\n', '
    '); - originDescription = originDescription.replaceAll('\r\n', '
    ').replaceAll('\n', '
    '); + let originDescription = ''; + const originName = getOriginName(origin); + const originTitle = getOriginTitle(origin); + const originDes = getOriginDes(origin); + if (originName) { + originDescription += `//转发自: @${getOriginName(origin)}: `; + } + if (originTitle) { + originDescription += originTitle; + } + if (originDes) { + originDescription += `
    ${originDes}`; + } - const descriptions = [description, originDescription, urlText, originUrlText, getIframe(data, disableEmbed), getIframe(origin, disableEmbed), getImgs(data), getImgs(origin)] - .filter(Boolean) - .map((e) => e?.trim()) - .join('
    '); + // 换行处理 + description = description.replaceAll('\r\n', '
    ').replaceAll('\n', '
    '); + originDescription = originDescription.replaceAll('\r\n', '
    ').replaceAll('\n', '
    '); + const descriptions = [description, getIframe(data, embed), getImgs(data), urlText, originDescription, getIframe(origin, embed), getImgs(origin), originUrlText] + .map((e) => e?.trim()) + .filter(Boolean) + .join('
    '); - return { - title, - description: descriptions, - pubDate: data.module_author?.pub_ts ? parseDate(data.module_author.pub_ts, 'X') : undefined, - link, - author, - }; - }) + return { + title, + description: descriptions, + pubDate: data.module_author?.pub_ts ? parseDate(data.module_author.pub_ts, 'X') : undefined, + link, + author, + category: category.length ? [...new Set(category)] : undefined, + attachments: + urlResult?.videoPageUrl || originUrlResult?.videoPageUrl + ? [ + { + url: urlResult?.videoPageUrl || originUrlResult?.videoPageUrl, + mime_type: 'text/html', + }, + ] + : undefined, + }; + }) ); return { diff --git a/lib/routes/bilibili/fav.ts b/lib/routes/bilibili/fav.ts index 0316ee172efaae..81ed06aaf85ed1 100644 --- a/lib/routes/bilibili/fav.ts +++ b/lib/routes/bilibili/fav.ts @@ -5,10 +5,10 @@ import { parseDate } from '@/utils/parse-date'; import { config } from '@/config'; export const route: Route = { - path: '/fav/:uid/:fid/:disableEmbed?', + path: '/fav/:uid/:fid/:embed?', categories: ['social-media'], example: '/bilibili/fav/756508/50948568', - parameters: { uid: '用户 id, 可在 UP 主主页中找到', fid: '收藏夹 ID, 可在收藏夹的 URL 中找到, 默认收藏夹建议使用 UP 主默认收藏夹功能', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { uid: '用户 id, 可在 UP 主主页中找到', fid: '收藏夹 ID, 可在收藏夹的 URL 中找到, 默认收藏夹建议使用 UP 主默认收藏夹功能', embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: false, requirePuppeteer: false, @@ -25,7 +25,7 @@ export const route: Route = { async function handler(ctx) { const fid = ctx.req.param('fid'); const uid = ctx.req.param('uid'); - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const response = await got({ url: `https://api.bilibili.com/x/v3/fav/resource/list?media_id=${fid}&ps=20`, @@ -51,7 +51,7 @@ async function handler(ctx) { data.medias && data.medias.map((item) => ({ title: item.title, - description: `${item.intro}${disableEmbed ? '' : `

    ${utils.iframe(item.id)}`}
    `, + description: utils.renderUGCDescription(embed, item.cover, item.intro, item.id, undefined, item.bvid), pubDate: parseDate(item.fav_time * 1000), link: item.fav_time > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.id}`, author: item.upper.name, diff --git a/lib/routes/bilibili/followers.ts b/lib/routes/bilibili/followers.ts index 3cb11f00439dce..2662aedda13d80 100644 --- a/lib/routes/bilibili/followers.ts +++ b/lib/routes/bilibili/followers.ts @@ -35,9 +35,9 @@ export const route: Route = { name: 'UP 主粉丝', maintainers: ['Qixingchen'], handler, - description: `:::warning + description: `::: warning UP 主粉丝现在需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/bilibili/followings-article.ts b/lib/routes/bilibili/followings-article.ts index 42ae47e72e0eeb..aa8371d2232ebb 100644 --- a/lib/routes/bilibili/followings-article.ts +++ b/lib/routes/bilibili/followings-article.ts @@ -29,9 +29,9 @@ export const route: Route = { name: '用户关注专栏', maintainers: ['woshiluo'], handler, - description: `:::warning + description: `::: warning 用户动态需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/bilibili/followings-dynamic.ts b/lib/routes/bilibili/followings-dynamic.ts index 1e09fa53930c3c..a4ceb81832fbd4 100644 --- a/lib/routes/bilibili/followings-dynamic.ts +++ b/lib/routes/bilibili/followings-dynamic.ts @@ -12,7 +12,19 @@ export const route: Route = { path: '/followings/dynamic/:uid/:routeParams?', categories: ['social-media'], example: '/bilibili/followings/dynamic/109937383', - parameters: { uid: '用户 id', routeParams: '额外参数;请参阅 [#UP 主动态](#bilibili-up-zhu-dong-tai) 的说明和表格' }, + parameters: { + uid: '用户 id, 可在 UP 主主页中找到', + routeParams: ` +| 键 | 含义 | 接受的值 | 默认值 | +| ---------- | --------------------------------- | -------------- | ------ | +| showEmoji | 显示或隐藏表情图片 | 0/1/true/false | false | +| embed | 默认开启内嵌视频 | 0/1/true/false | true | +| useAvid | 视频链接使用 AV 号 (默认为 BV 号) | 0/1/true/false | false | +| directLink | 使用内容直链 | 0/1/true/false | false | +| hideGoods | 隐藏带货动态 | 0/1/true/false | false | + +用例:\`/bilibili/followings/dynamic/2267573/showEmoji=1&embed=0&useAvid=1\``, + }, features: { requireConfig: [ { @@ -33,9 +45,9 @@ export const route: Route = { name: '用户关注动态', maintainers: ['TigerCubDen', 'JimenezLi'], handler, - description: `:::warning + description: `::: warning 用户动态需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 - :::`, +:::`, }; async function handler(ctx) { @@ -43,11 +55,10 @@ async function handler(ctx) { const routeParams = querystring.parse(ctx.req.param('routeParams')); const showEmoji = fallback(undefined, queryToBoolean(routeParams.showEmoji), false); - const disableEmbed = fallback(undefined, queryToBoolean(routeParams.disableEmbed), false); + const embed = fallback(undefined, queryToBoolean(routeParams.embed), true); const displayArticle = fallback(undefined, queryToBoolean(routeParams.displayArticle), false); const name = await cache.getUsernameFromUID(uid); - const cookie = config.bilibili.cookies[uid]; if (cookie === undefined) { throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值'); @@ -64,6 +75,9 @@ async function handler(ctx) { if (response.data.code === -6) { throw new ConfigNotFoundError('对应 uid 的 Bilibili 用户的 Cookie 已过期'); } + if (response.data.code === 4_100_000) { + throw new ConfigNotFoundError('对应 uid 的 Bilibili 用户 请求失败'); + } const data = JSONbig.parse(response.body).data.cards; const getTitle = (data) => (data ? data.title || data.description || data.content || (data.vest && data.vest.content) || '' : ''); @@ -72,7 +86,17 @@ async function handler(ctx) { const getOriginDes = (data) => (data && (data.apiSeasonInfo && data.apiSeasonInfo.title && `//转发自: ${data.apiSeasonInfo.title}`) + (data.index_title && `
    ${data.index_title}`)) || ''; const getOriginName = (data) => data.uname || (data.author && data.author.name) || (data.upper && data.upper.name) || (data.user && (data.user.uname || data.user.name)) || (data.owner && data.owner.name) || ''; const getOriginTitle = (data) => (data.title ? `${data.title}
    ` : ''); - const getIframe = (data) => (!disableEmbed && data && data.aid ? `

    ${utils.iframe(data.aid)}
    ` : ''); + const getIframe = (data) => { + if (!embed) { + return ''; + } + const aid = data?.aid; + const bvid = data?.bvid; + if (aid === undefined && bvid === undefined) { + return ''; + } + return utils.renderUGCDescription(embed, '', '', aid, undefined, bvid); + }; const getImgs = (data) => { let imgs = ''; // 动态图片 @@ -108,7 +132,9 @@ async function handler(ctx) { data.map(async (item) => { const parsed = JSONbig.parse(item.card); const data = parsed.apiSeasonInfo || (getTitle(parsed.item) ? parsed.item : parsed); - const origin = parsed.origin ? JSONbig.parse(parsed.origin) : null; + // parsed.origin is already parsed, and it may be json or string. + // Don't parse it again, or it will cause an error. + const origin = parsed.origin || null; // img let imgHTML = ''; diff --git a/lib/routes/bilibili/followings-video.ts b/lib/routes/bilibili/followings-video.ts index 6a112ed90a8b45..9332ce1124c9bc 100644 --- a/lib/routes/bilibili/followings-video.ts +++ b/lib/routes/bilibili/followings-video.ts @@ -4,12 +4,13 @@ import cache from './cache'; import { config } from '@/config'; import utils from './utils'; import ConfigNotFoundError from '@/errors/types/config-not-found'; +import logger from '@/utils/logger'; export const route: Route = { - path: '/followings/video/:uid/:disableEmbed?', + path: '/followings/video/:uid/:embed?', categories: ['social-media'], example: '/bilibili/followings/video/2267573', - parameters: { uid: '用户 id', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { uid: '用户 id', embed: '默认为开启内嵌视频,任意值为关闭' }, features: { requireConfig: [ { @@ -30,14 +31,14 @@ export const route: Route = { name: '用户关注视频动态', maintainers: ['LogicJake'], handler, - description: `:::warning + description: `::: warning 用户动态需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 - :::`, +:::`, }; async function handler(ctx) { const uid = String(ctx.req.param('uid')); - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const name = await cache.getUsernameFromUID(uid); const cookie = config.bilibili.cookies[uid]; @@ -53,19 +54,24 @@ async function handler(ctx) { Cookie: cookie, }, }); - if (response.data.code === -6) { - throw new ConfigNotFoundError('对应 uid 的 Bilibili 用户的 Cookie 已过期'); + const data = response.data; + if (data.code) { + logger.error(JSON.stringify(data)); + if (data.code === -6 || data.code === 4_100_000) { + throw new ConfigNotFoundError('对应 uid 的 Bilibili 用户的 Cookie 已过期'); + } + throw new Error(`Got error code ${data.code} while fetching: ${data.message}`); } - const cards = response.data.data.cards; + const cards = data.data.cards; const out = cards.map((card) => { const card_data = JSON.parse(card.card); return { title: card_data.title, - description: `${card_data.desc}${disableEmbed ? '' : `

    ${utils.iframe(card_data.aid)}`}
    `, + description: utils.renderUGCDescription(embed, card_data.pic, card_data.desc, card_data.aid, undefined, card.desc.bvid), pubDate: new Date(card_data.pubdate * 1000).toUTCString(), - link: card_data.pubdate > utils.bvidTime && card_data.bvid ? `https://www.bilibili.com/video/${card_data.bvid}` : `https://www.bilibili.com/video/av${card_data.aid}`, + link: card_data.pubdate > utils.bvidTime && card.desc.bvid ? `https://www.bilibili.com/video/${card.desc.bvid}` : `https://www.bilibili.com/video/av${card_data.aid}`, author: card.desc.user_profile.info.uname, }; }); diff --git a/lib/routes/bilibili/followings.ts b/lib/routes/bilibili/followings.ts index 0304864a70f69c..5fc22cbea47bf6 100644 --- a/lib/routes/bilibili/followings.ts +++ b/lib/routes/bilibili/followings.ts @@ -36,9 +36,9 @@ export const route: Route = { name: 'UP 主关注用户', maintainers: ['Qixingchen'], handler, - description: `:::warning + description: `::: warning UP 主关注用户现在需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/bilibili/hot-search.ts b/lib/routes/bilibili/hot-search.ts index 305ecd254ce0b1..647159f696e807 100644 --- a/lib/routes/bilibili/hot-search.ts +++ b/lib/routes/bilibili/hot-search.ts @@ -18,7 +18,7 @@ export const route: Route = { }, radar: [ { - source: ['www.bilibili.com/'], + source: ['www.bilibili.com/', 'm.bilibili.com/'], }, ], name: '热搜', diff --git a/lib/routes/bilibili/like.ts b/lib/routes/bilibili/like.ts index 4ad27daaa3c149..3c92b4b8459179 100644 --- a/lib/routes/bilibili/like.ts +++ b/lib/routes/bilibili/like.ts @@ -5,10 +5,10 @@ import utils from './utils'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: '/user/like/:uid/:disableEmbed?', + path: '/user/like/:uid/:embed?', categories: ['social-media'], example: '/bilibili/user/like/208259', - parameters: { uid: '用户 id, 可在 UP 主主页中找到', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { uid: '用户 id, 可在 UP 主主页中找到', embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: false, requirePuppeteer: false, @@ -30,7 +30,7 @@ export const route: Route = { async function handler(ctx) { const uid = ctx.req.param('uid'); - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const name = await cache.getUsernameFromUID(uid); @@ -51,7 +51,7 @@ async function handler(ctx) { description: `${name} 的 bilibili 点赞视频`, item: data.list.map((item) => ({ title: item.title, - description: `${item.desc}${disableEmbed ? '' : `

    ${utils.iframe(item.aid)}`}
    `, + description: utils.renderUGCDescription(embed, item.pic, item.desc, item.aid, undefined, item.bvid), pubDate: parseDate(item.pubdate * 1000), link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, author: item.owner.name, diff --git a/lib/routes/bilibili/live-area.ts b/lib/routes/bilibili/live-area.ts index b2b444e64b0882..c3cb69dbc561ab 100644 --- a/lib/routes/bilibili/live-area.ts +++ b/lib/routes/bilibili/live-area.ts @@ -17,9 +17,9 @@ export const route: Route = { name: '直播分区', maintainers: ['Qixingchen'], handler, - description: `:::warning + description: `::: warning 由于接口未提供开播时间,如果直播间未更换标题与分区,将视为一次。如果直播间更换分区与标题,将视为另一项 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/bilibili/live-room.ts b/lib/routes/bilibili/live-room.ts index 35a50c5f168170..7285da7adf9468 100644 --- a/lib/routes/bilibili/live-room.ts +++ b/lib/routes/bilibili/live-room.ts @@ -1,6 +1,9 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; +import { DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; import cache from './cache'; +import { decodeHTML } from 'entities'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; export const route: Route = { path: '/live/room/:roomID', @@ -32,33 +35,32 @@ async function handler(ctx) { if (Number.parseInt(roomID, 10) < 10000) { roomID = await cache.getLiveIDFromShortID(roomID); } - const name = await cache.getUsernameFromLiveID(roomID); + const info = await cache.getUserInfoFromLiveID(roomID); - const response = await got({ - method: 'get', - url: `https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${roomID}&from=room`, + const response = await ofetch(`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${roomID}&from=room`, { headers: { Referer: `https://live.bilibili.com/${roomID}`, }, }); - const data = response.data.data; + const data = response.data; - const liveItem = []; + const liveItem: DataItem[] = []; if (data.live_status === 1) { liveItem.push({ title: `${data.title} ${data.live_time}`, - description: `${data.title}
    ${data.description}`, - pubDate: new Date(data.live_time.replace(' ', 'T') + '+08:00').toUTCString(), + description: `
    ${decodeHTML(data.description)}`, + pubDate: timezone(parseDate(data.live_time), 8), guid: `https://live.bilibili.com/${roomID} ${data.live_time}`, link: `https://live.bilibili.com/${roomID}`, }); } return { - title: `${name} 直播间开播状态`, + title: `${info.uname} 直播间开播状态`, link: `https://live.bilibili.com/${roomID}`, - description: `${name} 直播间开播状态`, + description: `${info.uname} 直播间开播状态`, + image: info.face, item: liveItem, allowEmpty: true, }; diff --git a/lib/routes/bilibili/mall-new.ts b/lib/routes/bilibili/mall-new.ts index 5176e535eee576..0552e1588c37c1 100644 --- a/lib/routes/bilibili/mall-new.ts +++ b/lib/routes/bilibili/mall-new.ts @@ -18,8 +18,8 @@ export const route: Route = { maintainers: ['DIYgod'], handler, description: `| 全部 | 手办 | 魔力赏 | 周边 | 游戏 | - | ---- | ---- | ------ | ---- | ---- | - | 0 | 1 | 7 | 3 | 6 |`, +| ---- | ---- | ------ | ---- | ---- | +| 0 | 1 | 7 | 3 | 6 |`, }; async function handler(ctx) { diff --git a/lib/routes/bilibili/manga-followings.ts b/lib/routes/bilibili/manga-followings.ts index b992b3e4aa9988..3ecf19ef2e5c67 100644 --- a/lib/routes/bilibili/manga-followings.ts +++ b/lib/routes/bilibili/manga-followings.ts @@ -29,9 +29,9 @@ export const route: Route = { name: '用户追漫更新', maintainers: ['yindaheng98'], handler, - description: `:::warning + description: `::: warning 用户追漫需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/bilibili/manga-update.ts b/lib/routes/bilibili/manga-update.ts index 2b100dcea47904..183f56ec72143e 100644 --- a/lib/routes/bilibili/manga-update.ts +++ b/lib/routes/bilibili/manga-update.ts @@ -28,6 +28,8 @@ async function handler(ctx) { const comic_id = ctx.req.param('comicid').startsWith('mc') ? ctx.req.param('comicid').replace('mc', '') : ctx.req.param('comicid'); const link = `https://manga.bilibili.com/detail/mc${comic_id}`; + const spi_response = await got('https://api.bilibili.com/x/frontend/finger/spi'); + const response = await got({ method: 'POST', url: `https://manga.bilibili.com/twirp/comic.v2.Comic/ComicDetail?device=pc&platform=web`, @@ -36,6 +38,7 @@ async function handler(ctx) { }, headers: { Referer: link, + Cookie: `buvid3=${spi_response.data.data.b_3}; buvid4=${spi_response.data.data.b_4}`, }, }); const data = response.data.data; diff --git a/lib/routes/bilibili/namespace.ts b/lib/routes/bilibili/namespace.ts index cc6da5657d9990..bfcaeb80a07c10 100644 --- a/lib/routes/bilibili/namespace.ts +++ b/lib/routes/bilibili/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Bilibili', + name: '哔哩哔哩 bilibili', url: 'www.bilibili.com', + lang: 'zh-CN', }; diff --git a/lib/routes/bilibili/page.ts b/lib/routes/bilibili/page.ts index 39b6061f905c37..81b9354411d6a5 100644 --- a/lib/routes/bilibili/page.ts +++ b/lib/routes/bilibili/page.ts @@ -3,10 +3,10 @@ import got from '@/utils/got'; import utils from './utils'; export const route: Route = { - path: '/video/page/:bvid/:disableEmbed?', + path: '/video/page/:bvid/:embed?', categories: ['social-media'], example: '/bilibili/video/page/BV1i7411M7N9', - parameters: { bvid: '可在视频页 URL 中找到', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { bvid: '可在视频页 URL 中找到', embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: false, requirePuppeteer: false, @@ -27,7 +27,7 @@ async function handler(ctx) { aid = bvid; bvid = null; } - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const link = `https://www.bilibili.com/video/${bvid || `av${aid}`}`; const response = await got({ method: 'get', @@ -37,6 +37,7 @@ async function handler(ctx) { }, }); + const respdata = response.data.data; const { title: name, pages: data } = response.data.data; return { @@ -48,7 +49,7 @@ async function handler(ctx) { .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10) .map((item) => ({ title: item.part, - description: `${item.part} - ${name}${disableEmbed ? '' : `

    ${utils.iframe(aid, item.page, bvid)}`}`, + description: utils.renderUGCDescription(embed, respdata.pic, `${item.part} - ${name}`, respdata.aid, item.cid, respdata.bvid), link: `${link}?p=${item.page}`, })), }; diff --git a/lib/routes/bilibili/partion-ranking.ts b/lib/routes/bilibili/partion-ranking.ts index 8e422816081684..2ae4f3fc7b2254 100644 --- a/lib/routes/bilibili/partion-ranking.ts +++ b/lib/routes/bilibili/partion-ranking.ts @@ -16,10 +16,10 @@ function formatDate(now) { } export const route: Route = { - path: '/partion/ranking/:tid/:days?/:disableEmbed?', + path: '/partion/ranking/:tid/:days?/:embed?', categories: ['social-media'], example: '/bilibili/partion/ranking/171/3', - parameters: { tid: '分区 id, 见上方表格', days: '缺省为 7, 指最近多少天内的热度排序', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { tid: '分区 id, 见上方表格', days: '缺省为 7, 指最近多少天内的热度排序', embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: false, requirePuppeteer: false, @@ -36,7 +36,7 @@ export const route: Route = { async function handler(ctx) { const tid = ctx.req.param('tid'); const days = ctx.req.param('days') ?? 7; - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const responseApi = `https://api.bilibili.com/x/web-interface/newlist?ps=15&rid=${tid}&_=${Date.now()}`; @@ -59,7 +59,7 @@ async function handler(ctx) { for (let item of hotlist) { item = { title: item.title, - description: `${item.description}${disableEmbed ? '' : `

    ${utils.iframe(item.id)}`}

    Tags:${item.tag}`, + description: utils.renderUGCDescription(embed, item.pic, `${item.description} - ${item.tag}`, item.id, undefined, item.bvid), pubDate: new Date(item.pubdate).toUTCString(), link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.id}`, author: item.author, diff --git a/lib/routes/bilibili/partion.ts b/lib/routes/bilibili/partion.ts index 246083928cefb3..043d0897ac6503 100644 --- a/lib/routes/bilibili/partion.ts +++ b/lib/routes/bilibili/partion.ts @@ -3,10 +3,10 @@ import got from '@/utils/got'; import utils from './utils'; export const route: Route = { - path: '/partion/:tid/:disableEmbed?', + path: '/partion/:tid/:embed?', categories: ['social-media'], example: '/bilibili/partion/33', - parameters: { tid: '分区 id', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { tid: '分区 id', embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: false, requirePuppeteer: false, @@ -20,122 +20,122 @@ export const route: Route = { handler, description: `动画 - | MAD·AMV | MMD·3D | 短片・手书・配音 | 特摄 | 综合 | - | ------- | ------ | ---------------- | ---- | ---- | - | 24 | 25 | 47 | 86 | 27 | +| MAD·AMV | MMD·3D | 短片・手书・配音 | 特摄 | 综合 | +| ------- | ------ | ---------------- | ---- | ---- | +| 24 | 25 | 47 | 86 | 27 | 番剧 - | 连载动画 | 完结动画 | 资讯 | 官方延伸 | - | -------- | -------- | ---- | -------- | - | 33 | 32 | 51 | 152 | +| 连载动画 | 完结动画 | 资讯 | 官方延伸 | +| -------- | -------- | ---- | -------- | +| 33 | 32 | 51 | 152 | 国创 - | 国产动画 | 国产原创相关 | 布袋戏 | 动态漫・广播剧 | 资讯 | - | -------- | ------------ | ------ | -------------- | ---- | - | 153 | 168 | 169 | 195 | 170 | +| 国产动画 | 国产原创相关 | 布袋戏 | 动态漫・广播剧 | 资讯 | +| -------- | ------------ | ------ | -------------- | ---- | +| 153 | 168 | 169 | 195 | 170 | 音乐 - | 原创音乐 | 翻唱 | VOCALOID·UTAU | 电音 | 演奏 | MV | 音乐现场 | 音乐综合 | ~~OP/ED/OST~~ | - | -------- | ---- | ------------- | ---- | ---- | --- | -------- | -------- | ------------- | - | 28 | 31 | 30 | 194 | 59 | 193 | 29 | 130 | 54 | +| 原创音乐 | 翻唱 | VOCALOID·UTAU | 电音 | 演奏 | MV | 音乐现场 | 音乐综合 | ~~OP/ED/OST~~ | +| -------- | ---- | ------------- | ---- | ---- | --- | -------- | -------- | ------------- | +| 28 | 31 | 30 | 194 | 59 | 193 | 29 | 130 | 54 | 舞蹈 - | 宅舞 | 街舞 | 明星舞蹈 | 中国舞 | 舞蹈综合 | 舞蹈教程 | - | ---- | ---- | -------- | ------ | -------- | -------- | - | 20 | 198 | 199 | 200 | 154 | 156 | +| 宅舞 | 街舞 | 明星舞蹈 | 中国舞 | 舞蹈综合 | 舞蹈教程 | +| ---- | ---- | -------- | ------ | -------- | -------- | +| 20 | 198 | 199 | 200 | 154 | 156 | 游戏 - | 单机游戏 | 电子竞技 | 手机游戏 | 网络游戏 | 桌游棋牌 | GMV | 音游 | Mugen | - | -------- | -------- | -------- | -------- | -------- | --- | ---- | ----- | - | 17 | 171 | 172 | 65 | 173 | 121 | 136 | 19 | +| 单机游戏 | 电子竞技 | 手机游戏 | 网络游戏 | 桌游棋牌 | GMV | 音游 | Mugen | +| -------- | -------- | -------- | -------- | -------- | --- | ---- | ----- | +| 17 | 171 | 172 | 65 | 173 | 121 | 136 | 19 | 知识 - | 科学科普 | 社科人文 | 财经 | 校园学习 | 职业职场 | 野生技术协会 | - | -------- | -------- | ---- | -------- | -------- | ------------ | - | 201 | 124 | 207 | 208 | 209 | 122 | +| 科学科普 | 社科人文 | 财经 | 校园学习 | 职业职场 | 野生技术协会 | +| -------- | -------- | ---- | -------- | -------- | ------------ | +| 201 | 124 | 207 | 208 | 209 | 122 | ~~科技~~ - | ~~演讲・公开课~~ | ~~星海~~ | ~~机械~~ | ~~汽车~~ | - | ---------------- | -------- | -------- | -------- | - | 39 | 96 | 98 | 176 | +| ~~演讲・公开课~~ | ~~星海~~ | ~~机械~~ | ~~汽车~~ | +| ---------------- | -------- | -------- | -------- | +| 39 | 96 | 98 | 176 | 数码 - | 手机平板 | 电脑装机 | 摄影摄像 | 影音智能 | - | -------- | -------- | -------- | -------- | - | 95 | 189 | 190 | 191 | +| 手机平板 | 电脑装机 | 摄影摄像 | 影音智能 | +| -------- | -------- | -------- | -------- | +| 95 | 189 | 190 | 191 | 生活 - | 搞笑 | 日常 | 美食圈 | 动物圈 | 手工 | 绘画 | 运动 | 汽车 | 其他 | ~~ASMR~~ | - | ---- | ---- | ------ | ------ | ---- | ---- | ---- | ---- | ---- | -------- | - | 138 | 21 | 76 | 75 | 161 | 162 | 163 | 176 | 174 | 175 | +| 搞笑 | 日常 | 美食圈 | 动物圈 | 手工 | 绘画 | 运动 | 汽车 | 其他 | ~~ASMR~~ | +| ---- | ---- | ------ | ------ | ---- | ---- | ---- | ---- | ---- | -------- | +| 138 | 21 | 76 | 75 | 161 | 162 | 163 | 176 | 174 | 175 | 鬼畜 - | 鬼畜调教 | 音 MAD | 人力 VOCALOID | 教程演示 | - | -------- | ------ | ------------- | -------- | - | 22 | 26 | 126 | 127 | +| 鬼畜调教 | 音 MAD | 人力 VOCALOID | 教程演示 | +| -------- | ------ | ------------- | -------- | +| 22 | 26 | 126 | 127 | 时尚 - | 美妆 | 服饰 | 健身 | T 台 | 风向标 | - | ---- | ---- | ---- | ---- | ------ | - | 157 | 158 | 164 | 159 | 192 | +| 美妆 | 服饰 | 健身 | T 台 | 风向标 | +| ---- | ---- | ---- | ---- | ------ | +| 157 | 158 | 164 | 159 | 192 | ~~广告~~ - | ~~广告~~ | - | -------- | - | 166 | +| ~~广告~~ | +| -------- | +| 166 | 资讯 - | 热点 | 环球 | 社会 | 综合 | - | ---- | ---- | ---- | ---- | - | 203 | 204 | 205 | 206 | +| 热点 | 环球 | 社会 | 综合 | +| ---- | ---- | ---- | ---- | +| 203 | 204 | 205 | 206 | 娱乐 - | 综艺 | 明星 | Korea 相关 | - | ---- | ---- | ---------- | - | 71 | 137 | 131 | +| 综艺 | 明星 | Korea 相关 | +| ---- | ---- | ---------- | +| 71 | 137 | 131 | 影视 - | 影视杂谈 | 影视剪辑 | 短片 | 预告・资讯 | - | -------- | -------- | ---- | ---------- | - | 182 | 183 | 85 | 184 | +| 影视杂谈 | 影视剪辑 | 短片 | 预告・资讯 | +| -------- | -------- | ---- | ---------- | +| 182 | 183 | 85 | 184 | 纪录片 - | 全部 | 人文・历史 | 科学・探索・自然 | 军事 | 社会・美食・旅行 | - | ---- | ---------- | ---------------- | ---- | ---------------- | - | 177 | 37 | 178 | 179 | 180 | +| 全部 | 人文・历史 | 科学・探索・自然 | 军事 | 社会・美食・旅行 | +| ---- | ---------- | ---------------- | ---- | ---------------- | +| 177 | 37 | 178 | 179 | 180 | 电影 - | 全部 | 华语电影 | 欧美电影 | 日本电影 | 其他国家 | - | ---- | -------- | -------- | -------- | -------- | - | 23 | 147 | 145 | 146 | 83 | +| 全部 | 华语电影 | 欧美电影 | 日本电影 | 其他国家 | +| ---- | -------- | -------- | -------- | -------- | +| 23 | 147 | 145 | 146 | 83 | 电视剧 - | 全部 | 国产剧 | 海外剧 | - | ---- | ------ | ------ | - | 11 | 185 | 187 |`, +| 全部 | 国产剧 | 海外剧 | +| ---- | ------ | ------ | +| 11 | 185 | 187 |`, }; async function handler(ctx) { const tid = ctx.req.param('tid'); - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const response = await got({ method: 'get', @@ -159,7 +159,7 @@ async function handler(ctx) { list && list.map((item) => ({ title: item.title, - description: `${item.desc}${disableEmbed ? '' : `

    ${utils.iframe(item.aid)}`}
    `, + description: utils.renderUGCDescription(embed, item.pic, item.desc, item.aid, undefined, item.bvid), pubDate: new Date(item.pubdate * 1000).toUTCString(), link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, author: item.owner.name, diff --git a/lib/routes/bilibili/popular.ts b/lib/routes/bilibili/popular.ts index 3206d6242b6281..cc8df1a4316975 100644 --- a/lib/routes/bilibili/popular.ts +++ b/lib/routes/bilibili/popular.ts @@ -3,10 +3,12 @@ import got from '@/utils/got'; import utils from './utils'; export const route: Route = { - path: '/popular/all', + path: '/popular/all/:embed?', categories: ['social-media'], example: '/bilibili/popular/all', - parameters: {}, + parameters: { + embed: '默认为开启内嵌视频, 任意值为关闭', + }, features: { requireConfig: false, requirePuppeteer: false, @@ -21,7 +23,7 @@ export const route: Route = { }; async function handler(ctx) { - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const response = await got({ method: 'get', url: `https://api.bilibili.com/x/web-interface/popular`, @@ -39,7 +41,7 @@ async function handler(ctx) { list && list.map((item) => ({ title: item.title, - description: `${item.desc}${disableEmbed ? '' : `

    ${utils.iframe(item.aid)}`}
    `, + description: utils.renderUGCDescription(embed, item.pic, item.desc, item.aid, undefined, item.bvid), pubDate: new Date(item.pubdate * 1000).toUTCString(), link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, author: item.owner.name, diff --git a/lib/routes/bilibili/ranking.ts b/lib/routes/bilibili/ranking.ts index 800ecbb3a43255..06cca828238066 100644 --- a/lib/routes/bilibili/ranking.ts +++ b/lib/routes/bilibili/ranking.ts @@ -1,62 +1,231 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; -import utils from './utils'; +import utils, { getVideoUrl } from './utils'; + +// https://www.bilibili.com/v/popular/rank/all + +// 0 all https://api.bilibili.com/x/web-interface/ranking/v2?rid=0&type=all&web_location=333.934&w_rid=d4e0c1b83157e3d36836eb3c4258ef61&wts=1731320484 +// 1 bangumi https://api.bilibili.com/pgc/web/rank/list?day=3&season_type=1&web_location=333.934&w_rid=2d46eff2d363c4960bc875e63e24df6c&wts=1731320507 +// 2 guochan https://api.bilibili.com/pgc/season/rank/web/list?day=3&season_type=4&web_location=333.934&w_rid=b26195dc9ee2f925bc196da68df341a5&wts=1731320523 +// 3 guochuang https://api.bilibili.com/x/web-interface/ranking/v2?rid=168&type=all&web_location=333.934&w_rid=f99e5982b011eb24643a2daffb7baf00&wts=1731320537 +// 4 documentary https://api.bilibili.com/pgc/season/rank/web/list?day=3&season_type=3&web_location=333.934&w_rid=2067f7277cf49cbea4c5e5630eeb929a&wts=1731320556 +// 5 douga https://api.bilibili.com/x/web-interface/ranking/v2?rid=1&type=all&web_location=333.934&w_rid=14bf53ce651e8d575d5982b24e1cebdf&wts=1731320579 +// 6 music https://api.bilibili.com/x/web-interface/ranking/v2?rid=3&type=all&web_location=333.934&w_rid=70f4c870f860b9334ebe6e9fe835d3fe&wts=1731320595 +// 7 dance https://api.bilibili.com/x/web-interface/ranking/v2?rid=129&type=all&web_location=333.934&w_rid=691f713f7fc6d3cc08174affcc59f97c&wts=1731321260 +// 8 game https://api.bilibili.com/x/web-interface/ranking/v2?rid=4&type=all&web_location=333.934&w_rid=cac9f26f49da223cb8ab6f189250ec23&wts=1731320726 +// 9 knowledge https://api.bilibili.com/x/web-interface/ranking/v2?rid=36&type=all&web_location=333.934&w_rid=79c274d74e90d93ac7adfd2df968288e&wts=1731320750 +// 10 tech https://api.bilibili.com/x/web-interface/ranking/v2?rid=188&type=all&web_location=333.934&w_rid=115d9e69c48bf958622c4cc0ee861b57&wts=1731320766 +// 11 sports https://api.bilibili.com/x/web-interface/ranking/v2?rid=234&type=all&web_location=333.934&w_rid=c618d12f36e2379bda0c9a2754cd71e0&wts=1731320783 +// 12 car https://api.bilibili.com/x/web-interface/ranking/v2?rid=223&type=all&web_location=333.934&w_rid=753bc1395718051aa53aedaa3cd04d76&wts=1731320797 +// 13 life https://api.bilibili.com/x/web-interface/ranking/v2?rid=160&type=all&web_location=333.934&w_rid=3e8895d4749e905173886dd387f657e9&wts=1731320823 +// 14 food https://api.bilibili.com/x/web-interface/ranking/v2?rid=211&type=all&web_location=333.934&w_rid=9ec93cab672a98ea972dfb9cb7ed6368&wts=1731320838 +// 15 animal https://api.bilibili.com/x/web-interface/ranking/v2?rid=217&type=all&web_location=333.934&w_rid=794e69434ec4a818f4d589e5306e9a21&wts=1731320852 +// 16 kichiku https://api.bilibili.com/x/web-interface/ranking/v2?rid=119&type=all&web_location=333.934&w_rid=c5e35f3f247bc9294557ab90e0be166a&wts=1731320865 +// 17 fashion https://api.bilibili.com/x/web-interface/ranking/v2?rid=155&type=all&web_location=333.934&w_rid=f3711c888057a8fef1f47da9cf4bcd86&wts=1731320878 +// 18 ent https://api.bilibili.com/x/web-interface/ranking/v2?rid=5&type=all&web_location=333.934&w_rid=5ca1b2da22de1c9e818ac619d309fed2&wts=1731320889 +// 19 cinephile https://api.bilibili.com/x/web-interface/ranking/v2?rid=181&type=all&web_location=333.934&w_rid=8f5cae08b232025f93b74feaefdc95d9&wts=1731320903 +// 20 movie https://api.bilibili.com/pgc/season/rank/web/list?day=3&season_type=2&web_location=333.934&w_rid=ccd42543ab1c4330e9f81fb52b098a9c&wts=1731320916 +// 21 tv https://api.bilibili.com/pgc/season/rank/web/list?day=3&season_type=5&web_location=333.934&w_rid=10fae974e8d30dd6bba11527fe17e551&wts=1731320934 +// 22 variety https://api.bilibili.com/pgc/season/rank/web/list?day=3&season_type=7&web_location=333.934&w_rid=c3105fd0dac70dcdf4f08ca6b5cbdb8f&wts=1731320948 +// 23 origin https://api.bilibili.com/x/web-interface/ranking/v2?rid=0&type=origin&web_location=333.934&w_rid=53100b7aeeca012399f4f8f3746bcbdb&wts=1731320960 +// 24 rookie https://api.bilibili.com/x/web-interface/ranking/v2?rid=0&type=rookie&web_location=333.934&w_rid=b8adda7447e2f115b2ed36495e436934&wts=1731320971 + +const ridNumberList = ['0', '1', '4', '168', '3', '1', '3', '129', '4', '36', '188', '234', '223', '160', '211', '217', '119', '155', '5', '181', '2', '5', '7', '0', '0']; +const ridChineseList = [ + '全站', + '番剧', + '国产动画', + '国创相关', + '纪录片', + '动画', + '音乐', + '舞蹈', + '游戏', + '知识', + '科技', + '运动', + '汽车', + '生活', + '美食', + '动物圈', + '鬼畜', + '时尚', + '娱乐', + '影视', + '电影', + '电视剧', + '综艺', + '原创', + '新人', +]; +const ridEnglishList = [ + 'all', + 'bangumi', + 'guochan', + 'guochuang', + 'documentary', + 'douga', + 'music', + 'dance', + 'game', + 'knowledge', + 'tech', + 'sports', + 'car', + 'life', + 'food', + 'animal', + 'kichiku', + 'fashion', + 'ent', + 'cinephile', + 'movie', + 'tv', + 'variety', + 'origin', + 'rookie', +]; +const ridTypeList = [ + 'x/rid', + 'pgc/web', + 'pgc/season', + 'x/rid', + 'pgc/season', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'x/rid', + 'pgc/season', + 'pgc/season', + 'pgc/season', + 'x/type', + 'x/type', +]; export const route: Route = { - path: '/ranking/:rid?/:day?/:arc_type?/:disableEmbed?', + path: '/ranking/:rid_index?/:embed?/:redirect1?/:redirect2?', name: '排行榜', - maintainers: ['DIYgod'], - categories: ['social-media'], - example: '/bilibili/ranking/0/3/1', + maintainers: ['DIYgod', 'hyoban'], + categories: ['social-media', 'popular'], + view: ViewType.Videos, + example: '/bilibili/ranking/0', parameters: { - rid: '排行榜分区 id, 默认 0', - day: '时间跨度, 可为 1 3 7 30', - arc_type: '投稿时间, 可为 0(全部投稿) 1(近期投稿) , 默认 1', - disableEmbed: '默认为开启内嵌视频, 任意值为关闭', + rid_index: { + description: '排行榜分区 id 序号', + default: '0', + options: Array.from({ length: ridNumberList.length }, (_, i) => ({ + value: String(i), + label: ridChineseList[i], + })).filter((_, i) => !ridTypeList[i].startsWith('pgc/')), + }, + embed: '默认为开启内嵌视频, 任意值为关闭', + redirect1: '留空,用于兼容之前的路由', + redirect2: '留空,用于兼容之前的路由', }, - description: `| 全站 | 动画 | 国创相关 | 音乐 | 舞蹈 | 游戏 | 科技 | 数码 | 生活 | 鬼畜 | 时尚 | 娱乐 | 影视 | - | ---- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | - | 0 | 1 | 168 | 3 | 129 | 4 | 36 | 188 | 160 | 119 | 155 | 5 | 181 |`, handler, }; +function getRidIndexByRid(rid: string): number { + const index = ridNumberList.indexOf(rid); + if (index === -1) { + throw new Error('Invalid rid'); + } + return index; +} + +function getAPI(ridIndex: number) { + if (ridIndex < 0 || ridIndex >= ridNumberList.length) { + throw new Error('Invalid rid index'); + } + const rid = ridNumberList[ridIndex]; + const ridType = ridTypeList[ridIndex]; + const ridChinese = ridChineseList[ridIndex]; + const ridEnglish = ridEnglishList[ridIndex]; + + let apiURL = ''; + + switch (ridType) { + case 'x/rid': + apiURL = `https://api.bilibili.com/x/web-interface/ranking?rid=${rid}&type=all`; + break; + case 'pgc/web': + apiURL = `https://api.bilibili.com/pgc/web/rank/list?day=3&season_type=${rid}`; + break; + case 'pgc/season': + apiURL = `https://api.bilibili.com/pgc/season/rank/web/list?day=3&season_type=${rid}`; + break; + case 'x/type': + apiURL = `https://api.bilibili.com/x/web-interface/ranking?rid=0&type=${ridEnglish}`; + break; + default: + throw new Error('Invalid rid type'); + } + + return { + apiURL, + referer: `https://www.bilibili.com/v/popular/rank/${ridEnglish}`, + ridChinese, + ridType, + link: `https://www.bilibili.com/v/popular/rank/${ridEnglish}`, + }; +} + async function handler(ctx) { - const rid = ctx.req.param('rid') || '0'; - const day = ctx.req.param('day') || '3'; - const arc_type = ctx.req.param('arc_type') || '1'; - const disableEmbed = ctx.req.param('disableEmbed'); - const arc_type1 = arc_type === '0' ? '全部投稿' : '近期投稿'; - const rid_1 = ['0', '1', '168', '3', '129', '4', '36', '188', '160', '119', '155', '5', '181']; - const rid_2 = ['全站', '动画', '国创相关', '音乐', '舞蹈', '游戏', '科技', '数码', '生活', '鬼畜', '时尚', '娱乐', '影视']; - const rid_i = rid_1.indexOf(rid + ''); - const rid_type = rid_2[rid_i]; + const args = ctx.req.param(); + if (args.redirect1 || args.redirect2) { + // redirect old routes like /bilibili/ranking/0/3/1 or /bilibili/ranking/0/3/1/xxx + const embedArg = args.redirect2 ? '/' + args.redirect2 : ''; + ctx.set('redirect', `/bilibili/ranking/${getRidIndexByRid(args.rid_index)}${embedArg}`); + return null; + } + + const ridIndex = ctx.req.param('rid_index') || '0'; + const embed = !ctx.req.param('embed'); + + const { apiURL, referer, ridChinese, link, ridType } = getAPI(Number(ridIndex)); + if (ridType.startsWith('pgc/')) { + throw new Error('This type of ranking is not supported yet'); + } + const response = await got({ method: 'get', - url: `https://api.bilibili.com/x/web-interface/ranking?jsonp=jsonp&rid=${rid}&day=${day}&type=1&arc_type=${arc_type}&callback=__jp0`, + url: apiURL, headers: { - Referer: `https://www.bilibili.com/ranking/all/${rid}/${arc_type}/${day}`, + Referer: referer, }, }); - const data = JSON.parse(response.data.match(/^__jp0\((.*)\)$/)[1]).data || {}; - let list = data.list || []; - for (let i = 0; i < list.length; i++) { - if (list[i].others && list[i].others.length) { - for (const item of list[i].others) { - item.author = list[i].author; - } - list = [...list, ...list[i].others]; - } - } + const data = response.data.data || response.data.result; + const list = data.list || []; return { - title: `bilibili ${day}日排行榜-${rid_type}-${arc_type1}`, - link: `https://www.bilibili.com/ranking/all/${rid}/0/${day}`, + title: `bilibili 排行榜-${ridChinese}`, + link, item: list.map((item) => ({ title: item.title, - description: `${item.description || item.title}${disableEmbed ? '' : `

    ${utils.iframe(item.aid, null, item.bvid)}`}
    `, + description: utils.renderUGCDescription(embed, item.pic, item.description || item.title, item.aid, undefined, item.bvid), pubDate: item.create && new Date(item.create).toUTCString(), author: item.author, - link: !item.create || (new Date(item.create) / 1000 > utils.bvidTime && item.bvid) ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, + link: !item.create || (new Date(item.create).getTime() / 1000 > utils.bvidTime && item.bvid) ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, + image: item.pic, + attachments: item.bvid + ? [ + { + url: getVideoUrl(item.bvid), + mime_type: 'text/html', + }, + ] + : undefined, })), }; } diff --git a/lib/routes/bilibili/readlist.ts b/lib/routes/bilibili/readlist.ts index 00a11db28240d9..f88b95d274f8f7 100644 --- a/lib/routes/bilibili/readlist.ts +++ b/lib/routes/bilibili/readlist.ts @@ -1,9 +1,10 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; export const route: Route = { path: '/readlist/:listid', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.Articles, example: '/bilibili/readlist/25611', parameters: { listid: '文集 id, 可在专栏文集 URL 中找到' }, features: { diff --git a/lib/routes/bilibili/reply.ts b/lib/routes/bilibili/reply.ts index cb63d4cc678efe..25a69415cf81b9 100644 --- a/lib/routes/bilibili/reply.ts +++ b/lib/routes/bilibili/reply.ts @@ -33,11 +33,13 @@ async function handler(ctx) { } const link = `https://www.bilibili.com/video/${bvid || `av${aid}`}`; + const cookie = await cache.getCookie(); const response = await got({ method: 'get', url: `https://api.bilibili.com/x/v2/reply?type=1&oid=${aid}&sort=0`, headers: { Referer: link, + Cookie: cookie, }, }); diff --git a/lib/routes/bilibili/templates/description.art b/lib/routes/bilibili/templates/description.art new file mode 100644 index 00000000000000..5f6e5847d51ec8 --- /dev/null +++ b/lib/routes/bilibili/templates/description.art @@ -0,0 +1,14 @@ +{{ if embed }} +{{ if ugc }} + +{{ /if }} +{{ if ogv }} + +{{ /if }} +
    +{{ /if }} +{{ if img}} + +
    +{{ /if }} +{{@ description }} diff --git a/lib/routes/bilibili/types.ts b/lib/routes/bilibili/types.ts new file mode 100644 index 00000000000000..bc53df24f2cf07 --- /dev/null +++ b/lib/routes/bilibili/types.ts @@ -0,0 +1,52 @@ +export interface ResultResponse { + result: Result; +} + +/** + * 番剧信息 + * + * @interface MediaResult + * + * @property {string} cover - 封面。 + * @property {string} evaluate - 摘要。 + * @property {number} media_id - 媒体 ID。 + * @property {number} season_id - 季度 ID。 + * @property {string} share_url - 分享 URL。此属性是注入的。 + * @property {string} title - 标题。 + */ +export interface MediaResult { + cover: string; + evaluate: string; + media_id: number; + season_id: number; + share_url: string; // injected + title: string; +} + +export interface SeasonResult { + main_section: SectionResult; + section: SectionResult[]; +} + +export interface SectionResult { + episodes: EpisodeResult[]; +} + +/** + * 番剧剧集信息 + * + * @interface EpisodeResult + * + * @property {string} cover - 封面。 + * @property {number} id - 剧集 ID。 + * @property {string} long_title - 完整标题。 + * @property {string} share_url - 分享 URL。 + * @property {string} title - 短标题。 + */ +export interface EpisodeResult { + cover: string; + id: number; + long_title: string; + share_url: string; + title: string; +} diff --git a/lib/routes/bilibili/user-channel.ts b/lib/routes/bilibili/user-channel.ts index db02b81b44e07b..a319a67fd70b01 100644 --- a/lib/routes/bilibili/user-channel.ts +++ b/lib/routes/bilibili/user-channel.ts @@ -10,10 +10,10 @@ const notFoundData = { }; export const route: Route = { - path: '/user/channel/:uid/:sid/:disableEmbed?', + path: '/user/channel/:uid/:sid/:embed?', categories: ['social-media'], example: '/bilibili/user/channel/2267573/396050', - parameters: { uid: '用户 id, 可在 UP 主主页中找到', sid: '频道 id, 可在频道的 URL 中找到', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { uid: '用户 id, 可在 UP 主主页中找到', sid: '频道 id, 可在频道的 URL 中找到', embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: false, requirePuppeteer: false, @@ -30,7 +30,7 @@ export const route: Route = { async function handler(ctx) { const uid = Number.parseInt(ctx.req.param('uid')); const sid = Number.parseInt(ctx.req.param('sid')); - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const limit = ctx.req.query('limit') ?? 25; const link = `https://space.bilibili.com/${uid}/channel/seriesdetail?sid=${sid}`; @@ -68,21 +68,15 @@ async function handler(ctx) { title: `${userName} 的 bilibili 频道 ${channelInfo.meta.name}`, link, description: `${userName} 的 bilibili 频道`, + image: face, logo: face, icon: face, - item: data.archives.map((item) => { - const descList = []; - if (!disableEmbed) { - descList.push(utils.iframe(item.aid)); - } - descList.push(``); - return { - title: item.title, - description: descList.join('
    '), - pubDate: parseDate(item.pubdate, 'X'), - link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, - author: userName, - }; - }), + item: data.archives.map((item) => ({ + title: item.title, + description: utils.renderUGCDescription(embed, item.pic, '', item.aid, undefined, item.bvid), + pubDate: parseDate(item.pubdate, 'X'), + link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, + author: userName, + })), }; } diff --git a/lib/routes/bilibili/user-collection.ts b/lib/routes/bilibili/user-collection.ts index 0d46ade10fd6a1..5dab48de714d4f 100644 --- a/lib/routes/bilibili/user-collection.ts +++ b/lib/routes/bilibili/user-collection.ts @@ -10,13 +10,13 @@ const notFoundData = { }; export const route: Route = { - path: '/user/collection/:uid/:sid/:disableEmbed?/:sortReverse?/:page?', + path: '/user/collection/:uid/:sid/:embed?/:sortReverse?/:page?', categories: ['social-media'], example: '/bilibili/user/collection/245645656/529166', parameters: { uid: '用户 id, 可在 UP 主主页中找到', sid: '合集 id, 可在合集页面的 URL 中找到', - disableEmbed: '空,0与false为开启内嵌视频, 其他任意值为关闭', + embed: '默认为开启内嵌视频, 任意值为关闭', sortReverse: '默认:默认排序 1:升序排序', page: '页码, 默认1', }, @@ -29,21 +29,21 @@ export const route: Route = { supportScihub: false, }, name: 'UP 主频道的合集', - maintainers: ['shininome'], + maintainers: ['shininome', 'cscnk52'], handler, }; async function handler(ctx) { const uid = Number.parseInt(ctx.req.param('uid')); const sid = Number.parseInt(ctx.req.param('sid')); - const disableEmbed = queryToBoolean(ctx.req.param('disableEmbed')); + const embed = queryToBoolean(ctx.req.param('embed') || 'true'); const sortReverse = Number.parseInt(ctx.req.param('sortReverse')) === 1; const page = ctx.req.param('page') ? Number.parseInt(ctx.req.param('page')) : 1; const limit = ctx.req.query('limit') ?? 25; const link = `https://space.bilibili.com/${uid}/channel/collectiondetail?sid=${sid}`; const [userName, face] = await cache.getUsernameAndFaceFromUID(uid); - const host = `https://api.bilibili.com/x/polymer/space/seasons_archives_list?mid=${uid}&season_id=${sid}&sort_reverse=${sortReverse}&page_num=${page}&page_size=${limit}`; + const host = `https://api.bilibili.com/x/polymer/web-space/seasons_archives_list?mid=${uid}&season_id=${sid}&sort_reverse=${sortReverse}&page_num=${page}&page_size=${limit}`; const response = await got(host, { headers: { @@ -60,21 +60,15 @@ async function handler(ctx) { title: `${userName} 的 bilibili 合集 ${data.meta.name}`, link, description: `${userName} 的 bilibili 合集`, + image: face, logo: face, icon: face, - item: data.archives.map((item) => { - const descList = []; - if (!disableEmbed) { - descList.push(utils.iframe(item.aid)); - } - descList.push(``); - return { - title: item.title, - description: descList.join('
    '), - pubDate: parseDate(item.pubdate, 'X'), - link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, - author: userName, - }; - }), + item: data.archives.map((item) => ({ + title: item.title, + description: utils.renderUGCDescription(embed, item.pic, '', item.aid, undefined, item.bvid), + pubDate: parseDate(item.pubdate, 'X'), + link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, + author: userName, + })), }; } diff --git a/lib/routes/bilibili/user-fav.ts b/lib/routes/bilibili/user-fav.ts index 88db0bb1009897..04deeb6d0e3cfc 100644 --- a/lib/routes/bilibili/user-fav.ts +++ b/lib/routes/bilibili/user-fav.ts @@ -5,10 +5,10 @@ import utils from './utils'; import { config } from '@/config'; export const route: Route = { - path: '/user/fav/:uid/:disableEmbed?', + path: '/user/fav/:uid/:embed?', categories: ['social-media'], example: '/bilibili/user/fav/2267573', - parameters: { uid: '用户 id, 可在 UP 主主页中找到', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { uid: '用户 id, 可在 UP 主主页中找到', embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: false, requirePuppeteer: false, @@ -30,7 +30,7 @@ export const route: Route = { async function handler(ctx) { const uid = ctx.req.param('uid'); - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const name = await cache.getUsernameFromUID(uid); const response = await got({ @@ -53,7 +53,7 @@ async function handler(ctx) { data.data.archives && data.data.archives.map((item) => ({ title: item.title, - description: `${item.desc}${disableEmbed ? '' : `

    ${utils.iframe(item.aid)}`}
    `, + description: utils.renderUGCDescription(embed, item.pic, item.desc, item.aid, undefined, item.bvid), pubDate: new Date(item.fav_at * 1000).toUTCString(), link: item.fav_at > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, author: item.owner.name, diff --git a/lib/routes/bilibili/utils.ts b/lib/routes/bilibili/utils.ts index 15fd78850a742c..d67213229964ef 100644 --- a/lib/routes/bilibili/utils.ts +++ b/lib/routes/bilibili/utils.ts @@ -1,12 +1,13 @@ +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + import { config } from '@/config'; import md5 from '@/utils/md5'; +import ofetch from '@/utils/ofetch'; +import { art } from '@/utils/render'; import CryptoJS from 'crypto-js'; - -function iframe(aid: any, page?: any, bvid?: any) { - return ``; -} +import path from 'node:path'; +import { MediaResult, ResultResponse, SeasonResult } from './types'; // a function randomHexStr(length) { @@ -66,6 +67,10 @@ function hexsign(e) { return o; } +function addRenderData(params, renderData) { + return `${params}&w_webid=${encodeURIComponent(renderData)}`; +} + function addWbiVerifyInfo(params, wbiVerifyString) { const searchParams = new URLSearchParams(params); searchParams.sort(); @@ -91,8 +96,8 @@ function getDmImgList() { const dmImgList = JSON.parse(config.bilibili.dmImgList); return JSON.stringify([dmImgList[Math.floor(Math.random() * dmImgList.length)]]); } - const x = Math.max(generateGaussianInteger(650, 5), 0); - const y = Math.max(generateGaussianInteger(400, 5), 0); + const x = Math.max(generateGaussianInteger(1245, 5), 0); + const y = Math.max(generateGaussianInteger(1285, 5), 0); const path = [ { x: 3 * x + 2 * y, @@ -105,21 +110,211 @@ function getDmImgList() { return JSON.stringify(path); } -function addDmVerifyInfo(params, dmImgList) { +function getDmImgInter() { + if (config.bilibili.dmImgInter !== undefined) { + const dmImgInter = JSON.parse(config.bilibili.dmImgInter); + return JSON.stringify([dmImgInter[Math.floor(Math.random() * dmImgInter.length)]]); + } + const p1 = getDmImgInterWh(274, 601); + const s1 = getDmImgInterOf(134, 30); + const p2 = getDmImgInterWh(332, 64); + const s2 = getDmImgInterOf(1101, 338); + const of = getDmImgInterOf(0, 0); + const wh = getDmImgInterWh(1245, 1285); + const ds = [ + { + t: getDmImgInterT('div'), + c: getDmImgInterC('clearfix g-search search-container'), + p: [p1[0], p1[2], p1[1]], + s: [s1[2], s1[0], s1[1]], + }, + { + t: getDmImgInterT('div'), + c: getDmImgInterC('wrapper'), + p: [p2[0], p2[2], p2[1]], + s: [s2[2], s2[0], s2[1]], + }, + ]; + return JSON.stringify({ ds, wh, of }); +} + +function getDmImgInterT(tag: string) { + return { + a: 4, + article: 29, + button: 7, + div: 2, + em: 27, + form: 17, + h1: 11, + h2: 12, + h3: 13, + h4: 14, + h5: 15, + h6: 16, + img: 5, + input: 6, + label: 25, + li: 10, + ol: 9, + option: 20, + p: 3, + section: 28, + select: 19, + span: 1, + strong: 26, + table: 21, + td: 23, + textarea: 18, + th: 24, + tr: 22, + ul: 8, + }[tag]; +} + +function getDmImgInterC(className: string) { + return Buffer.from(className).toString('base64').slice(0, -2); +} + +function getDmImgInterOf(top: number, left: number) { + const seed = Math.floor(514 * Math.random()); + return [3 * top + 2 * left + seed, 4 * top - 4 * left + 2 * seed, seed]; +} + +function getDmImgInterWh(width: number, height: number) { + const seed = Math.floor(114 * Math.random()); + return [2 * width + 2 * height + 3 * seed, 4 * width - height + seed, seed]; +} + +function addDmVerifyInfo(params: string, dmImgList: string) { const dmImgStr = Buffer.from('no webgl').toString('base64').slice(0, -2); const dmCoverImgStr = Buffer.from('no webgl').toString('base64').slice(0, -2); return `${params}&dm_img_list=${dmImgList}&dm_img_str=${dmImgStr}&dm_cover_img_str=${dmCoverImgStr}`; } +function addDmVerifyInfoWithInter(params: string, dmImgList: string, dmImgInter: string) { + return `${addDmVerifyInfo(params, dmImgList)}&dm_img_inter=${dmImgInter}`; +} + const bvidTime = 1_589_990_400; +/** + * 获取番剧信息并缓存 + * + * @param {string} id - 番剧 ID。 + * @param cache - 缓存 module。 + * @returns {Promise} 番剧信息。 + */ +export const getBangumi = (id: string, cache): Promise => + cache.tryGet( + `bilibili:getBangumi:${id}`, + async () => { + const res = await ofetch>('https://api.bilibili.com/pgc/view/web/media', { + query: { + media_id: id, + }, + }); + if (res.result.share_url === undefined) { + // reference: https://api.bilibili.com/pgc/review/user?media_id=${id} + res.result.share_url = `https://www.bilibili.com/bangumi/media/md${res.result.media_id}`; + } + return res.result; + }, + config.cache.routeExpire, + false + ) as Promise; + +/** + * 获取番剧分集信息并缓存 + * + * @param {string} id - 番剧 ID。 + * @param cache - 缓存 module。 + * @returns {Promise} 番剧分集信息。 + */ +export const getBangumiItems = (id: string, cache): Promise => + cache.tryGet( + `bilibili:getBangumiItems:${id}`, + async () => { + const res = await ofetch>('https://api.bilibili.com/pgc/web/season/section', { + query: { + season_id: id, + }, + }); + return res.result; + }, + config.cache.routeExpire, + false + ) as Promise; + +/** + * 使用模板渲染 UGC(用户生成内容)描述。 + * + * @param {boolean} embed - 是否嵌入视频。 + * @param {string} img - 要包含在描述中的图片 URL。 + * @param {string} description - UGC 的文本描述。 + * @param {string} [aid] - 可选。UGC 的 aid。 + * @param {string} [cid] - 可选。UGC 的 cid。 + * @param {string} [bvid] - 可选。UGC 的 bvid。 + * @returns {string} 渲染的 UGC 描述。 + * + * @see https://player.bilibili.com/ 获取更多信息。 + */ +export const renderUGCDescription = (embed: boolean, img: string, description: string, aid?: string, cid?: string, bvid?: string): string => { + // docs: https://player.bilibili.com/ + const rendered = art(path.join(__dirname, 'templates/description.art'), { + embed, + ugc: true, + aid, + cid, + bvid, + img: img.replace('http://', 'https://'), + description, + }); + return rendered; +}; + +/** + * 使用模板渲染 OGV(原创视频)描述。 + * + * @param {boolean} embed - 是否嵌入视频。 + * @param {string} img - 要包含在描述中的图片 URL。 + * @param {string} description - OGV 的文本描述。 + * @param {string} [seasonId] - 可选。OGV 的季 ID。 + * @param {string} [episodeId] - 可选。OGV 的集 ID。 + * @returns {string} 渲染的 OGV 描述。 + * + * @see https://player.bilibili.com/ 获取更多信息。 + */ +export const renderOGVDescription = (embed: boolean, img: string, description: string, seasonId?: string, episodeId?: string): string => { + // docs: https://player.bilibili.com/ + const rendered = art(path.join(__dirname, 'templates/description.art'), { + embed, + ogv: true, + seasonId, + episodeId, + img: img.replace('http://', 'https://'), + description, + }); + return rendered; +}; + +export const getVideoUrl = (bvid?: string) => (bvid ? `https://www.bilibili.com/blackboard/newplayer.html?isOutside=true&autoplay=true&danmaku=true&muted=false&highQuality=true&bvid=${bvid}` : undefined); +export const getLiveUrl = (roomId?: string) => (roomId ? `https://www.bilibili.com/blackboard/live/live-activity-player.html?cid=${roomId}` : undefined); + export default { - iframe, lsid, _uuid, hexsign, addWbiVerifyInfo, getDmImgList, + getDmImgInter, addDmVerifyInfo, + addDmVerifyInfoWithInter, bvidTime, + addRenderData, + getBangumi, + getBangumiItems, + renderUGCDescription, + renderOGVDescription, + getVideoUrl, }; diff --git a/lib/routes/bilibili/video-all.ts b/lib/routes/bilibili/video-all.ts index b0f154d3581ae5..cb43f447eba619 100644 --- a/lib/routes/bilibili/video-all.ts +++ b/lib/routes/bilibili/video-all.ts @@ -5,16 +5,21 @@ import utils from './utils'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: '/user/video-all/:uid/:disableEmbed?', + path: '/user/video-all/:uid/:embed?', name: '用户所有视频', maintainers: [], handler, + example: '/bilibili/user/video-all/2267573', + parameters: { + uid: '用户 id, 可在 UP 主主页中找到', + embed: '默认为开启内嵌视频, 任意值为关闭', + }, categories: ['social-media'], }; async function handler(ctx) { const uid = ctx.req.param('uid'); - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const cookie = await cache.getCookie(); const wbiVerifyString = await cache.getWbiVerifyString(); const dmImgList = utils.getDmImgList(); @@ -74,7 +79,7 @@ async function handler(ctx) { icon: face, item: vlist.map((item) => ({ title: item.title, - description: `${item.description}${disableEmbed ? '' : `

    ${utils.iframe(item.aid)}`}
    `, + description: utils.renderUGCDescription(embed, item.pic, item.description, item.aid, undefined, item.bvid), pubDate: parseDate(item.created, 'X'), link: item.created > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, author: name, diff --git a/lib/routes/bilibili/video.ts b/lib/routes/bilibili/video.ts index 8c8a7d5ed1e044..543ecb2bcc4350 100644 --- a/lib/routes/bilibili/video.ts +++ b/lib/routes/bilibili/video.ts @@ -1,18 +1,19 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import cache from './cache'; -import utils from './utils'; +import utils, { getVideoUrl } from './utils'; import logger from '@/utils/logger'; export const route: Route = { - path: '/user/video/:uid/:disableEmbed?', - categories: ['social-media'], + path: '/user/video/:uid/:embed?', + categories: ['social-media', 'popular'], + view: ViewType.Videos, example: '/bilibili/user/video/2267573', - parameters: { uid: '用户 id, 可在 UP 主主页中找到', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { uid: '用户 id, 可在 UP 主主页中找到', embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: false, requirePuppeteer: false, - antiCrawler: true, + antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, @@ -24,31 +25,27 @@ export const route: Route = { }, ], name: 'UP 主投稿', - maintainers: ['DIYgod'], + maintainers: ['DIYgod', 'Konano', 'pseudoyu'], handler, - description: `:::tip 动态的专栏显示全文 - 可以使用 [UP 主动态](#bilibili-up-zhu-dong-tai)路由作为代替绕过反爬限制 - :::`, }; async function handler(ctx) { const uid = ctx.req.param('uid'); - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const cookie = await cache.getCookie(); const wbiVerifyString = await cache.getWbiVerifyString(); const dmImgList = utils.getDmImgList(); + const dmImgInter = utils.getDmImgInter(); + const renderData = await cache.getRenderData(uid); const [name, face] = await cache.getUsernameAndFaceFromUID(uid); - // await got(`https://space.bilibili.com/${uid}/video?tid=0&page=1&keyword=&order=pubdate`, { - // headers: { - // Referer: `https://space.bilibili.com/${uid}/`, - // Cookie: cookie, - // }, - // }); - const params = utils.addWbiVerifyInfo(utils.addDmVerifyInfo(`mid=${uid}&ps=30&tid=0&pn=1&keyword=&order=pubdate&platform=web&web_location=1550101&order_avoided=true`, dmImgList), wbiVerifyString); + const params = utils.addWbiVerifyInfo( + utils.addRenderData(utils.addDmVerifyInfoWithInter(`mid=${uid}&ps=30&tid=0&pn=1&keyword=&order=pubdate&platform=web&web_location=1550101&order_avoided=true`, dmImgList, dmImgInter), renderData), + wbiVerifyString + ); const response = await got(`https://api.bilibili.com/x/space/wbi/arc/search?${params}`, { headers: { - Referer: `https://space.bilibili.com/${uid}/video?tid=0&page=1&keyword=&order=pubdate`, + Referer: `https://space.bilibili.com/${uid}/video?tid=0&pn=1&keyword=&order=pubdate`, Cookie: cookie, }, }); @@ -62,6 +59,7 @@ async function handler(ctx) { title: `${name} 的 bilibili 空间`, link: `https://space.bilibili.com/${uid}`, description: `${name} 的 bilibili 空间`, + image: face, logo: face, icon: face, item: @@ -70,11 +68,19 @@ async function handler(ctx) { data.data.list.vlist && data.data.list.vlist.map((item) => ({ title: item.title, - description: `${item.description}${disableEmbed ? '' : `

    ${utils.iframe(item.aid)}`}
    `, + description: utils.renderUGCDescription(embed, item.pic, item.description, item.aid, undefined, item.bvid), pubDate: new Date(item.created * 1000).toUTCString(), link: item.created > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, author: name, comments: item.comment, + attachments: item.bvid + ? [ + { + url: getVideoUrl(item.bvid), + mime_type: 'text/html', + }, + ] + : undefined, })), }; } diff --git a/lib/routes/bilibili/vsearch.ts b/lib/routes/bilibili/vsearch.ts index e76126756bd065..401f80eb47f46d 100644 --- a/lib/routes/bilibili/vsearch.ts +++ b/lib/routes/bilibili/vsearch.ts @@ -2,14 +2,18 @@ import { Route } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; import utils from './utils'; -import { queryToBoolean } from '@/utils/readable-social'; import cacheIn from './cache'; export const route: Route = { - path: '/vsearch/:kw/:order?/:disableEmbed?/:tid?', + path: '/vsearch/:kw/:order?/:embed?/:tid?', categories: ['social-media'], example: '/bilibili/vsearch/RSSHub', - parameters: { kw: '检索关键字', order: '排序方式, 综合:totalrank 最多点击:click 最新发布:pubdate(缺省) 最多弹幕:dm 最多收藏:stow', disableEmbed: '默认为开启内嵌视频, 任意值为关闭', tid: '分区 id' }, + parameters: { + kw: '检索关键字', + order: '排序方式, 综合:totalrank 最多点击:click 最新发布:pubdate(缺省) 最多弹幕:dm 最多收藏:stow', + embed: '默认为开启内嵌视频, 任意值为关闭', + tid: '分区 id', + }, features: { requireConfig: [ { @@ -33,15 +37,27 @@ export const route: Route = { handler, description: `分区 id 的取值请参考下表: - | 全部分区 | 动画 | 番剧 | 国创 | 音乐 | 舞蹈 | 游戏 | 知识 | 科技 | 运动 | 汽车 | 生活 | 美食 | 动物圈 | 鬼畜 | 时尚 | 资讯 | 娱乐 | 影视 | 纪录片 | 电影 | 电视剧 | - | -------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | ---- | ---- | ---- | ---- | ---- | ------ | ---- | ------ | - | 0 | 1 | 13 | 167 | 3 | 129 | 4 | 36 | 188 | 234 | 223 | 160 | 211 | 217 | 119 | 155 | 202 | 5 | 181 | 177 | 23 | 11 |`, +| 全部分区 | 动画 | 番剧 | 国创 | 音乐 | 舞蹈 | 游戏 | 知识 | 科技 | 运动 | 汽车 | 生活 | 美食 | 动物圈 | 鬼畜 | 时尚 | 资讯 | 娱乐 | 影视 | 纪录片 | 电影 | 电视剧 | +| -------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | ---- | ---- | ---- | ---- | ---- | ------ | ---- | ------ | +| 0 | 1 | 13 | 167 | 3 | 129 | 4 | 36 | 188 | 234 | 223 | 160 | 211 | 217 | 119 | 155 | 202 | 5 | 181 | 177 | 23 | 11 |`, +}; + +const getIframe = (data, embed: boolean = true) => { + if (!embed) { + return ''; + } + const aid = data?.aid; + const bvid = data?.bvid; + if (aid === undefined && bvid === undefined) { + return ''; + } + return utils.renderUGCDescription(embed, '', '', aid, undefined, bvid); }; async function handler(ctx) { const kw = ctx.req.param('kw'); const order = ctx.req.param('order') || 'pubdate'; - const disableEmbed = queryToBoolean(ctx.req.param('disableEmbed')); + const embed = !ctx.req.param('embed'); const kw_url = encodeURIComponent(kw); const tids = ctx.req.param('tid') ?? 0; const cookie = await cacheIn.getCookie(); @@ -83,8 +99,8 @@ async function handler(ctx) { `Danmaku: ${item.video_review} Comment: ${item.review}
    ` + `
    ${des}
    ` + `
    ` + - `Match By: ${item.hit_columns.join(',')}` + - (disableEmbed ? '' : `

    ${utils.iframe(item.aid)}`), + `Match By: ${item.hit_columns?.join(',') || ''}` + + getIframe(item, embed), pubDate: parseDate(item.pubdate, 'X'), guid: item.arcurl, link: item.arcurl, diff --git a/lib/routes/bilibili/watchlater.ts b/lib/routes/bilibili/watchlater.ts index cb0cf9e31c7a59..2fa1a14c6620ff 100644 --- a/lib/routes/bilibili/watchlater.ts +++ b/lib/routes/bilibili/watchlater.ts @@ -7,10 +7,10 @@ import { parseDate } from '@/utils/parse-date'; import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { - path: '/watchlater/:uid/:disableEmbed?', + path: '/watchlater/:uid/:embed?', categories: ['social-media'], example: '/bilibili/watchlater/2267573', - parameters: { uid: '用户 id', disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { uid: '用户 id', embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: [ { @@ -31,14 +31,14 @@ export const route: Route = { name: '用户稍后再看', maintainers: ['JimenezLi'], handler, - description: `:::warning + description: `::: warning 用户稍后再看需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 - :::`, +:::`, }; async function handler(ctx) { const uid = ctx.req.param('uid'); - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const name = await cache.getUsernameFromUID(uid); const cookie = config.bilibili.cookies[uid]; @@ -62,7 +62,7 @@ async function handler(ctx) { const out = list.map((item) => ({ title: item.title, - description: `${item.desc}

    在稍后再看列表中查看${disableEmbed ? '' : `

    ${utils.iframe(item.aid)}`}
    `, + description: utils.renderUGCDescription(embed, item.pic, `${item.desc}
    在稍后再看列表中查看`, item.aid, undefined, item.bvid), pubDate: parseDate(item.add_at * 1000), link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, author: item.owner.name, diff --git a/lib/routes/bilibili/weekly-recommend.ts b/lib/routes/bilibili/weekly-recommend.ts index 1ab47868422b63..8e214ef0238b89 100644 --- a/lib/routes/bilibili/weekly-recommend.ts +++ b/lib/routes/bilibili/weekly-recommend.ts @@ -3,10 +3,10 @@ import got from '@/utils/got'; import utils from './utils'; export const route: Route = { - path: '/weekly/:disableEmbed?', + path: '/weekly/:embed?', categories: ['social-media'], example: '/bilibili/weekly', - parameters: { disableEmbed: '默认为开启内嵌视频, 任意值为关闭' }, + parameters: { embed: '默认为开启内嵌视频, 任意值为关闭' }, features: { requireConfig: false, requirePuppeteer: false, @@ -21,7 +21,7 @@ export const route: Route = { }; async function handler(ctx) { - const disableEmbed = ctx.req.param('disableEmbed'); + const embed = !ctx.req.param('embed'); const status_response = await got({ method: 'get', @@ -48,12 +48,7 @@ async function handler(ctx) { description: 'B站每周必看', item: data.map((item) => ({ title: item.title, - // description: `${weekly_name} ${item.title}
    ${item.rcmd_reason}
    ${!disableEmbed ? `${utils.iframe(item.param)}` : ''}`, - description: ` - ${weekly_name} ${item.title}
    - ${item.rcmd_reason}
    - ${disableEmbed ? '' : utils.iframe(item.param)} - `, + description: utils.renderUGCDescription(embed, item.cover, `${weekly_name} ${item.title} - ${item.rcmd_reason}`, item.param, undefined, item.bvid), link: weekly_number > 60 && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.param}`, })), }; diff --git a/lib/routes/binance/announcement.ts b/lib/routes/binance/announcement.ts new file mode 100644 index 00000000000000..cf641f7609df4f --- /dev/null +++ b/lib/routes/binance/announcement.ts @@ -0,0 +1,169 @@ +import { DataItem, Route, ViewType } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import * as cheerio from 'cheerio'; +import { AnnouncementCatalog, AnnouncementsConfig } from './types'; + +interface AnnouncementFragment { + reactRoot: [{ id: 'Fragment'; children: { id: string; props: object }[]; props: object }]; +} + +const ROUTE_PARAMETERS_CATALOGID_MAPPING = { + 'new-cryptocurrency-listing': 48, + 'latest-binance-news': 49, + 'latest-activities': 93, + 'new-fiat-listings': 50, + 'api-updates': 51, + 'crypto-airdrop': 128, + 'wallet-maintenance-updates': 157, + delisting: 161, +}; + +function assertAnnouncementsConfig(playlist: unknown): playlist is AnnouncementFragment { + if (!playlist || typeof playlist !== 'object') { + return false; + } + if (!('reactRoot' in (playlist as { reactRoot: unknown[] }))) { + return false; + } + if (!Array.isArray((playlist as { reactRoot: unknown[] }).reactRoot)) { + return false; + } + if ((playlist as { reactRoot: { id: string }[] }).reactRoot?.[0]?.id !== 'Fragment') { + return false; + } + return true; +} + +function assertAnnouncementsConfigList(props: unknown): props is { config: { list: AnnouncementsConfig[] } } { + if (!props || typeof props !== 'object') { + return false; + } + if (!('config' in props)) { + return false; + } + if (!('list' in (props.config as { list: AnnouncementsConfig[] }))) { + return false; + } + return true; +} + +const handler: Route['handler'] = async (ctx) => { + const baseUrl = 'https://www.binance.com'; + const announcementCategoryUrl = `${baseUrl}/support/announcement`; + const { type } = ctx.req.param<'/binance/announcement/:type'>(); + const language = ctx.req.header('Accept-Language'); + const headers = { + Referer: baseUrl, + 'Accept-Language': language ?? 'en-US,en;q=0.9', + }; + const announcementsConfig = (await cache.tryGet(`binance:announcements:${language}`, async () => { + const announcementRes = await ofetch(announcementCategoryUrl, { headers }); + const $ = cheerio.load(announcementRes); + + const appData = JSON.parse($('#__APP_DATA').text()); + + const announcements = Object.values(appData.appState.loader.dataByRouteId as Record).find((value) => 'playlist' in value) as { playlist: unknown }; + + if (!assertAnnouncementsConfig(announcements.playlist)) { + throw new Error('Get announcement config failed'); + } + + const listConfigProps = announcements.playlist.reactRoot[0].children.find((i) => i.id === 'TopicCardList')?.props; + + if (!assertAnnouncementsConfigList(listConfigProps)) { + throw new Error("Can't get announcement config list"); + } + + return listConfigProps.config.list; + })) as AnnouncementsConfig[]; + + const announcementCatalogId = ROUTE_PARAMETERS_CATALOGID_MAPPING[type]; + + if (!announcementCatalogId) { + throw new Error(`${type} is not supported`); + } + + const targetItem = announcementsConfig.find((i) => i.url.includes(`c-${announcementCatalogId}`)); + + if (!targetItem) { + throw new Error('Unexpected announcements config'); + } + + const link = new URL(targetItem.url, baseUrl).toString(); + + const response = await ofetch(link, { headers }); + + const $ = cheerio.load(response); + const appData = JSON.parse($('#__APP_DATA').text()); + + const values = Object.values(appData.appState.loader.dataByRouteId as Record); + const catalogs = values.find((value) => 'catalogs' in value) as { catalogs: AnnouncementCatalog[] }; + const catalog = catalogs.catalogs.find((catalog) => catalog.catalogId === announcementCatalogId); + + const item = await Promise.all( + catalog!.articles.map((i) => { + const link = `${announcementCategoryUrl}/${i.code}`; + const item = { + title: i.title, + link, + description: i.title, + pubDate: parseDate(i.releaseDate), + } as DataItem; + return cache.tryGet(`binance:announcement:${i.code}:${language}`, async () => { + const res = await ofetch(link, { headers }); + const $ = cheerio.load(res); + const descriptionEl = $('#support_article > div').first(); + descriptionEl.find('style').remove(); + item.description = descriptionEl.html() ?? ''; + return item; + }) as Promise; + }) + ); + + return { + title: targetItem.title, + link, + description: targetItem.description, + item, + }; +}; + +export const route: Route = { + path: '/announcement/:type', + categories: ['finance', 'popular'], + view: ViewType.Articles, + example: '/binance/announcement/new-cryptocurrency-listing', + parameters: { + type: { + description: 'Binance Announcement type', + default: 'new-cryptocurrency-listing', + options: [ + { value: 'new-cryptocurrency-listing', label: 'New Cryptocurrency Listing' }, + { value: 'latest-binance-news', label: 'Latest Binance News' }, + { value: 'latest-activities', label: 'Latest Activities' }, + { value: 'new-fiat-listings', label: 'New Fiat Listings' }, + { value: 'api-updates', label: 'API Updates' }, + { value: 'crypto-airdrop', label: 'Crypto Airdrop' }, + { value: 'wallet-maintenance-updates', label: 'Wallet Maintenance Updates' }, + { value: 'delisting', label: 'Delisting' }, + ], + }, + }, + name: 'Announcement', + description: ` +Type category + + - new-cryptocurrency-listing => New Cryptocurrency Listing + - latest-binance-news => Latest Binance News + - latest-activities => Latest Activities + - new-fiat-listings => New Fiat Listings + - api-updates => API Updates + - crypto-airdrop => Crypto Airdrop + - wallet-maintenance-updates => Wallet Maintenance Updates + - delisting => Delisting +`, + maintainers: ['enpitsulin'], + handler, +}; diff --git a/lib/routes/binance/namespace.ts b/lib/routes/binance/namespace.ts index 1e520389802c3f..361bdeb4cf7292 100644 --- a/lib/routes/binance/namespace.ts +++ b/lib/routes/binance/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Binance', url: 'binance.com', + lang: 'en', }; diff --git a/lib/routes/binance/types.ts b/lib/routes/binance/types.ts new file mode 100644 index 00000000000000..4078150e634a9c --- /dev/null +++ b/lib/routes/binance/types.ts @@ -0,0 +1,26 @@ +export interface AnnouncementsConfig { + title: string; + description: string; + url: string; + imgUrl: string; +} + +export interface AnnouncementCatalog { + articles: AnnouncementArticle[]; + catalogId: number; + catalogName: string; + catalogType: 1; + catalogs: []; + description: null; + icon: string; + parentCatalogId: null; + total: number; +} + +export interface AnnouncementArticle { + id: number; + code: string; + title: string; + type: number; + releaseDate: number; +} diff --git a/lib/routes/bing/daily-wallpaper.ts b/lib/routes/bing/daily-wallpaper.ts index 10b1460ac643f8..4545b273ad67ee 100644 --- a/lib/routes/bing/daily-wallpaper.ts +++ b/lib/routes/bing/daily-wallpaper.ts @@ -4,38 +4,81 @@ import { parseDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; export const route: Route = { - path: '/', + path: '/:routeParams?', + parameters: { + routeParams: '额外参数type,story和lang:请参阅以下说明和表格', + }, radar: [ + { + source: ['www.bing.com/'], + target: '', + }, { source: ['cn.bing.com/'], target: '', }, ], name: '每日壁纸', - maintainers: ['FHYunCai'], + maintainers: ['FHYunCai', 'LLLLLFish'], handler, - url: 'cn.bing.com/', + url: 'www.bing.com/', + example: '/bing/type=UHD&story=1&lang=zh-CN', + description: `| 参数 | 含义 | 接受的值 | 默认值 | 备注 | +|-------|--------------------|-----------------------------------------------------------|-----------|--------------------------------------------------------| +| type | 输出壁纸的像素类型 | UHD/1920x1080/1920x1200/768x1366/1080x1920/1080x1920_logo | 1920x1080 | 1920x1200与1080x1920_logo带有水印,输入的值不在接受范围内都会输出成1920x1080 | +| story | 是否输出壁纸的故事 | 1/0 | 0 | 输入的值不为1都不会输出故事 | +| lang | 输出壁纸图文的地区(中文或者是英文) | zh/en | zh | zh/en输出的壁纸图文不一定是一样的;如果en不生效,试着部署到其他地方 | +`, }; async function handler(ctx) { - const response = await ofetch('HPImageArchive.aspx', { - baseURL: 'https://cn.bing.com', + const routeParams = new URLSearchParams(ctx.req.param('routeParams')); + let type = routeParams.get('type') || '1920x1080'; + let lang = routeParams.get('lang'); + let apiUrl = ''; + const allowedTypes = ['UHD', '1920x1080', '1920x1200', '768x1366', '1080x1920', '1080x1920_logo']; + if (lang !== 'zh' && lang !== 'en') { + lang = 'zh'; + } + if (lang === 'zh') { + lang = 'zh-CN'; + apiUrl = 'https://cn.bing.com'; + } else { + lang = 'en-US'; + apiUrl = 'https://www.bing.com'; + } + if (!allowedTypes.includes(type)) { + type = '1920x1080'; + } + const story = routeParams.get('story') === '1'; + const resp = await ofetch('/hp/api/model', { + baseURL: apiUrl, + method: 'GET', query: { - format: 'js', - idx: 0, - n: ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 7, - mkt: 'zh-CN', + mtk: lang, }, }); - const data = response; + const items = resp.MediaContents.map((item) => { + const ssd = item.Ssd; + const link = `${apiUrl}${item.ImageContent.Image.Url.match(/\/th\?id=[^_]+_[^_]+/)[0].replace(/(_\d+x\d+\.webp)$/i, '')}_${type}.jpg`; + let description = `Article Cover Image
    `; + if (story) { + description += `${item.ImageContent.Headline}`; + description += `${item.ImageContent.QuickFact.MainText}
    `; + description += `

    ${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: ``, - 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(announcementApiUrl, { + method: 'POST', + body: reqBody, + headers, + }); + if (result?.code !== '200') { + throw new Error('Failed to fetch announcements, error code: ' + result?.code); + } + return result; + }, + config.cache.routeExpire, + false + )) as BitgetResponse; + + if (!response) { + throw new Error('Failed to fetch announcements'); + } + const items = response.data.items; + const data = await Promise.all( + items.map( + (item) => + cache.tryGet(`bitget:announcement:${item.id}:${pageSize}:${lang}`, async () => { + // 从 unix 时间戳转换为日期 + const date = parseDate(Number(item.sendTime)); + const dataItem: DataItem = { + title: item.title ?? '', + link: item.openUrl ?? '', + pubDate: item.sendTime ? date : undefined, + description: item.content ?? '', + }; + + if (item.imgUrl) { + dataItem.image = item.imgUrl; + } + + if (item.stationLetterType === '01' || item.stationLetterType === '06') { + try { + const itemResponse = await ofetch(item.openUrl ?? '', { + headers, + }); + const $ = load(itemResponse); + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + dataItem.description = nextData.props.pageProps.details?.content || nextData.props.pageProps.pageInitInfo?.ruleContent || item.content || ''; + } catch (error: any) { + if (error.name && (error.name === 'HTTPError' || error.name === 'RequestError' || error.name === 'FetchError')) { + dataItem.description = item.content ?? ''; + } else { + throw error; + } + } + } + return dataItem; + }) as Promise + ) + ); + + return { + title: `Bitget | ${findTypeLabel(type)}`, + link: `https://www.bitget.com/${lang}/inmail`, + item: data, + }; +}; + +const findTypeLabel = (type: string) => { + const typeMap = { + all: 'All', + 'new-listing': 'New Listing', + 'latest-activities': 'Latest Activities', + 'new-announcement': 'New Announcement', + }; + return typeMap[type]; +}; + +export const route: Route = { + path: '/announcement/:type/:lang?', + categories: ['finance', 'popular'], + view: ViewType.Articles, + example: '/bitget/announcement/all/zh-CN', + parameters: { + type: { + description: 'Bitget 通知类型', + default: 'all', + options: [ + { value: 'all', label: '全部通知' }, + { value: 'new-listing', label: '新币上线' }, + { value: 'latest-activities', label: '最新活动' }, + { value: 'new-announcement', label: '最新公告' }, + ], + }, + lang: { + description: '语言', + default: 'zh-CN', + options: [ + { value: 'zh-CN', label: '中文' }, + { value: 'en-US', label: 'English' }, + { value: 'es-ES', label: 'Español' }, + { value: 'fr-FR', label: 'Français' }, + { value: 'de-DE', label: 'Deutsch' }, + { value: 'ja-JP', label: '日本語' }, + { value: 'ru-RU', label: 'Русский' }, + { value: 'ar-SA', label: 'العربية' }, + ], + }, + }, + radar: [ + { + source: ['www.bitget.com/:lang/inmail'], + target: '/announcement/all/:lang', + }, + ], + name: 'Announcement', + description: ` +type: +| Type | Description | +| --- | --- | +| all | 全部通知 | +| new-listing | 新币上线 | +| latest-activities | 最新活动 | +| new-announcement | 最新公告 | + +lang: +| Lang | Description | +| --- | --- | +| zh-CN | 中文 | +| en-US | English | +| es-ES | Español | +| fr-FR | Français | +| de-DE | Deutsch | +| ja-JP | 日本語 | +| ru-RU | Русский | +| ar-SA | العربية | +`, + maintainers: ['YukiCoco'], + handler, +}; diff --git a/lib/routes/bitget/namespace.ts b/lib/routes/bitget/namespace.ts new file mode 100644 index 00000000000000..808a994ef2fe24 --- /dev/null +++ b/lib/routes/bitget/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Bitget', + url: 'bitget.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/bitget/type.ts b/lib/routes/bitget/type.ts new file mode 100644 index 00000000000000..3e87c360b48c22 --- /dev/null +++ b/lib/routes/bitget/type.ts @@ -0,0 +1,26 @@ +export interface BitgetResponse { + code: string; + data: { + endId: string; + hasNextPage: boolean; + hasPrePage: boolean; + items: Array<{ + businessType?: number; + content?: string; + id: string; + imgUrl?: string; + openUrl?: string; + openUrlName?: string; + readStats?: number; + sendTime?: string; + stationLetterType?: string; + title?: string; + }>; + notifyFlag: boolean; + page: number; + pageSize: number; + startId: string; + total: number; + }; + params: any[]; +} diff --git a/lib/routes/bitmovin/namespace.ts b/lib/routes/bitmovin/namespace.ts index da019512359691..85fa757746ab20 100644 --- a/lib/routes/bitmovin/namespace.ts +++ b/lib/routes/bitmovin/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Bitmovin', url: 'bitmovin.com', + lang: 'en', }; diff --git a/lib/routes/bjfu/it/index.ts b/lib/routes/bjfu/it/index.ts index 619540d9790409..4da69a4659d91f 100644 --- a/lib/routes/bjfu/it/index.ts +++ b/lib/routes/bjfu/it/index.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['wzc-blog'], handler, description: `| 学院新闻 | 科研动态 | 本科生培养 | 研究生培养 | - | -------- | -------- | ---------- | ---------- | - | xyxw | kydt | pydt | pydt2 |`, +| -------- | -------- | ---------- | ---------- | +| xyxw | kydt | pydt | pydt2 |`, }; async function handler(ctx) { diff --git a/lib/routes/bjfu/jwc/index.ts b/lib/routes/bjfu/jwc/index.ts index 14f01020448ecd..6061aa69d386dd 100644 --- a/lib/routes/bjfu/jwc/index.ts +++ b/lib/routes/bjfu/jwc/index.ts @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['markmingjie'], handler, description: `| 教务快讯 | 考试信息 | 课程信息 | 教改动态 | 图片新闻 | - | -------- | -------- | -------- | -------- | -------- | - | jwkx | ksxx | kcxx | jgdt | tpxw |`, +| -------- | -------- | -------- | -------- | -------- | +| jwkx | ksxx | kcxx | jgdt | tpxw |`, }; async function handler(ctx) { diff --git a/lib/routes/bjfu/namespace.ts b/lib/routes/bjfu/namespace.ts index a23862f818f0b1..91be22f07922da 100644 --- a/lib/routes/bjfu/namespace.ts +++ b/lib/routes/bjfu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京林业大学', url: 'graduate.bjfu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/bjfu/news/index.ts b/lib/routes/bjfu/news/index.ts index 588743899b1fcf..1e6edb77473ef2 100644 --- a/lib/routes/bjfu/news/index.ts +++ b/lib/routes/bjfu/news/index.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['markmingjie'], handler, description: `| 绿色要闻 | 校园动态 | 教学科研 | 党建思政 | 一周排行 | - | -------- | -------- | -------- | -------- | -------- | - | lsyw | xydt | jxky | djsz | yzph |`, +| -------- | -------- | -------- | -------- | -------- | +| lsyw | xydt | jxky | djsz | yzph |`, }; async function handler(ctx) { diff --git a/lib/routes/bjnews/cat.ts b/lib/routes/bjnews/cat.ts new file mode 100644 index 00000000000000..4ce068e8fc0183 --- /dev/null +++ b/lib/routes/bjnews/cat.ts @@ -0,0 +1,51 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; + +import { fetchArticle } from './utils'; +import asyncPool from 'tiny-async-pool'; + +export const route: Route = { + path: '/cat/:cat', + categories: ['traditional-media'], + example: '/bjnews/cat/depth', + parameters: { cat: '分类, 可从URL中找到' }, + features: {}, + radar: [ + { + source: ['www.bjnews.com.cn/:cat'], + }, + ], + name: '分类', + maintainers: ['dzx-dzx'], + handler, + url: 'www.bjnews.com.cn', +}; + +async function handler(ctx) { + const url = `https://www.bjnews.com.cn/${ctx.req.param('cat')}`; + const res = await ofetch(url); + const $ = load(res); + const list = $('#waterfall-container .pin_demo > a') + .toArray() + .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 15) + .map((a) => ({ + title: $(a).text(), + link: $(a).attr('href'), + category: $(a).parent().find('.source').text().trim(), + })); + + const out = await asyncPoolAll(2, list, (item) => fetchArticle(item)); + return { + title: `新京报 - 分类 - ${$('.cur').text().trim()}`, + link: url, + item: out, + }; +} +async function asyncPoolAll(poolLimit: number, array: readonly IN[], iteratorFn: (generator: IN) => Promise) { + const results: Awaited = []; + for await (const result of asyncPool(poolLimit, array, iteratorFn)) { + results.push(result); + } + return results; +} diff --git a/lib/routes/bjnews/column.ts b/lib/routes/bjnews/column.ts new file mode 100644 index 00000000000000..709138563d5539 --- /dev/null +++ b/lib/routes/bjnews/column.ts @@ -0,0 +1,43 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +import { fetchArticle } from './utils'; + +export const route: Route = { + path: '/column/:column', + categories: ['traditional-media'], + example: '/bjnews/column/204', + parameters: { column: '栏目ID, 可从手机版网页URL中找到' }, + features: {}, + radar: [ + { + source: ['m.bjnews.com.cn/column/:column.htm'], + }, + ], + name: '分类', + maintainers: ['dzx-dzx'], + handler, + url: 'www.bjnews.com.cn', +}; + +async function handler(ctx) { + const columnID = ctx.req.param('column'); + const url = `https://api.bjnews.com.cn/api/v101/news/column_news.php?column_id=${columnID}`; + const res = await ofetch(url); + const list = res.data.slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 15).map((e) => ({ + title: e.row.title, + guid: e.uuid, + pubDate: timezone(parseDate(e.row.publish_time), +8), + updated: timezone(parseDate(e.row.update_time), +8), + link: `https://www.bjnews.com.cn/detail/${e.uuid}.html`, + })); + + const out = await Promise.all(list.map((item) => fetchArticle(item))); + return { + title: `新京报 - 栏目 - ${res.data[0].row.column_info[0].column_name}`, + link: `https://m.bjnews.com.cn/column/${columnID}.html`, + item: out, + }; +} diff --git a/lib/routes/bjnews/namespace.ts b/lib/routes/bjnews/namespace.ts new file mode 100644 index 00000000000000..9a69d93d2f7a2d --- /dev/null +++ b/lib/routes/bjnews/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '新京报', + url: 'www.bjnews.com.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/bjnews/utils.ts b/lib/routes/bjnews/utils.ts new file mode 100644 index 00000000000000..5b0496a7f55d45 --- /dev/null +++ b/lib/routes/bjnews/utils.ts @@ -0,0 +1,19 @@ +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +export function fetchArticle(item) { + return cache.tryGet(item.link, async () => { + const responses = await ofetch(item.link); + const $d = load(responses); + // $d('img').each((i, e) => $(e).attr('referrerpolicy', 'no-referrer')); + + item.pubDate = timezone(parseDate($d('.left-info .timer').text()), +8); + item.author = $d('.left-info .reporter').text(); + item.description = $d('#contentStr').html(); + + return item; + }); +} diff --git a/lib/routes/bjp/apod.ts b/lib/routes/bjp/apod.ts index b883fabc6aca20..8258b102c4ee7a 100644 --- a/lib/routes/bjp/apod.ts +++ b/lib/routes/bjp/apod.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import { load } from 'cheerio'; import got from '@/utils/got'; @@ -7,7 +7,8 @@ import timezone from '@/utils/timezone'; export const route: Route = { path: '/apod', - categories: ['picture'], + categories: ['picture', 'popular'], + view: ViewType.Pictures, example: '/bjp/apod', parameters: {}, features: { @@ -29,7 +30,7 @@ export const route: Route = { url: 'bjp.org.cn/APOD/today.shtml', }; -async function handler() { +async function handler(ctx) { const baseUrl = 'https://www.bjp.org.cn'; const listUrl = `${baseUrl}/APOD/list.shtml`; @@ -46,7 +47,9 @@ async function handler() { link: `${baseUrl}${e.find('a').attr('href')}`, pubDate: timezone(parseDate(e.find('span').text().replace(':', ''), 'YYYY-MM-DD'), 8), }; - }); + }) + .sort((a, b) => b.pubDate - a.pubDate) + .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10); const items = await Promise.all( list.map((e) => diff --git a/lib/routes/bjp/namespace.ts b/lib/routes/bjp/namespace.ts index 718f28575df5ea..3a5e74689f2fdc 100644 --- a/lib/routes/bjp/namespace.ts +++ b/lib/routes/bjp/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京天文馆', - url: 'bjp.org.cn', + url: 'www.bjp.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/bjsk/index.ts b/lib/routes/bjsk/index.ts index 7002e79511a37a..907b9c7706f69e 100644 --- a/lib/routes/bjsk/index.ts +++ b/lib/routes/bjsk/index.ts @@ -22,11 +22,11 @@ export const route: Route = { name: '通用', maintainers: ['TonyRL'], handler, - description: `:::tip + description: `::: tip 路径处填写对应页面 URL 中 \`https://www.bjsk.org.cn/\` 和 \`.html\` 之间的字段。下面是一个例子。 若订阅 [社科资讯 > 社科要闻](https://www.bjsk.org.cn/newslist-1394-1474-0.html) 则将对应页面 URL \`https://www.bjsk.org.cn/newslist-1394-1474-0.html\` 中 \`https://www.bjsk.org.cn/\` 和 \`.html\` 之间的字段 \`newslist-1394-1474-0\` 作为路径填入。此时路由为 [\`/bjsk/newslist-1394-1474-0\`](https://rsshub.app/bjsk/newslist-1394-1474-0) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/bjsk/keti.ts b/lib/routes/bjsk/keti.ts index 8139ae535cd95b..fef951a68414df 100644 --- a/lib/routes/bjsk/keti.ts +++ b/lib/routes/bjsk/keti.ts @@ -28,8 +28,8 @@ export const route: Route = { handler, url: 'keti.bjsk.org.cn/indexAction!to_index.action', description: `| 通知公告 | 资料下载 | - | -------------------------------- | -------------------------------- | - | 402881027cbb8c6f017cbb8e17710002 | 2c908aee818e04f401818e08645c0002 |`, +| -------------------------------- | -------------------------------- | +| 402881027cbb8c6f017cbb8e17710002 | 2c908aee818e04f401818e08645c0002 |`, }; async function handler(ctx) { diff --git a/lib/routes/bjsk/namespace.ts b/lib/routes/bjsk/namespace.ts index 8a782b2f088e68..98b823b5d52114 100644 --- a/lib/routes/bjsk/namespace.ts +++ b/lib/routes/bjsk/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京社科网', url: 'bjsk.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/bjtu/gs.ts b/lib/routes/bjtu/gs.ts new file mode 100644 index 00000000000000..e702c0b0111c97 --- /dev/null +++ b/lib/routes/bjtu/gs.ts @@ -0,0 +1,224 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +const rootURL = 'https://gs.bjtu.edu.cn'; +const urlCms = `${rootURL}/cms/item/?tag=`; +const urlZszt = `${rootURL}/cms/zszt/item/?cat=`; +const title = ' - 北京交通大学研究生院'; +const zsztRegex = /_zszt/; +const struct = { + noti_zs: { + selector: { + list: '.tab-content li', + }, + tag: 1, + name: '通知公告_招生', + }, + noti: { + selector: { + list: '.tab-content li', + }, + tag: 2, + name: '通知公告', + }, + news: { + selector: { + list: '.tab-content li', + }, + tag: 3, + name: '新闻动态', + }, + zsxc: { + selector: { + list: '.tab-content li', + }, + tag: 4, + name: '招生宣传', + }, + py: { + selector: { + list: '.tab-content li', + }, + tag: 5, + name: '培养', + }, + zs: { + selector: { + list: '.tab-content li', + }, + tag: 6, + name: '招生', + }, + xw: { + selector: { + list: '.tab-content li', + }, + tag: 7, + name: '学位', + }, + ygb: { + selector: { + list: '.tab-content li', + }, + tag: 8, + name: '研工部', + }, + ygbtzgg: { + selector: { + list: '.tab-content li', + }, + tag: 9, + name: '通知公告 - 研工部', + }, + ygbnews: { + selector: { + list: '.tab-content li', + }, + tag: 10, + name: '新闻动态 - 研工部', + }, + ygbnewscover: { + selector: { + list: '.tab-content li', + }, + tag: 11, + name: '新闻封面 - 研工部', + }, + all: { + selector: { + list: '.tab-content li', + }, + tag: 12, + name: '文章列表', + }, + bszs_zszt: { + selector: { + list: '.mainleft_box li', + }, + tag: 2, + name: '博士招生 - 招生专题', + }, + sszs_zszt: { + selector: { + list: '.mainleft_box li', + }, + tag: 3, + name: '硕士招生 - 招生专题', + }, + zsjz_zszt: { + selector: { + list: '.mainleft_box li', + }, + tag: 4, + name: '招生简章 - 招生专题', + }, + zcfg_zszt: { + selector: { + list: '.mainleft_box li', + }, + tag: 5, + name: '政策法规 - 招生专题', + }, +}; + +const getItem = (item, selector) => { + const newsInfo = item.find('a'); + const newsDate = item + .find('span') + .text() + .match(/\d{4}(-|\/|.)\d{1,2}\1\d{1,2}/)[0]; + + const infoTitle = newsInfo.text(); + const link = rootURL + newsInfo.attr('href'); + return cache.tryGet(link, async () => { + const resp = await ofetch(link); + const $$ = load(resp); + const infoText = $$(selector).html(); + + return { + title: infoTitle, + pubDate: parseDate(newsDate), + link, + description: infoText, + }; + }) as any; +}; + +export const route: Route = { + path: '/gs/:type?', + categories: ['university'], + example: '/bjtu/gs/noti', + parameters: { type: 'Article type' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['gs.bjtu.edu.cn'], + }, + ], + name: '研究生院', + maintainers: ['E1nzbern'], + handler, + description: ` +| 文章来源 | 参数 | +| ----------------- | ------------ | +| 通知公告_招生 | noti_zs | +| 通知公告 | noti | +| 新闻动态 | news | +| 招生宣传 | zsxc | +| 培养 | py | +| 招生 | zs | +| 学位 | xw | +| 研工部 | ygb | +| 通知公告 - 研工部 | ygbtzgg | +| 新闻动态 - 研工部 | ygbnews | +| 新闻封面 - 研工部 | ygbnewscover | +| 文章列表 | all | +| 博士招生 - 招生专题 | bszs_zszt | +| 硕士招生 - 招生专题 | sszs_zszt | +| 招生简章 - 招生专题 | zsjz_zszt | +| 政策法规 - 招生专题 | zcfg_zszt | + +::: tip + 文章来源的命名均来自研究生院网站标题。 + 最常用的几项有“通知公告_招生”、“通知公告”、“博士招生 - 招生专题”、“硕士招生 - 招生专题”。 +:::`, +}; + +async function handler(ctx) { + const { type = 'noti' } = ctx.req.param(); + let url = urlCms; + let selectorArticle = 'div.main_left.main_left_list'; + if (zsztRegex.test(type)) { + url = urlZszt; + selectorArticle = 'div.mainleft_box'; + } + const urlAddr = `${url}${struct[type].tag}`; + const resp = await ofetch(urlAddr); + const $ = load(resp); + + const list = $(struct[type].selector.list); + + const items = await Promise.all( + list.toArray().map((i) => { + const item = $(i); + return getItem(item, selectorArticle); + }) + ); + + return { + title: `${struct[type].name}${title}`, + link: urlAddr, + item: items, + allowEmpty: true, + }; +} diff --git a/lib/routes/bjtu/namespace.ts b/lib/routes/bjtu/namespace.ts new file mode 100644 index 00000000000000..4fd3ddb7a0c258 --- /dev/null +++ b/lib/routes/bjtu/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; +export const namespace: Namespace = { + name: 'Beijing Jiaotong University', + url: 'bjtu.edu.cn', + zh: { + name: '北京交通大学', + }, + lang: 'zh-CN', +}; diff --git a/lib/routes/bjwxdxh/index.ts b/lib/routes/bjwxdxh/index.ts index a39b263ad78771..c1c27c1989bf5f 100644 --- a/lib/routes/bjwxdxh/index.ts +++ b/lib/routes/bjwxdxh/index.ts @@ -22,8 +22,8 @@ export const route: Route = { maintainers: ['Misaka13514'], handler, description: `| 协会活动 | 公告通知 | 会议情况 | 简报 | 政策法规 | 学习园地 | 业余无线电服务中心 | 经验交流 | 新技术推介 | 活动通知 | 爱好者园地 | 结果查询 | 资料下载 | 会员之家 | 会员简介 | 会员风采 | 活动报道 | - | -------- | -------- | -------- | ---- | -------- | -------- | ------------------ | -------- | ---------- | -------- | ---------- | -------- | -------- | -------- | -------- | -------- | -------- | - | 86 | 99 | 102 | 103 | 106 | 107 | 108 | 111 | 112 | 114 | 115 | 116 | 118 | 119 | 120 | 121 | 122 |`, +| -------- | -------- | -------- | ---- | -------- | -------- | ------------------ | -------- | ---------- | -------- | ---------- | -------- | -------- | -------- | -------- | -------- | -------- | +| 86 | 99 | 102 | 103 | 106 | 107 | 108 | 111 | 112 | 114 | 115 | 116 | 118 | 119 | 120 | 121 | 122 |`, }; async function handler(ctx) { diff --git a/lib/routes/bjwxdxh/namespace.ts b/lib/routes/bjwxdxh/namespace.ts index 8e077db91e69cb..912e1b7b3685fd 100644 --- a/lib/routes/bjwxdxh/namespace.ts +++ b/lib/routes/bjwxdxh/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京无线电协会', url: 'www.bjwxdxh.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/bjx/fd.ts b/lib/routes/bjx/fd.ts new file mode 100644 index 00000000000000..9d6c4d68138613 --- /dev/null +++ b/lib/routes/bjx/fd.ts @@ -0,0 +1,63 @@ +import { DataItem, Route } from '@/types'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/fd/:type', + categories: ['traditional-media'], + example: '/bjx/fd/yw', + parameters: { type: '文章分类,详见下表' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '风电', + maintainers: ['hualiong'], + description: `\`:type\` 类型可选如下 + +| 要闻 | 政策 | 数据 | 市场 | 企业 | 招标 | 技术 | 报道 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| yw | zc | sj | sc | mq | zb | js | bd |`, + handler: async (ctx) => { + const type = ctx.req.param('type'); + const response = await ofetch(`https://fd.bjx.com.cn/${type}/`); + + const $ = load(response); + const typeName = $('div.box2 em:last-child').text(); + const list = $('div.cc-list-content ul li:nth-child(-n+20)') + .toArray() + .map((item): DataItem => { + const each = $(item); + return { + title: each.find('a').attr('title')!, + link: each.find('a').attr('href'), + pubDate: parseDate(each.find('span').text()), + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link!, async () => { + const response = await ofetch(item.link!); + const $ = load(response); + + item.description = $('#article_cont').html()!; + return item; + }) + ) + ); + + return { + title: `北极星风力发电网${typeName}`, + description: $('meta[name="Description"]').attr('content'), + link: `https://fd.bjx.com.cn/${type}/`, + item: items as DataItem[], + }; + }, +}; diff --git a/lib/routes/bjx/namespace.ts b/lib/routes/bjx/namespace.ts index 357b9ad4a39152..3904d7fa61a7b2 100644 --- a/lib/routes/bjx/namespace.ts +++ b/lib/routes/bjx/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北极星电力网', - url: 'guangfu.bjx.com.cn', + url: 'www.bjx.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/bjx/types.ts b/lib/routes/bjx/types.ts index 90d6a9a3b1ca1e..e7e36a6016ae16 100644 --- a/lib/routes/bjx/types.ts +++ b/lib/routes/bjx/types.ts @@ -21,9 +21,9 @@ export const route: Route = { handler, description: `\`:type\` 类型可选如下 - | 要闻 | 政策 | 市场行情 | 企业动态 | 独家观点 | 项目工程 | 招标采购 | 财经 | 国际行情 | 价格趋势 | 技术跟踪 | - | ---- | ---- | -------- | -------- | -------- | -------- | -------- | ---- | -------- | -------- | -------- | - | yw | zc | sc | mq | dj | xm | zb | cj | gj | sj | js |`, +| 要闻 | 政策 | 市场行情 | 企业动态 | 独家观点 | 项目工程 | 招标采购 | 财经 | 国际行情 | 价格趋势 | 技术跟踪 | +| ---- | ---- | -------- | -------- | -------- | -------- | -------- | ---- | -------- | -------- | -------- | +| yw | zc | sc | mq | dj | xm | zb | cj | gj | sj | js |`, }; async function handler(ctx) { diff --git a/lib/routes/blizzard/namespace.ts b/lib/routes/blizzard/namespace.ts index f9bf3c5ca8def9..4f1141a3fe3e99 100644 --- a/lib/routes/blizzard/namespace.ts +++ b/lib/routes/blizzard/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Blizzard', url: 'news.blizzard.com', + lang: 'en', }; diff --git a/lib/routes/blizzard/news-cn.ts b/lib/routes/blizzard/news-cn.ts new file mode 100644 index 00000000000000..b13237a7547dbf --- /dev/null +++ b/lib/routes/blizzard/news-cn.ts @@ -0,0 +1,130 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/news-cn/:category?', + categories: ['game'], + example: '/blizzard/news-cn/ow', + parameters: { category: '游戏类别, 默认为 ow' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['ow.blizzard.cn', 'wow.blizzard.cn', 'hs.blizzard.cn'], + target: '/news-cn/', + }, + ], + name: '暴雪游戏国服新闻', + maintainers: ['zhangpeng2k'], + description: ` +| 守望先锋 | 炉石传说 | 魔兽世界 | +|----------|----------|---------| +| ow | hs | wow | +`, + handler, +}; + +const categoryNames = { + ow: '守望先锋', + hs: '炉石传说', + wow: '魔兽世界', +}; + +/* 列表解析逻辑 */ +const parsers = { + ow: ($) => + $('.list-data-container .list-item-container') + .toArray() + .map((item) => { + item = $(item); + return { + title: item.find('.content-title').text(), + link: item.find('.fill-link').attr('href'), + description: item.find('.content-intro').text(), + pubDate: parseDate(item.find('.content-date').text()), + image: item.find('.item-pic').attr('src'), + }; + }), + hs: ($) => + $('.article-container>a') + .toArray() + .map((item) => { + item = $(item); + return { + title: item.find('.title').text(), + link: item.attr('href'), + description: item.find('.desc').text(), + pubDate: parseDate(item.find('.date').attr('data-time')), + image: item.find('.article-img img').attr('src'), + }; + }), + wow: ($) => + $('.Pane-list>a') + .toArray() + .map((item) => { + item = $(item); + return { + title: item.find('.list-title').text(), + link: item.attr('href'), + description: item.find('.list-desc').text(), + pubDate: parseDate(item.find('.list-time').attr('data-time')), + image: item.find('.img-box img').attr('src'), + }; + }), +}; + +// 详情页解析逻辑 +const detailParsers = { + ow: ($) => $('.deatil-content').first().html(), + hs: ($) => $('.article').first().html(), + wow: ($) => $('.detail').first().html(), +}; + +function getList(category, $) { + return parsers[category] ? parsers[category]($) : []; +} + +async function fetchDetail(item, category) { + return await cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + + const parseDetail = detailParsers[category]; + item.description = parseDetail($); + return item; + }); +} + +async function handler(ctx) { + const category = ctx.req.param('category') || 'ow'; + if (!categoryNames[category]) { + throw new Error('Invalid category'); + } + + const rootUrl = `https://${category}.blizzard.cn/news`; + + const response = await ofetch(rootUrl); + const $ = load(response); + + const list = getList(category, $); + if (!list.length) { + throw new Error('No news found'); + } + + const items = await Promise.all(list.map((item) => fetchDetail(item, category))); + + return { + title: `${categoryNames[category]}新闻`, + link: rootUrl, + item: items, + }; +} diff --git a/lib/routes/blizzard/news.ts b/lib/routes/blizzard/news.ts index 96dac3200d3245..4d083bc4018991 100644 --- a/lib/routes/blizzard/news.ts +++ b/lib/routes/blizzard/news.ts @@ -21,87 +21,176 @@ export const route: Route = { handler, description: `Categories - | Category | Slug | - | ---------------------- | ------------------- | - | All News | | - | Diablo II: Resurrected | diablo2 | - | Diablo III | diablo3 | - | Diablo IV | diablo4 | - | Diablo: Immortal | diablo-immortal | - | Hearthstone | hearthstone | - | Heroes of the Storm | heroes-of-the-storm | - | Overwatch 2 | overwatch | - | StarCraft: Remastered | starcraft | - | StarCraft II | starcraft2 | - | World of Warcraft | world-of-warcraft | - | Warcraft III: Reforged | warcraft3 | - | Battle.net | battlenet | - | BlizzCon | blizzcon | - | Inside Blizzard | blizzard | +| Category | Slug | +| ---------------------- | ------------------- | +| All News | | +| Diablo II: Resurrected | diablo2 | +| Diablo III | diablo3 | +| Diablo IV | diablo4 | +| Diablo Immortal | diablo-immortal | +| Hearthstone | hearthstone | +| Heroes of the Storm | heroes-of-the-storm | +| Overwatch 2 | overwatch | +| StarCraft: Remastered | starcraft | +| StarCraft II | starcraft2 | +| World of Warcraft | world-of-warcraft | +| Warcraft 3: Reforged | warcraft3 | +| Warcraft Rumble | warcraft-rumble | +| Battle.net | battlenet | +| BlizzCon | blizzcon | +| Inside Blizzard | blizzard | Language codes - | Language | Code | - | ------------------ | ----- | - | Deutsch | de-de | - | English (US) | en-us | - | English (EU) | en-gb | - | Español (EU) | es-es | - | Español (Latino) | es-mx | - | Français | fr-fr | - | Italiano | it-it | - | Português (Brasil) | pt-br | - | Polski | pl-pl | - | Русский | ru-ru | - | 한국어 | ko-kr | - | ภาษาไทย | th-th | - | 日本語 | ja-jp | - | 繁體中文 | zh-tw |`, +| Language | Code | +| ------------------ | ----- | +| Deutsch | de-de | +| English (US) | en-us | +| English (EU) | en-gb | +| Español (EU) | es-es | +| Español (Latino) | es-mx | +| Français | fr-fr | +| Italiano | it-it | +| Português (Brasil) | pt-br | +| Polski | pl-pl | +| Русский | ru-ru | +| 한국어 | ko-kr | +| ภาษาไทย | th-th | +| 日本語 | ja-jp | +| 繁體中文 | zh-tw |`, }; +const GAME_MAP = { + diablo2: { + key: 'diablo2', + value: 'diablo-2-resurrected', + id: 'blt54fbd3787a705054', + }, + diablo3: { + key: 'diablo3', + value: 'diablo-3', + id: 'blt2031aef34200656d', + }, + diablo4: { + key: 'diablo4', + value: 'diablo-4', + id: 'blt795c314400d7ded9', + }, + 'diablo-immortal': { + key: 'diablo-immortal', + value: 'diablo-immortal', + id: 'blt525c436e4a1b0a97', + }, + hearthstone: { + key: 'hearthstone', + value: 'hearthstone', + id: 'blt5cfc6affa3ca0638', + }, + 'heroes-of-the-storm': { + key: 'heroes-of-the-storm', + value: 'heroes-of-the-storm', + id: 'blt2e50e1521bb84dc6', + }, + overwatch: { + key: 'overwatch', + value: 'overwatch', + id: 'blt376fb94931906b6f', + }, + starcraft: { + key: 'starcraft', + value: 'starcraft', + id: 'blt81d46fcb05ab8811', + }, + starcraft2: { + key: 'starcraft2', + value: 'starcraft-2', + id: 'bltede2389c0a8885aa', + }, + 'world-of-warcraft': { + key: 'world-of-warcraft', + value: 'world-of-warcraft', + id: 'blt2caca37e42f19839', + }, + warcraft3: { + key: 'warcraft3', + value: 'warcraft-3', + id: 'blt24859ba8086fb294', + }, + 'warcraft-rumble': { + key: 'warcraft-rumble', + value: 'warcraft-rumble', + id: 'blte27d02816a8ff3e1', + }, + battlenet: { + key: 'battlenet', + value: 'battle-net', + id: 'blt90855744d00cd378', + }, + blizzcon: { + key: 'blizzcon', + value: 'blizzcon', + id: 'bltec70ad0ea4fd6d1d', + }, + blizzard: { + key: 'blizzard', + value: 'blizzard', + id: 'blt500c1f8b5470bfdb', + }, +}; + +function getSearchParams(category = 'all') { + return category === 'all' + ? Object.values(GAME_MAP) + .map((item) => `feedCxpProductIds[]=${item.id}`) + .join('&') + : `feedCxpProductIds[]=${GAME_MAP[category].id}`; +} + async function handler(ctx) { - const category = ctx.req.param('category') || ''; + const category = GAME_MAP[ctx.req.param('category')]?.key || 'all'; const language = ctx.req.param('language') || 'en-us'; + const rootUrl = `https://news.blizzard.com/${language}`; + const currentUrl = category === 'all' ? rootUrl : `${rootUrl}/?filter=${GAME_MAP[category].value}`; + const apiUrl = `${rootUrl}/api/news/blizzard`; + let rssTitle = ''; - const rootUrl = 'https://news.blizzard.com'; - const currentUrl = `${rootUrl}/${language}/${category}`; - const apiUrl = `${rootUrl}/${language}/blog/list`; - const response = await got(apiUrl, { - searchParams: { - community: category === '' ? 'all' : category, + const { + data: { + feed: { contentItems: response }, }, - }); - - const $ = load(response.data.html, null, false); + } = await got(`${apiUrl}?${getSearchParams(category)}`); - const list = $('.FeaturedArticle-text > a, .ArticleListItem > article > a') - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30) - .toArray() - .map((item) => { - item = $(item); - return { - title: item.text(), - link: `${rootUrl}${item.attr('href')}`, - }; - }); + const list = response.map((item) => { + const content = item.properties; + rssTitle = category === 'all' ? 'All News' : content.cxpProduct.title; // 这个是用来填充 RSS 订阅源频道级别 title,没别的地方能拿到了(而且会根据语言切换) + return { + title: content.title, + link: content.newsUrl, + author: content.author, + category: content.category, + guid: content.newsId, + description: content.summary, + pubDate: content.lastUpdated, + }; + }); const items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { - const detailResponse = await got(item.link); - const content = load(detailResponse.data); - - item.author = content('.ArticleDetail-bylineAuthor').text(); - item.description = content('.ArticleDetail-headingImageBlock').html() + content('.ArticleDetail-content').html(); - item.pubDate = content('.ArticleDetail-subHeadingLeft time').attr('timestamp'); - - return item; + try { + const { data: response } = await got(item.link); + const $ = load(response); + item.description = $('.Content').html(); + return item; + } catch { + return item; + } }) ) ); return { - title: $('title').text(), + title: rssTitle, link: currentUrl, item: items, }; diff --git a/lib/routes/blockworks/index.ts b/lib/routes/blockworks/index.ts new file mode 100644 index 00000000000000..e4afea423513b2 --- /dev/null +++ b/lib/routes/blockworks/index.ts @@ -0,0 +1,128 @@ +import { Route, Data, DataItem } 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'; +import { config } from '@/config'; + +export const route: Route = { + path: '/', + categories: ['finance'], + example: '/blockworks', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['blockworks.co/'], + target: '/', + }, + ], + name: 'News', + maintainers: ['pseudoyu'], + handler, + description: 'Blockworks news with full text support.', +}; + +async function handler(ctx): Promise { + const rssUrl = 'https://blockworks.co/feed'; + const feed = await parser.parseURL(rssUrl); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + // Limit to 20 items + const limitedItems = feed.items.slice(0, limit); + + const buildId = await getBuildId(); + + const items = await Promise.all( + limitedItems + .map((item) => ({ + ...item, + link: item.link?.split('?')[0], + })) + .map((item) => + cache.tryGet(item.link!, async () => { + // Get cached content or fetch new content + const content = await extractFullText(item.link!.split('/').pop()!, buildId); + + return { + title: item.title || 'Untitled', + pubDate: item.isoDate ? parseDate(item.isoDate) : undefined, + link: item.link, + description: content.description || item.content || item.contentSnippet || item.summary || '', + author: item.author, + category: content.category, + media: content.imageUrl + ? { + content: { url: content.imageUrl }, + } + : undefined, + } as DataItem; + }) + ) + ); + + return { + title: feed.title || 'Blockworks News', + link: feed.link || 'https://blockworks.co', + description: feed.description || 'Latest news from Blockworks', + item: items, + language: feed.language || 'en', + }; +} + +async function extractFullText(slug: string, buildId: string): Promise<{ description: string; imageUrl: string; category: string[] }> { + try { + const response = await ofetch(`https://blockworks.co/_next/data/${buildId}/news/${slug}.json?slug=${slug}`); + const article = response.pageProps.article; + const $ = load(article.content, null, false); + + // Remove promotional content at the end + $('hr').remove(); + $('p > em, p > strong').each((_, el) => { + const $el = $(el); + if ($el.text().includes('To read full editions') || $el.text().includes('Get the news in your inbox')) { + $el.parent().remove(); + } + }); + $('ul.wp-block-list > li > a').each((_, el) => { + const $el = $(el); + if ($el.attr('href') === 'https://blockworks.co/newsletter/daily') { + $el.parent().parent().remove(); + } + }); + + return { + description: $.html(), + imageUrl: article.imageUrl, + category: [...new Set([...article.categories, ...article.tags])], + }; + } catch (error) { + logger.error('Error extracting full text from Blockworks:', error); + return { description: '', imageUrl: '', category: [] }; + } +} + +const getBuildId = () => + cache.tryGet( + 'blockworks:buildId', + async () => { + const response = await ofetch('https://blockworks.co'); + const $ = load(response); + + return ( + $('script#__NEXT_DATA__') + .text() + ?.match(/"buildId":"(.*?)",/)?.[1] || '' + ); + }, + config.cache.routeExpire, + false + ) as Promise; diff --git a/lib/routes/blockworks/namespace.ts b/lib/routes/blockworks/namespace.ts new file mode 100644 index 00000000000000..0a48cd8d682972 --- /dev/null +++ b/lib/routes/blockworks/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Blockworks', + url: 'blockworks.co', + lang: 'en', +}; diff --git a/lib/routes/blogread/namespace.ts b/lib/routes/blogread/namespace.ts index 7f56723d3350ae..228e4b958885f1 100644 --- a/lib/routes/blogread/namespace.ts +++ b/lib/routes/blogread/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '技术头条', url: 'blogread.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/bloomberg/authors.ts b/lib/routes/bloomberg/authors.ts index 154f9e02e356a0..83b6eed8586aa7 100644 --- a/lib/routes/bloomberg/authors.ts +++ b/lib/routes/bloomberg/authors.ts @@ -1,18 +1,18 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import { load } from 'cheerio'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import rssParser from '@/utils/rss-parser'; import { asyncPoolAll, parseArticle } from './utils'; const parseAuthorNewsList = async (slug) => { const baseURL = `https://www.bloomberg.com/authors/${slug}`; const apiUrl = `https://www.bloomberg.com/lineup/api/lazy_load_author_stories?slug=${slug}&authorType=default&page=1`; - const resp = await got(apiUrl); + const resp = await ofetch(apiUrl); // Likely rate limited - if (!resp.data.html) { + if (!resp.html) { return []; } - const $ = load(resp.data.html); + const $ = load(resp.html); const articles = $('article.story-list-story'); return articles .map((index, item) => { @@ -30,7 +30,8 @@ const parseAuthorNewsList = async (slug) => { export const route: Route = { path: '/authors/:id/:slug/:source?', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/bloomberg/authors/ARbTQlRLRjE/matthew-s-levine', parameters: { id: 'Author ID, can be found in URL', slug: 'Author Slug, can be found in URL', source: 'Data source, either `api` or `rss`,`api` by default' }, features: { @@ -48,7 +49,7 @@ export const route: Route = { }, ], name: 'Authors', - maintainers: ['josh'], + maintainers: ['josh', 'pseudoyu'], handler, }; @@ -66,7 +67,7 @@ async function handler(ctx) { } const item = await asyncPoolAll(1, list, (item) => parseArticle(item)); - const authorName = item.find((i) => i.author)?.author ?? 'Unknown'; + const authorName = item.find((i) => i.author)?.author ?? slug; return { title: `Bloomberg - ${authorName}`, diff --git a/lib/routes/bloomberg/index.ts b/lib/routes/bloomberg/index.ts index 9a0b8d43a39c6c..46f6a7d8d7abb4 100644 --- a/lib/routes/bloomberg/index.ts +++ b/lib/routes/bloomberg/index.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import { rootUrl, asyncPoolAll, parseNewsList, parseArticle } from './utils'; const site_title_mapping = { '/': 'News', @@ -17,10 +17,14 @@ const site_title_mapping = { export const route: Route = { path: '/:site?', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/bloomberg/bbiz', parameters: { - site: 'Site ID, can be found below', + site: { + description: 'Site ID, can be found below', + options: Object.keys(site_title_mapping).map((key) => ({ value: key, label: site_title_mapping[key] })), + }, }, features: { requireConfig: false, @@ -33,21 +37,21 @@ export const route: Route = { name: 'Bloomberg Site', maintainers: ['bigfei'], description: ` - | Site ID | Title | - | ------------ | ------------ | - | / | News | - | bpol | Politics | - | bbiz | Business | - | markets | Markets | - | technology | Technology | - | green | Green | - | wealth | Wealth | - | pursuits | Pursuits | - | bview | Opinion | - | equality | Equality | - | businessweek | Businessweek | - | citylab | CityLab | - `, +| Site ID | Title | +| ------------ | ------------ | +| / | News | +| bpol | Politics | +| bbiz | Business | +| markets | Markets | +| technology | Technology | +| green | Green | +| wealth | Wealth | +| pursuits | Pursuits | +| bview | Opinion | +| equality | Equality | +| businessweek | Businessweek | +| citylab | CityLab | + `, handler, }; diff --git a/lib/routes/bloomberg/namespace.ts b/lib/routes/bloomberg/namespace.ts index ed652da2c0398d..b0c4f21f3206d8 100644 --- a/lib/routes/bloomberg/namespace.ts +++ b/lib/routes/bloomberg/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Bloomberg', url: 'www.bloomberg.com', + lang: 'en', }; diff --git a/lib/routes/bloomberg/utils.ts b/lib/routes/bloomberg/utils.ts index 6a0ef7cd47cc4a..92daa603c1c59c 100644 --- a/lib/routes/bloomberg/utils.ts +++ b/lib/routes/bloomberg/utils.ts @@ -85,9 +85,9 @@ const parseNewsList = async (url, ctx) => { .map((u) => { u = $(u); const item = { - title: u.find('news\\:title').text(), + title: u.find(String.raw`news\:title`).text(), link: u.find('loc').text(), - pubDate: parseDate(u.find('news\\:publication_date').text()), + pubDate: parseDate(u.find(String.raw`news\:publication_date`).text()), }; return item; }); diff --git a/lib/routes/bluearchive/namespace.ts b/lib/routes/bluearchive/namespace.ts new file mode 100644 index 00000000000000..8964009f943eda --- /dev/null +++ b/lib/routes/bluearchive/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Blue Archive', + url: 'bluearchive.jp', + categories: ['game'], + lang: 'ja', +}; diff --git a/lib/routes/bluearchive/news.ts b/lib/routes/bluearchive/news.ts new file mode 100644 index 00000000000000..4a64426f7e2d07 --- /dev/null +++ b/lib/routes/bluearchive/news.ts @@ -0,0 +1,88 @@ +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +// type id => display name +type Mapping = Record; + +const JP: Mapping = { + '0': '全て', + '1': 'イベント', + '2': 'お知らせ', + '3': 'メンテナンス', +}; + +// render into MD table +const mkTable = (mapping: Mapping): string => { + const heading: string[] = [], + separator: string[] = [], + body: string[] = []; + + for (const key in mapping) { + heading.push(mapping[key]); + separator.push(':--:'); + body.push(key); + } + + return [heading.join(' | '), separator.join(' | '), body.join(' | ')].map((s) => `| ${s} |`).join('\n'); +}; + +const handler: Route['handler'] = async (ctx) => { + const { server } = ctx.req.param(); + + switch (server.toUpperCase()) { + case 'JP': + return await ja(ctx); + default: + throw []; + } +}; + +const ja: Route['handler'] = async (ctx) => { + const { type = '0' } = ctx.req.param(); + + const data = await ofetch<{ data: { rows: { id: number; content: string; summary: string; publishTime: number }[] } }, 'json'>('https://api-web.bluearchive.jp/api/news/list', { + params: { + typeId: type, + pageNum: 16, + pageIndex: 1, + }, + }); + + return { + title: `ブルアカ - ${JP[type]}`, + link: 'https://bluearchive.jp/news/newsJump', + language: 'ja-JP', + image: 'https://webcnstatic.yostar.net/ba_cn_web/prod/web/favicon.png', // The CN website has a larger one + icon: 'https://webcnstatic.yostar.net/ba_cn_web/prod/web/favicon.png', + logo: 'https://webcnstatic.yostar.net/ba_cn_web/prod/web/favicon.png', + item: data.data.rows.map((row) => ({ + title: row.summary, + description: row.content, + link: `https://bluearchive.jp/news/newsJump/${row.id}`, + pubDate: parseDate(row.publishTime), + })), + }; +}; + +export const route: Route = { + path: '/news/:server/:type?', + name: 'News', + categories: ['game'], + maintainers: ['equt'], + example: '/bluearchive/news/jp', + parameters: { + server: 'game server (ISO 3166 two-letter country code, case-insensitive), only `JP` is supported for now', + type: 'news type, checkout the table below for details', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + handler, + description: [JP].map((el) => mkTable(el)).join('\n\n'), +}; diff --git a/lib/routes/bluestacks/namespace.ts b/lib/routes/bluestacks/namespace.ts index 230a8323aa08cd..b4f0b17c3cb6e8 100644 --- a/lib/routes/bluestacks/namespace.ts +++ b/lib/routes/bluestacks/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'BlueStacks', url: 'bluestacks.com', + lang: 'en', }; diff --git a/lib/routes/bmkg/namespace.ts b/lib/routes/bmkg/namespace.ts index e43f29d3a0fea2..f65228885c5bc5 100644 --- a/lib/routes/bmkg/namespace.ts +++ b/lib/routes/bmkg/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'BADAN METEOROLOGI, KLIMATOLOGI, DAN GEOFISIKA(Indonesian)', url: 'bmkg.go.id', + lang: 'en', }; diff --git a/lib/routes/bnu/bs.ts b/lib/routes/bnu/bs.ts index 1a21e02541c9e5..fa2bcdc9e8587d 100644 --- a/lib/routes/bnu/bs.ts +++ b/lib/routes/bnu/bs.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 学院新闻 | 通知公告 | 学术成果 | 学术讲座 | 教师观点 | 人才招聘 | - | -------- | -------- | -------- | -------- | -------- | -------- | - | xw | zytzyyg | xzcg | xzjz | xz | bshzs |`, +| -------- | -------- | -------- | -------- | -------- | -------- | +| xw | zytzyyg | xzcg | xzjz | xz | bshzs |`, }; async function handler(ctx) { diff --git a/lib/routes/bnu/fe.ts b/lib/routes/bnu/fe.ts new file mode 100644 index 00000000000000..9f405637c83b7e --- /dev/null +++ b/lib/routes/bnu/fe.ts @@ -0,0 +1,72 @@ +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import { Route } from '@/types'; + +export const route: Route = { + path: '/fe/:category', + categories: ['university'], + example: '/bnu/fe/18', + parameters: {}, + radar: [ + { + source: ['fe.bnu.edu.cn/pc/cms1info/list/1/:category'], + }, + ], + name: '教育学部-培养动态', + maintainers: ['etShaw-zh'], + handler, + description: `\`https://fe.bnu.edu.cn/pc/cms1info/list/1/18\` 则对应为 \`/bnu/fe/18`, +}; + +async function handler(ctx) { + const { category } = ctx.req.param(); + const apiUrl = 'https://fe.bnu.edu.cn/pc/cmscommon/nlist'; + let response; + try { + // 发送 POST 请求 + response = await got.post(apiUrl, { + headers: { + Accept: 'application/json, text/javascript, */*; q=0.01', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Origin: 'https://fe.bnu.edu.cn', + Referer: 'https://fe.bnu.edu.cn/pc/cms1info/list/1/18', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: `columnid=${category}&page=1`, // POST 数据 + }); + } catch { + throw new Error('Failed to fetch data from API'); + } + const jsonData = JSON.parse(response.body); + // 检查返回的 code + if (jsonData.code !== 0 || !jsonData.data) { + throw new Error('Invalid API response'); + } + + const list = jsonData.data.map((item) => ({ + title: item.title, + link: `https://fe.bnu.edu.cn/html/1/news/${item.htmlpath}/n${item.newsid}.html`, + pubDate: parseDate(item.happendate, 'YYYY-MM-DD'), + })); + + const out = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await got(item.link); + const $ = load(response.data); + item.author = '北京师范大学教育学部'; + item.description = $('.news02_div').html() || '暂无详细内容'; + return item; + }) + ) + ); + + return { + title: '北京师范大学教育学部-培养动态', + link: 'https://fe.bnu.edu.cn/pc/cms1info/list/1/18', + description: '北京师范大学教育学部-培养动态最新通知', + item: out, + }; +} diff --git a/lib/routes/bnu/jwb.ts b/lib/routes/bnu/jwb.ts index 5d190eeacf16a0..0f3fc9465ac038 100644 --- a/lib/routes/bnu/jwb.ts +++ b/lib/routes/bnu/jwb.ts @@ -2,6 +2,7 @@ import got from '@/utils/got'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; import cache from '@/utils/cache'; +import { Route } from '@/types'; export const route: Route = { path: '/jwb', diff --git a/lib/routes/bnu/mba.ts b/lib/routes/bnu/mba.ts index 4512b7ea11e5e7..e42435d0e300e1 100644 --- a/lib/routes/bnu/mba.ts +++ b/lib/routes/bnu/mba.ts @@ -84,39 +84,39 @@ export const route: Route = { handler, example: '/bnu/mba/xwdt', parameters: { category: '分类,默认为 xwdt,即新闻聚焦' }, - description: `:::tip + description: `::: tip 若订阅 [新闻聚焦](https://mba.bnu.edu.cn/xwdt/index.html),网址为 \`https://mba.bnu.edu.cn/xwdt/index.html\`。截取 \`https://mba.bnu.edu.cn/\` 到末尾 \`/index.html\` 的部分 \`xwdt\` 作为参数填入,此时路由为 [\`/bnu/mba/xwdt\`](https://rsshub.app/bnu/mba/xwdt)。 - ::: +::: - #### [主页](https://mba.bnu.edu.cn) +#### [主页](https://mba.bnu.edu.cn) - | [新闻聚焦](https://mba.bnu.edu.cn/xwdt/index.html) | [通知公告](https://mba.bnu.edu.cn/tzgg/index.html) | [MBA 系列讲座](https://mba.bnu.edu.cn/mbaxljz/index.html) | - | -------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------- | - | [xwdt](https://rsshub.app/bnu/mba/xwdt) | [tzgg](https://rsshub.app/bnu/mba/tzgg) | [mbaxljz](https://rsshub.app/bnu/mba/mbaxljz) | +| [新闻聚焦](https://mba.bnu.edu.cn/xwdt/index.html) | [通知公告](https://mba.bnu.edu.cn/tzgg/index.html) | [MBA 系列讲座](https://mba.bnu.edu.cn/mbaxljz/index.html) | +| -------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------- | +| [xwdt](https://rsshub.app/bnu/mba/xwdt) | [tzgg](https://rsshub.app/bnu/mba/tzgg) | [mbaxljz](https://rsshub.app/bnu/mba/mbaxljz) | - #### [招生动态](https://mba.bnu.edu.cn/zsdt/zsjz/index.html) +#### [招生动态](https://mba.bnu.edu.cn/zsdt/zsjz/index.html) - | [下载专区](https://mba.bnu.edu.cn/zsdt/cjwt/index.html) | - | ------------------------------------------------------- | - | [zsdt/cjwt](https://rsshub.app/bnu/mba/zsdt/cjwt) | +| [下载专区](https://mba.bnu.edu.cn/zsdt/cjwt/index.html) | +| ------------------------------------------------------- | +| [zsdt/cjwt](https://rsshub.app/bnu/mba/zsdt/cjwt) | - #### [国际视野](https://mba.bnu.edu.cn/gjhz/hwjd/index.html) +#### [国际视野](https://mba.bnu.edu.cn/gjhz/hwjd/index.html) - | [海外基地](https://mba.bnu.edu.cn/gjhz/hwjd/index.html) | [学位合作](https://mba.bnu.edu.cn/gjhz/xwhz/index.html) | [长期交换](https://mba.bnu.edu.cn/gjhz/zqjh/index.html) | [短期项目](https://mba.bnu.edu.cn/gjhz/dqxm/index.html) | - | ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | - | [gjhz/hwjd](https://rsshub.app/bnu/mba/gjhz/hwjd) | [gjhz/xwhz](https://rsshub.app/bnu/mba/gjhz/xwhz) | [gjhz/zqjh](https://rsshub.app/bnu/mba/gjhz/zqjh) | [gjhz/dqxm](https://rsshub.app/bnu/mba/gjhz/dqxm) | +| [海外基地](https://mba.bnu.edu.cn/gjhz/hwjd/index.html) | [学位合作](https://mba.bnu.edu.cn/gjhz/xwhz/index.html) | [长期交换](https://mba.bnu.edu.cn/gjhz/zqjh/index.html) | [短期项目](https://mba.bnu.edu.cn/gjhz/dqxm/index.html) | +| ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | +| [gjhz/hwjd](https://rsshub.app/bnu/mba/gjhz/hwjd) | [gjhz/xwhz](https://rsshub.app/bnu/mba/gjhz/xwhz) | [gjhz/zqjh](https://rsshub.app/bnu/mba/gjhz/zqjh) | [gjhz/dqxm](https://rsshub.app/bnu/mba/gjhz/dqxm) | - #### [校园生活](https://mba.bnu.edu.cn/xysh/xszz/index.html) +#### [校园生活](https://mba.bnu.edu.cn/xysh/xszz/index.html) - | [学生组织](https://mba.bnu.edu.cn/xysh/xszz/index.html) | - | ------------------------------------------------------- | - | [xysh/xszz](https://rsshub.app/bnu/mba/xysh/xszz) | +| [学生组织](https://mba.bnu.edu.cn/xysh/xszz/index.html) | +| ------------------------------------------------------- | +| [xysh/xszz](https://rsshub.app/bnu/mba/xysh/xszz) | - #### [职业发展](https://mba.bnu.edu.cn/zyfz/xwds/index.html) +#### [职业发展](https://mba.bnu.edu.cn/zyfz/xwds/index.html) - | [校外导师](https://mba.bnu.edu.cn/zyfz/xwds/index.html) | [企业实践](https://mba.bnu.edu.cn/zyfz/zycp/index.html) | [就业创业](https://mba.bnu.edu.cn/zyfz/jycy/index.html) | - | ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | - | [zyfz/xwds](https://rsshub.app/bnu/mba/zyfz/xwds) | [zyfz/zycp](https://rsshub.app/bnu/mba/zyfz/zycp) | [zyfz/jycy](https://rsshub.app/bnu/mba/zyfz/jycy) | +| [校外导师](https://mba.bnu.edu.cn/zyfz/xwds/index.html) | [企业实践](https://mba.bnu.edu.cn/zyfz/zycp/index.html) | [就业创业](https://mba.bnu.edu.cn/zyfz/jycy/index.html) | +| ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | +| [zyfz/xwds](https://rsshub.app/bnu/mba/zyfz/xwds) | [zyfz/zycp](https://rsshub.app/bnu/mba/zyfz/zycp) | [zyfz/jycy](https://rsshub.app/bnu/mba/zyfz/jycy) | `, categories: ['university'], diff --git a/lib/routes/bnu/namespace.ts b/lib/routes/bnu/namespace.ts index 7d0c7a69e10fdd..1a2f6fea8ba4aa 100644 --- a/lib/routes/bnu/namespace.ts +++ b/lib/routes/bnu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京师范大学', url: 'bs.bnu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/boc/namespace.ts b/lib/routes/boc/namespace.ts index 442459678922b6..cbcdaae031b05e 100644 --- a/lib/routes/boc/namespace.ts +++ b/lib/routes/boc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国银行', url: 'boc.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/boc/whpj.ts b/lib/routes/boc/whpj.ts index 8e79b1e542bc7b..e167d847311d14 100644 --- a/lib/routes/boc/whpj.ts +++ b/lib/routes/boc/whpj.ts @@ -26,8 +26,8 @@ export const route: Route = { handler, url: 'boc.cn/sourcedb/whpj', description: `| 短格式 | 中行折算价 | 现汇买卖 | 现钞买卖 | 现汇买入 | 现汇卖出 | 现钞买入 | 现钞卖出 | - | ------ | ---------- | -------- | -------- | -------- | -------- | -------- | -------- | - | short | zs | xh | xc | xhmr | xhmc | xcmr | xcmc |`, +| ------ | ---------- | -------- | -------- | -------- | -------- | -------- | -------- | +| short | zs | xh | xc | xhmr | xhmc | xcmr | xcmc |`, }; async function handler(ctx) { diff --git a/lib/routes/bookfere/category.ts b/lib/routes/bookfere/category.ts index 8ecd286fbb2486..db31ffbe7b0852 100644 --- a/lib/routes/bookfere/category.ts +++ b/lib/routes/bookfere/category.ts @@ -1,13 +1,25 @@ -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: '/:category', - categories: ['reading'], + categories: ['reading', 'popular'], + view: ViewType.Articles, example: '/bookfere/skills', - parameters: { category: '分类名' }, + parameters: { + category: { + description: '分类名', + options: [ + { value: 'weekly', label: '每周一书' }, + { value: 'skills', label: '使用技巧' }, + { value: 'books', label: '图书推荐' }, + { value: 'news', label: '新闻速递' }, + { value: 'essay', label: '精选短文' }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -20,8 +32,8 @@ export const route: Route = { maintainers: ['OdinZhang'], handler, description: `| 每周一书 | 使用技巧 | 图书推荐 | 新闻速递 | 精选短文 | - | -------- | -------- | -------- | -------- | -------- | - | weekly | skills | books | news | essay |`, +| -------- | -------- | -------- | -------- | -------- | +| weekly | skills | books | news | essay |`, }; async function handler(ctx) { diff --git a/lib/routes/bookfere/namespace.ts b/lib/routes/bookfere/namespace.ts index 4e8c59e3ae01c4..aa4ad8dcdc061a 100644 --- a/lib/routes/bookfere/namespace.ts +++ b/lib/routes/bookfere/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '书伴', url: 'bookfere.com', + lang: 'zh-CN', }; diff --git a/lib/routes/booru/namespace.ts b/lib/routes/booru/namespace.ts index 8b019132c6f25a..e4d8c2dc515864 100644 --- a/lib/routes/booru/namespace.ts +++ b/lib/routes/booru/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Booru', url: 'mmda.booru.org', + lang: 'en', }; diff --git a/lib/routes/bossdesign/index.ts b/lib/routes/bossdesign/index.ts index 6b82474986c247..84c3fc16f125e4 100644 --- a/lib/routes/bossdesign/index.ts +++ b/lib/routes/bossdesign/index.ts @@ -20,8 +20,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| Boss 笔记 | 电脑日志 | 素材资源 | 设计师神器 | 设计教程 | 设计资讯 | - | --------- | --------------- | ---------------- | --------------- | --------------- | ------------------- | - | note | computer-skills | design-resources | design-software | design-tutorial | design\_information |`, +| --------- | --------------- | ---------------- | --------------- | --------------- | ------------------- | +| note | computer-skills | design-resources | design-software | design-tutorial | design\_information |`, }; async function handler(ctx) { diff --git a/lib/routes/bossdesign/namespace.ts b/lib/routes/bossdesign/namespace.ts index 955feda26704f9..61cd691dcfb874 100644 --- a/lib/routes/bossdesign/namespace.ts +++ b/lib/routes/bossdesign/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Boss 设计', url: 'bossdesign.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/brave/namespace.ts b/lib/routes/brave/namespace.ts index cd8ef3a519877b..b38a081e38d1a3 100644 --- a/lib/routes/brave/namespace.ts +++ b/lib/routes/brave/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Brave', url: 'brave.com', + lang: 'en', }; diff --git a/lib/routes/brooklynmuseum/namespace.ts b/lib/routes/brooklynmuseum/namespace.ts index 08320443d00454..34e7807709c4dc 100644 --- a/lib/routes/brooklynmuseum/namespace.ts +++ b/lib/routes/brooklynmuseum/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Brooklyn Museum 纽约布鲁克林博物馆', + name: 'Brooklyn Museum', url: 'www.brooklynmuseum.org', + lang: 'en', }; diff --git a/lib/routes/bse/index.ts b/lib/routes/bse/index.ts index ea7befb77b3806..58c4fffcbcbf4b 100644 --- a/lib/routes/bse/index.ts +++ b/lib/routes/bse/index.ts @@ -143,24 +143,24 @@ export const route: Route = { handler, url: 'bse.cn/', description: `| 本所要闻 | 人才招聘 | 采购信息 | 业务通知 | - | --------------- | -------- | -------- | ---------- | - | important\_news | recruit | purchase | news\_list | +| --------------- | -------- | -------- | ---------- | +| important\_news | recruit | purchase | news\_list | - | 法律法规 | 公开征求意见 | 部门规章 | 发行融资 | - | --------- | --------------- | ---------------- | ---------- | - | law\_list | public\_opinion | regulation\_list | fxrz\_list | +| 法律法规 | 公开征求意见 | 部门规章 | 发行融资 | +| --------- | --------------- | ---------------- | ---------- | +| law\_list | public\_opinion | regulation\_list | fxrz\_list | - | 持续监管 | 交易管理 | 市场管理 | 上市委会议公告 | - | ---------- | ---------- | ---------- | --------------- | - | cxjg\_list | jygl\_list | scgl\_list | meeting\_notice | +| 持续监管 | 交易管理 | 市场管理 | 上市委会议公告 | +| ---------- | ---------- | ---------- | --------------- | +| cxjg\_list | jygl\_list | scgl\_list | meeting\_notice | - | 上市委会议结果公告 | 上市委会议变更公告 | 并购重组委会议公告 | - | ------------------ | ------------------ | ------------------ | - | meeting\_result | meeting\_change | bgcz\_notice | +| 上市委会议结果公告 | 上市委会议变更公告 | 并购重组委会议公告 | +| ------------------ | ------------------ | ------------------ | +| meeting\_result | meeting\_change | bgcz\_notice | - | 并购重组委会议结果公告 | 并购重组委会议变更公告 | 终止审核 | 注册结果 | - | ---------------------- | ---------------------- | ------------------ | ------------- | - | bgcz\_result | bgcz\_change | termination\_audit | audit\_result |`, +| 并购重组委会议结果公告 | 并购重组委会议变更公告 | 终止审核 | 注册结果 | +| ---------------------- | ---------------------- | ------------------ | ------------- | +| bgcz\_result | bgcz\_change | termination\_audit | audit\_result |`, }; async function handler(ctx) { diff --git a/lib/routes/bse/namespace.ts b/lib/routes/bse/namespace.ts index 234ff3efb8fb10..042bb07cff0d17 100644 --- a/lib/routes/bse/namespace.ts +++ b/lib/routes/bse/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京证券交易所', url: 'bse.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/bsky/feeds.ts b/lib/routes/bsky/feeds.ts new file mode 100644 index 00000000000000..268738f77d84a1 --- /dev/null +++ b/lib/routes/bsky/feeds.ts @@ -0,0 +1,72 @@ +import { Route, ViewType } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import { resolveHandle, getFeed, getFeedGenerator } from './utils'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +export const route: Route = { + path: '/profile/:handle/feed/:space/:routeParams?', + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, + example: '/bsky.app/profile/jaz.bsky.social/feed/cv:cat', + parameters: { + handle: 'User handle, can be found in URL', + space: 'Space ID, can be found in URL', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Feeds', + maintainers: ['FerrisChi'], + handler, +}; + +async function handler(ctx) { + const handle = ctx.req.param('handle'); + const space = ctx.req.param('space'); + + const DID = await resolveHandle(handle, cache.tryGet); + const uri = `at://${DID}/app.bsky.feed.generator/${space}`; + const profile = await getFeedGenerator(uri, cache.tryGet); + const feeds = await getFeed(uri, cache.tryGet); + + const items = feeds.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 + }), + 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 }} {{ i.alt }}
    {{ /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 = {}) { + const { body } = await got.post(url, { form }); + const $ = load(body); + const title = $('#main > div.dqwz > a').last().text() || '北京航空航天大学教务部'; + const list = $('#main div.news_list > ul > li') + .toArray() + .map((item) => { + const $ = load(item); + const link = getArticleUrl($('a').attr('onclick')); + if (link === null) { + return null; + } + return { + title: $('a').text(), + link, + pubDate: timezone(parseDate($('span.Floatright').text()), +8), + }; + }) + .filter((item) => item !== null); + + return { + title, + list, + }; +} + +function getItems(list) { + return Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const { data: descrptionResponse } = await got(item.link); + const $descrption = load(descrptionResponse); + const desc = $descrption('#main > div.content > div.search_height > div.search_con:has(p)').html(); + item.description = desc?.replace(/(\r|\n)+/g, '
    '); + 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 { + return (await cache.tryGet(item.isbn, async () => { + const info = await getItemInfo(item.isbn); + const holdings = JSON.parse(item.holdings) as Holding[]; + const link = `https://space.lib.buaa.edu.cn/space/searchDetailLocal/${item.bibId}`; + const content = art(path.join(__dirname, 'templates/newbook.art'), { + item, + info, + holdings, + }); + return { + language: item.language === 'eng' ? 'en' : 'zh-CN', + title: item.title, + pubDate: item.onSelfDate ? timezone(parseDate(item.onSelfDate), +8) : undefined, + description: content, + link, + }; + })) as DataItem; +} + +async function getItemInfo(isbn: string): Promise { + const url = `https://space.lib.buaa.edu.cn/meta-local/opac/third_api/douban/${isbn}/info`; + const response = await got(url); + return JSON.parse(response.body).data; +} diff --git a/lib/routes/buaa/lib/space/templates/newbook.art b/lib/routes/buaa/lib/space/templates/newbook.art new file mode 100644 index 00000000000000..6068de6df656eb --- /dev/null +++ b/lib/routes/buaa/lib/space/templates/newbook.art @@ -0,0 +1,44 @@ +{{if info.imageUrl}} +

    +{{/if}} +

    书籍信息

    +
    + {{item.callno.at(0) || '无'}} / + {{item.author}} / + {{item.publisher}} / + {{item.pub_year}} +
    +

    简介

    +
    {{info?.content}}
    + + + + +
    ISBN{{item.isbn}}
    语言{{item.language}}
    类型{{item.docTypeDesc}}
    +{{if info.authorInfo}} +

    作者简介

    +
    {{info.authorInfo}}
    +{{/if}} +

    馆藏信息

    +{{if item.onSelfDate}} +上架时间: +{{item.onSelfDate}} +{{/if}} +
    +

    馆藏地点

    + + {{each holdings holding}} + + + + + + + + + {{/each}} +
    所属馆藏地{{holding.location}}
    索书号{{holding.callNo}}
    条码号{{holding.barCode}}
    编号{{holding.itemId}}
    书刊状态{{holding.status}}
    +{{if info.catalog}} +

    目录

    +
    {{@ info.catalog}}
    +{{/if}} \ No newline at end of file diff --git a/lib/routes/buaa/namespace.ts b/lib/routes/buaa/namespace.ts index 58c5ca080657d0..9ed95faf65b4d0 100644 --- a/lib/routes/buaa/namespace.ts +++ b/lib/routes/buaa/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京航空航天大学', url: 'news.buaa.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/buaa/news/index.ts b/lib/routes/buaa/news/index.ts index c21be7a33717c3..3389d086aa14fc 100644 --- a/lib/routes/buaa/news/index.ts +++ b/lib/routes/buaa/news/index.ts @@ -1,4 +1,5 @@ -import { Route } from '@/types'; +import { Route, Data, DataItem } from '@/types'; +import { Context } from 'hono'; import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; @@ -21,12 +22,12 @@ export const route: Route = { name: '新闻网', maintainers: ['AlanDecode'], handler, - description: `| 综合新闻 | 信息公告 | 学术文化 | 校园风采 | 科教在线 | 媒体北航 | 专题新闻 | 北航人物 | - | -------- | --------- | ------------ | --------- | --------- | --------- | -------- | -------- | - | zhxw | xxgg\_new | xsjwhhd\_new | xyfc\_new | kjzx\_new | mtbh\_new | ztxw | bhrw |`, + description: `| 综合新闻 | 信息公告 | 学术文化 | 校园风采 | 科教在线 | 媒体北航 | 专题新闻 | 北航人物 | +| -------- | -------- | ----------- | -------- | -------- | -------- | -------- | -------- | +| zhxw | xxgg_new | xsjwhhd_new | xyfc_new | kjzx_new | mtbh_new | ztxw | bhrw |`, }; -async function handler(ctx) { +async function handler(ctx: Context): Promise { const baseUrl = 'https://news.buaa.edu.cn'; const type = ctx.req.param('type'); @@ -34,36 +35,37 @@ async function handler(ctx) { const $ = load(response); const title = $('.subnav span').text().trim(); - const list = $('.mainleft > .listlefttop > .listleftop1') + const list: DataItem[] = $('.mainleft > .listlefttop > .listleftop1') .toArray() - .map((item) => { - item = $(item); + .map((item_) => { + const item = $(item_); const title = item.find('h2 > a'); return { title: title.text(), - link: new URL(title.attr('href'), baseUrl).href, + link: new URL(title.attr('href')!, baseUrl).href, pubDate: timezone(parseDate(item.find('h2 em').text(), '[YYYY-MM-DD]'), +8), }; }); - const result = await Promise.all( + const result = (await Promise.all( list.map((item) => - cache.tryGet(item.link, async () => { + cache.tryGet(item.link!, async () => { const response = await got(item.link); const $ = load(response.data); - item.description = $('.v_news_content').html(); + item.description = $('.v_news_content').html() || ''; item.author = $('.vsbcontent_end').text().trim(); return item; }) ) - ); + )) as DataItem[]; return { title: `北航新闻 - ${title}`, link, description: `北京航空航天大学新闻网 - ${title}`, + language: 'zh-CN', item: result, }; } diff --git a/lib/routes/buaa/sme.ts b/lib/routes/buaa/sme.ts index eadfe3ab0ac03f..9511895ac1afd4 100755 --- a/lib/routes/buaa/sme.ts +++ b/lib/routes/buaa/sme.ts @@ -17,7 +17,7 @@ export const route: Route = { parameters: { path: '版块路径,默认为 `tzgg`(通知公告)', }, - description: `:::tip + description: `::: tip 版块路径(\`path\`)应填写板块 URL 中 \`http://www.sme.buaa.edu.cn/\` 和 \`.htm\` 之间的字段。 @@ -28,7 +28,7 @@ export const route: Route = { ::: -:::warning +::: warning 部分页面(如[学院介绍](http://www.sme.buaa.edu.cn/xygk/xyjs.htm)、[微纳中心](http://www.sme.buaa.edu.cn/wnzx.htm)、[院学生会](http://www.sme.buaa.edu.cn/xsgz/yxsh.htm))存在无内容、内容跳转至外站等情况,因此可能出现解析失败的现象。 @@ -56,6 +56,8 @@ async function handler(ctx) { link: url, // 源文章 item: await getItems(list), + // 语言 + language: 'zh-CN', }; } @@ -69,13 +71,13 @@ async function getList(url) { .join(' - '); const list = $("div[class='Newslist'] > ul > li") .toArray() - .map((item) => { - item = $(item); + .map((item_) => { + const item = $(item_); const $a = item.find('a'); const link = $a.attr('href'); return { title: item.find('a').text(), - link: link.startsWith('http') ? link : `${BASE_URL}/${link}`, // 有些链接是相对路径 + link: link?.startsWith('http') ? link : `${BASE_URL}/${link}`, // 有些链接是相对路径 pubDate: timezone(parseDate(item.find('span').text()), +8), }; }); diff --git a/lib/routes/buct/cist.ts b/lib/routes/buct/cist.ts new file mode 100644 index 00000000000000..c482373d424568 --- /dev/null +++ b/lib/routes/buct/cist.ts @@ -0,0 +1,58 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/cist', + categories: ['university'], + example: '/buct/cist', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [{ source: ['cist.buct.edu.cn/xygg/list.htm', 'cist.buct.edu.cn/xygg/main.htm'], target: '/cist' }], + name: '信息学院', + maintainers: ['Epic-Creeper'], + handler, + url: 'buct.edu.cn/', +}; + +async function handler() { + const rootUrl = 'https://cist.buct.edu.cn'; + const currentUrl = `${rootUrl}/xygg/list.htm`; + + const response = await got.get(currentUrl); + const $ = load(response.data); + const list = $('ul.wp_article_list > li.list_item') + .toArray() + .map((item) => ({ + pubDate: $(item).find('.Article_PublishDate').text(), + title: $(item).find('a').attr('title'), + link: `${rootUrl}${$(item).find('a').attr('href')}`, + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got.get(item.link); + const content = load(detailResponse.data); + + item.description = content('.wp_articlecontent').html(); + + return item; + }) + ) + ); + + return { + title: $('title').text(), + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/buct/gr.ts b/lib/routes/buct/gr.ts new file mode 100644 index 00000000000000..f9f0f36fdd1166 --- /dev/null +++ b/lib/routes/buct/gr.ts @@ -0,0 +1,93 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import type { Context } from 'hono'; + +export const route: Route = { + path: '/gr/:type', + categories: ['university'], + example: '/buct/gr/jzml', + parameters: { + type: { + description: '信息类型,可选值:tzgg(通知公告),jzml(简章目录),xgzc(相关政策)', + options: [ + { value: 'tzgg', label: '通知公告' }, + { value: 'jzml', label: '简章目录' }, + { value: 'xgzc', label: '相关政策' }, + ], + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { source: ['graduate.buct.edu.cn/1392/list.htm'], target: '/gr/tzgg' }, + { source: ['graduate.buct.edu.cn/jzml/list.htm'], target: '/gr/jzml' }, + { source: ['graduate.buct.edu.cn/1393/list.htm'], target: '/gr/xgzc' }, + ], + name: '研究生院', + maintainers: ['Epic-Creeper'], + handler, + url: 'buct.edu.cn/', +}; + +async function handler(ctx: Context) { + const type = ctx.req.param('type'); + const rootUrl = 'https://graduate.buct.edu.cn'; + let currentUrl; + + switch (type) { + case 'tzgg': + currentUrl = `${rootUrl}/1392/list.htm`; + + break; + + case 'jzml': + currentUrl = `${rootUrl}/jzml/list.htm`; + + break; + + case 'xgzc': + currentUrl = `${rootUrl}/1393/list.htm`; + + break; + + default: + throw new Error('Invalid type parameter'); + } + + const response = await got.get(currentUrl); + + const $ = load(response.data); + const list = $('ul.wp_article_list > li.list_item') + .toArray() + .map((item) => ({ + pubDate: $(item).find('.Article_PublishDate').text(), + title: $(item).find('a').attr('title'), + link: `${rootUrl}${$(item).find('a').attr('href')}`, + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got.get(item.link); + const content = load(detailResponse.data); + item.description = content('.wp_articlecontent').html(); + + return item; + }) + ) + ); + + return { + title: $('title').text(), + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/buct/jwc.ts b/lib/routes/buct/jwc.ts new file mode 100644 index 00000000000000..3c084861a90f03 --- /dev/null +++ b/lib/routes/buct/jwc.ts @@ -0,0 +1,64 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/jwc', + categories: ['university'], + example: '/buct/jwc', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [{ source: ['jiaowuchu.buct.edu.cn/610/list.htm', 'jiaowuchu.buct.edu.cn/611/main.htm'], target: '/jwc' }], + name: '教务处', + maintainers: ['Epic-Creeper'], + handler, + url: 'buct.edu.cn/', +}; + +async function handler() { + const rootUrl = 'https://jiaowuchu.buct.edu.cn'; + const currentUrl = `${rootUrl}/610/list.htm`; + + const response = await got.get(currentUrl); + + const $ = load(response.data); + const list = $('div.list02 ul > li') + .not('#wp_paging_w66 li') + .toArray() + .map((item) => ({ + pubDate: $(item).find('span').text(), + title: $(item).find('a').attr('title'), + link: `${rootUrl}${$(item).find('a').attr('href')}`, + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got.get(item.link); + const content = load(detailResponse.data); + const iframeSrc = content('.wp_pdf_player').attr('pdfsrc'); + if (iframeSrc) { + const pdfUrl = `${rootUrl}${iframeSrc}`; + item.description = `此页面为PDF文档:点击查看pdf`; + return item; + } + item.description = content('.rt_zhengwen').html(); + return item; + }) + ) + ); + + return { + title: $('title').text(), + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/buct/namespace.ts b/lib/routes/buct/namespace.ts new file mode 100644 index 00000000000000..40e9971dc45338 --- /dev/null +++ b/lib/routes/buct/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '北京化工大学', + url: 'buct.edu.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/bugzilla/bug.ts b/lib/routes/bugzilla/bug.ts new file mode 100644 index 00000000000000..afd82f3a3ac11a --- /dev/null +++ b/lib/routes/bugzilla/bug.ts @@ -0,0 +1,59 @@ +import { load } from 'cheerio'; +import { Context } from 'hono'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import { Data, DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +const INSTANCES = new Map([ + ['apache', 'bz.apache.org/bugzilla'], + ['apache.ooo', 'bz.apache.org/ooo'], // Apache OpenOffice + ['apache.SpamAssassin', 'bz.apache.org/SpamAssassin'], + ['kernel', 'bugzilla.kernel.org'], + ['mozilla', 'bugzilla.mozilla.org'], + ['webkit', 'bugs.webkit.org'], +]); + +async function handler(ctx: Context): Promise { + const { site, bugId } = ctx.req.param(); + if (!INSTANCES.has(site)) { + throw new InvalidParameterError(`unknown site: ${site}`); + } + const link = `https://${INSTANCES.get(site)}/show_bug.cgi?id=${bugId}`; + const $ = load(await ofetch(`${link}&ctype=xml`)); + const items = $('long_desc').map((index, rawItem) => { + const $ = load(rawItem, null, false); + return { + title: `comment #${$('commentid').text()}`, + link: `${link}#c${index}`, + description: $('thetext').text(), + pubDate: parseDate($('bug_when').text()), + author: $('who').attr('name'), + } as DataItem; + }); + return { title: $('short_desc').text(), link, item: items.toArray() }; +} + +function markdownFrom(instances: Map, separator: string = ', '): string { + return [...instances.entries()].map(([k, v]) => `[\`${k}\`](https://${v})`).join(separator); +} + +export const route: Route = { + path: '/bug/:site/:bugId', + name: 'bugs', + maintainers: ['FranklinYu'], + handler, + example: '/bugzilla/bug/webkit/251528', + parameters: { + site: 'site identifier', + bugId: 'numeric identifier of the bug in the site', + }, + description: `Supported site identifiers: ${markdownFrom(INSTANCES)}.`, + categories: ['programming'], + + // Radar is infeasible, because it needs access to URL parameters. + zh: { + name: 'bugs', + description: `支持的站点标识符:${markdownFrom(INSTANCES, '、')}。`, + }, +}; diff --git a/lib/routes/bugzilla/namespace.ts b/lib/routes/bugzilla/namespace.ts new file mode 100644 index 00000000000000..012d6382f7e9be --- /dev/null +++ b/lib/routes/bugzilla/namespace.ts @@ -0,0 +1,12 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Bugzilla', + url: 'bugzilla.org', + description: 'Bugzilla instances hosted by organizations.', + zh: { + name: 'Bugzilla', + description: '各组织自建的Bugzilla实例。', + }, + lang: 'en', +}; diff --git a/lib/routes/bulianglin/namespace.ts b/lib/routes/bulianglin/namespace.ts index 9efb89ca8970c1..f269e43ca22a99 100644 --- a/lib/routes/bulianglin/namespace.ts +++ b/lib/routes/bulianglin/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '不良林', url: 'bulianglin.com', + lang: 'zh-CN', }; diff --git a/lib/routes/bullionvault/gold-news.ts b/lib/routes/bullionvault/gold-news.ts new file mode 100644 index 00000000000000..c71686a4bbcdf6 --- /dev/null +++ b/lib/routes/bullionvault/gold-news.ts @@ -0,0 +1,231 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +import cache from '@/utils/cache'; +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'; + +export const handler = async (ctx: Context): Promise => { + const { category } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'https://bullionvault.com'; + const targetUrl: string = new URL(`gold-news${category ? `/${category}` : ''}`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'en'; + + let items: DataItem[] = []; + + items = $('div.gold-news-content div.view-content table.views-table tbody tr') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio = $(el); + const $aEl: Cheerio = $el.find('td.views-field-title a').first(); + + const title: string = $aEl.text(); + const pubDateStr: string | undefined = $el.find('td.views-field-created').text().trim(); + const linkUrl: string | undefined = $aEl.attr('href'); + const authorEls: Element[] = $el.find('a.username').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $authorEl: Cheerio = $(authorEl); + + return { + name: $authorEl.text(), + url: $authorEl.attr('href'), + avatar: undefined, + }; + }); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl, + author: authors, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('header h1').text(); + const description: string | undefined = $$('div[property="content:encoded"]').html() ?? ''; + const pubDateStr: string | undefined = $$('div.submitted').text().split(/,/).pop(); + const categories: string[] = $$('meta[name="news_keywords"]').attr('content')?.split(/,/) ?? []; + const authorEls: Element[] = $$('div.view-author-bio').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $$authorEl: Cheerio = $$(authorEl); + + return { + name: $$authorEl.find('h1').text(), + url: undefined, + avatar: $$authorEl.find('img').attr('src'), + }; + }); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[property="og:description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('meta[property="og:image"]').attr('content'), + author: $('meta[property="og:title"]').attr('content')?.split(/\|/).pop(), + language, + id: $('meta[property="og:url"]').attr('content'), + }; +}; + +export const route: Route = { + path: '/gold-news/:category?', + name: 'Gold News', + url: 'bullionvault.com', + maintainers: ['nczitzk'], + handler, + example: '/bullionvault/gold-news', + parameters: { + category: { + description: 'Category', + options: [ + { + label: 'Gold market analysis & gold investment research', + value: '', + }, + { + label: 'Opinion & Analysis', + value: 'opinion-analysis', + }, + { + label: 'Gold Price News', + value: 'gold-price-news', + }, + { + label: 'Investment News', + value: 'news', + }, + { + label: 'Gold Investor Index', + value: 'gold-investor-index', + }, + { + label: 'Gold Infographics', + value: 'infographics', + }, + { + label: 'Market Fundamentals', + value: 'market-fundamentals', + }, + ], + }, + }, + description: `:::tip +If you subscribe to [Gold Price News](https://www.bullionvault.com/gold-news/gold-price-news),where the URL is \`https://www.bullionvault.com/gold-news/gold-price-news\`, extract the part \`https://www.bullionvault.com/gold-news/\` to the end, and use it as the parameter to fill in. Therefore, the route will be [\`/bullionvault/gold-news/gold-price-news\`](https://rsshub.app/bullionvault/gold-news/gold-price-news). +::: + +| Category | ID | +| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| [Opinion & Analysis](https://www.bullionvault.com/gold-news/opinion-analysis) | [opinion-analysis](https://rsshub.app/bullionvault/gold-news/opinion-analysis) | +| [Gold Price News](https://www.bullionvault.com/gold-news/gold-price-news) | [gold-price-news](https://rsshub.app/bullionvault/gold-news/gold-price-news) | +| [Investment News](https://www.bullionvault.com/gold-news/news) | [news](https://rsshub.app/bullionvault/gold-news/news) | +| [Gold Investor Index](https://www.bullionvault.com/gold-news/gold-investor-index) | [gold-investor-index](https://rsshub.app/bullionvault/gold-news/gold-investor-index) | +| [Gold Infographics](https://www.bullionvault.com/gold-news/infographics) | [infographics](https://rsshub.app/bullionvault/gold-news/infographics) | +| [Market Fundamentals](https://www.bullionvault.com/gold-news/market-fundamentals) | [market-fundamentals](https://rsshub.app/bullionvault/gold-news/market-fundamentals) | +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['bullionvault.com/gold-news/:category'], + target: (params) => { + const category: string = params.category; + + return `/bullionvault/gold-news${category ? `/${category}` : ''}`; + }, + }, + { + title: 'Gold market analysis & gold investment research', + source: ['bullionvault.com/gold-news'], + target: '/gold-news', + }, + { + title: 'Opinion & Analysis', + source: ['bullionvault.com/gold-news/opinion-analysis'], + target: '/gold-news/opinion-analysis', + }, + { + title: 'Gold Price News', + source: ['bullionvault.com/gold-news/gold-price-news'], + target: '/gold-news/gold-price-news', + }, + { + title: 'Investment News', + source: ['bullionvault.com/gold-news/news'], + target: '/gold-news/news', + }, + { + title: 'Gold Investor Index', + source: ['bullionvault.com/gold-news/gold-investor-index'], + target: '/gold-news/gold-investor-index', + }, + { + title: 'Gold Infographics', + source: ['bullionvault.com/gold-news/infographics'], + target: '/gold-news/infographics', + }, + { + title: 'Market Fundamentals', + source: ['bullionvault.com/gold-news/market-fundamentals'], + target: '/gold-news/market-fundamentals', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/bullionvault/namespace.ts b/lib/routes/bullionvault/namespace.ts new file mode 100644 index 00000000000000..2dd9d86b52b033 --- /dev/null +++ b/lib/routes/bullionvault/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'BullionVault', + url: 'bullionvault.com', + categories: ['finance'], + description: '', + lang: 'en', +}; diff --git a/lib/routes/bupt/jwc.ts b/lib/routes/bupt/jwc.ts new file mode 100644 index 00000000000000..b05d15aa87088b --- /dev/null +++ b/lib/routes/bupt/jwc.ts @@ -0,0 +1,130 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; +import type { Context } from 'hono'; + +export const route: Route = { + path: '/jwc/:type', + categories: ['university'], + example: '/bupt/jwc/tzgg', + parameters: { + type: { + type: 'string', + optional: false, + description: '信息类型,可选值:tzgg(通知公告),xwzx(新闻资讯)', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['jwc.bupt.edu.cn/tzgg1.htm'], + target: '/jwc/tzgg', + }, + { + source: ['jwc.bupt.edu.cn/xwzx2.htm'], + target: '/jwc/xwzx', + }, + ], + name: '教务处', + maintainers: ['Yoruet'], + handler, + url: 'jwc.bupt.edu.cn', +}; + +async function handler(ctx: Context) { + let type = ctx.req.param('type'); // 默认类型为通知公告 + if (!type) { + type = 'tzgg'; + } + const rootUrl = 'https://jwc.bupt.edu.cn'; + let currentUrl; + let pageTitle; + + if (type === 'tzgg') { + currentUrl = `${rootUrl}/tzgg1.htm`; + pageTitle = '通知公告'; + } else if (type === 'xwzx') { + currentUrl = `${rootUrl}/xwzx2.htm`; + pageTitle = '新闻资讯'; + } else { + throw new Error('Invalid type parameter'); + } + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + + const list = $('.txt-elise') + .map((_, item) => { + const $item = $(item); + const $link = $item.find('a'); + // Skip elements without links or with empty href + if ($link.length === 0 || !$link.attr('href')) { + return null; + } + return { + title: $link.text().trim(), + link: rootUrl + '/' + $link.attr('href'), + }; + }) + .get() + .filter(Boolean); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got({ + method: 'get', + url: item.link, + }); + + const content = load(detailResponse.data); + + // 选择包含新闻内容的元素 + const newsContent = content('.v_news_content'); + + // 移除不必要的标签,比如

    中无用的内容 + 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) => `${e.desc}

    ${e.desc}
    `).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(api); + + const items = timelineResponse.data.map((item) => { + const description = art(path.join(__dirname, 'templates/post.art'), { + content: item.content, + images: item.images.filter((i) => !i.isCoverImg).map((i) => ({ ...i, src: `${CDN_HOST}/${i.src}` })), + }); + return { + title: item.title || item.content, + pubDate: parseDate(item.createdAt), + link: `${HOST}/post/${item.id}`, + description, + } as DataItem; + }); + + return { + title: `Likes - ${userInfo.name}`, + link: `${HOST}/${user}/likes`, + image: `${CDN_HOST}/${userInfo.photo}`, + item: items, + }; +} diff --git a/lib/routes/cara/namespace.ts b/lib/routes/cara/namespace.ts new file mode 100644 index 00000000000000..aaeac1780bee95 --- /dev/null +++ b/lib/routes/cara/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Cara', + url: 'cara.app', + lang: 'en', +}; diff --git a/lib/routes/cara/portfolio.ts b/lib/routes/cara/portfolio.ts new file mode 100644 index 00000000000000..2151d66328662c --- /dev/null +++ b/lib/routes/cara/portfolio.ts @@ -0,0 +1,40 @@ +import type { Data, DataItem, Route } from '@/types'; +import type { PortfolioResponse } from './types'; +import { customFetch, fetchPortfolioItem, parseUserData } from './utils'; +import { API_HOST, CDN_HOST, HOST } from './constant'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: ['/portfolio/:user'], + categories: ['social-media', 'popular'], + example: '/cara/portfolio/fengz', + parameters: { user: 'username' }, + name: 'Portfolio', + maintainers: ['KarasuShin'], + handler, + radar: [ + { + source: ['cara.app/:user', 'cara.app/:user/*'], + target: '/portfolio/: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}/profiles/portfolio?id=${userInfo.id}&take=${limit}`; + + const portfolioResponse = await customFetch(api); + + const items = await Promise.all(portfolioResponse.data.map((item) => cache.tryGet(`${HOST}/post/${item.postId}`, async () => await fetchPortfolioItem(item)) as unknown as DataItem)); + + return { + title: `Portfolio - ${userInfo.name}`, + link: `${HOST}/${user}/portfolio`, + image: `${CDN_HOST}/${userInfo.photo}`, + item: items, + }; +} diff --git a/lib/routes/cara/templates/post.art b/lib/routes/cara/templates/post.art new file mode 100644 index 00000000000000..2cceed7e5f4f9f --- /dev/null +++ b/lib/routes/cara/templates/post.art @@ -0,0 +1,6 @@ +{{ if content }} +

    {{ content }}

    +{{ /if }} +{{ each images image }} + +{{ /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(api); + + const items = timelineResponse.data.map((item) => { + const description = art(path.join(__dirname, 'templates/post.art'), { + content: item.content, + images: item.images.filter((i) => !i.isCoverImg).map((i) => ({ ...i, src: `${CDN_HOST}/${i.src}` })), + }); + return { + title: item.title || item.content, + pubDate: parseDate(item.createdAt), + link: `${HOST}/post/${item.id}`, + description, + } as DataItem; + }); + + return { + title: `Timeline - ${userInfo.name}`, + link: `${HOST}/${user}/all`, + image: `${CDN_HOST}/${userInfo.photo}`, + item: items, + }; +} diff --git a/lib/routes/cara/types.ts b/lib/routes/cara/types.ts new file mode 100644 index 00000000000000..7d3aea28a023c3 --- /dev/null +++ b/lib/routes/cara/types.ts @@ -0,0 +1,45 @@ +export interface UserNextData { + pageProps: { + user: { + id: string; + name: string; + slug: string; + photo: string; + }; + }; +} + +export interface PortfolioResponse { + data: { + url: string; + postId: string; + imageNum: number; + }[]; +} + +export interface PortfolioDetailResponse { + data: { + createdAt: string; + images: { + src: string; + isCoverImg: boolean; + }[]; + title: string; + content: string; + }; +} + +export interface PostsResponse { + data: { + name: string; + photo: string; + createdAt: string; + images: { + src: string; + isCoverImg: boolean; + }[]; + id: string; + title: string; + content: string; + }[]; +} diff --git a/lib/routes/cara/utils.ts b/lib/routes/cara/utils.ts new file mode 100644 index 00000000000000..a68bb5f87a4155 --- /dev/null +++ b/lib/routes/cara/utils.ts @@ -0,0 +1,62 @@ +import { config } from '@/config'; +import ofetch from '@/utils/ofetch'; +import type { FetchOptions, FetchRequest, ResponseType } from 'ofetch'; +import asyncPool from 'tiny-async-pool'; +import type { PortfolioDetailResponse, PortfolioResponse, UserNextData } from './types'; +import type { DataItem } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import { API_HOST, CDN_HOST, HOST } from './constant'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; + +export function customFetch(request: FetchRequest, options?: FetchOptions) { + return ofetch(request, { + ...options, + headers: { + 'user-agent': config.trueUA, + }, + }); +} + +export async function parseUserData(user: string) { + const buildId = await cache.tryGet( + `${HOST}:buildId`, + async () => { + const res = await customFetch(`${HOST}/explore`); + const $ = load(res); + return JSON.parse($('#__NEXT_DATA__')?.text() ?? '{}').buildId; + }, + config.cache.routeExpire, + false + ); + return (await cache.tryGet(`${HOST}:${user}`, async () => { + const data = await customFetch(`${HOST}/_next/data/${buildId}/${user}.json`); + return data.pageProps.user; + })) as Promise; +} + +export async function asyncPoolAll(poolLimit: number, array: readonly IN[], iteratorFn: (generator: IN) => Promise) { + const results: Awaited = []; + for await (const result of asyncPool(poolLimit, array, iteratorFn)) { + results.push(result); + } + return results; +} + +export async function fetchPortfolioItem(item: PortfolioResponse['data'][number]) { + const res = await customFetch(`${API_HOST}/posts/${item.postId}`); + + const description = res.data.images + .filter((i) => !i.isCoverImg) + .map((image) => ``) + .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 = { + ggl: { + title: '通知公告', + description: 'CATTI 考试通知和公告', + }, + ywdt: { + title: '要闻动态', + description: 'CATTI 考试要闻动态', + }, + zxzc: { + title: '最新政策', + description: 'CATTI 考试最新政策', + }, +}; + +const handler: Route['handler'] = async (ctx) => { + const category = ctx.req.param('category'); + + const BASE_URL = `https://www.catticenter.com/${category}`; + + // Fetch the index page + const { data: listPage } = await got(BASE_URL); + const $ = load(listPage); + + // Select all list items containing news information + const ITEM_SELECTOR = 'ul.ui-card.ui-card-a > li'; + const listItems = $(ITEM_SELECTOR); + + // Map through each list item to extract details + const contentLinkList = listItems.toArray().map((element) => { + const date = $(element).find('span.ui-right-time').text(); + const title = $(element).find('a').attr('title')!; + const relativeLink = $(element).find('a').attr('href')!; + const absoluteLink = `https://www.catticenter.com${relativeLink}`; + const formattedDate = parseDate(date); + return { + date: formattedDate, + title, + link: absoluteLink, + }; + }); + + return { + title: NEWS_TYPES[category].title, + description: NEWS_TYPES[category].description, + link: BASE_URL, + image: 'https://www.catticenter.com/img/applogo.png', + item: (await Promise.all( + contentLinkList.map((item) => + cache.tryGet(item.link, async () => { + const CONTENT_SELECTOR = 'div.ui-article-cont'; + const { data: contentResponse } = await got(item.link); + const contentPage = load(contentResponse); + const content = contentPage(CONTENT_SELECTOR).html() || ''; + return { + title: item.title, + pubDate: item.date, + link: item.link, + description: content, + category: ['study'], + guid: item.link, + id: item.link, + image: 'https://www.catticenter.com/img/applogo.png', + content, + updated: item.date, + language: 'zh-cn', + }; + }) + ) + )) as DataItem[], + allowEmpty: true, + language: 'zh-cn', + feedLink: 'https://rsshub.app/ruankao/news', + id: 'https://rsshub.app/ruankao/news', + }; +}; + +export const route: Route = { + path: '/news/:category', + name: 'CATTI 考试消息', + maintainers: ['PrinOrange'], + description: ` +| Category | 标题 | 描述 | +|-----------|------------|--------------------| +| ggl | 通知公告 | CATTI 考试通知和公告 | +| ywdt | 要闻动态 | CATTI 考试要闻动态 | +| zxzc | 最新政策 | CATTI 考试最新政策 | +`, + handler, + categories: ['study'], + parameters: { + category: '消息分类名,可在下面的描述中找到。', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + supportRadar: true, + }, + example: '/catti/news/zxzc', + radar: [ + { + source: ['www.catticenter.com/:category'], + }, + ], +}; diff --git a/lib/routes/cau/namespace.ts b/lib/routes/cau/namespace.ts index eb1d7960a4e418..6d069cf3c41888 100644 --- a/lib/routes/cau/namespace.ts +++ b/lib/routes/cau/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国农业大学', url: 'ciee.cau.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/caus/index.ts b/lib/routes/caus/index.ts index ad7e7dd394bb98..a7a8bf1bab2a65 100644 --- a/lib/routes/caus/index.ts +++ b/lib/routes/caus/index.ts @@ -46,8 +46,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 全部 | 要闻 | 商业 | 快讯 | 财富 | 生活 | - | ---- | ---- | ---- | ---- | ---- | ---- | - | 0 | 1 | 2 | 3 | 8 | 6 |`, +| ---- | ---- | ---- | ---- | ---- | ---- | +| 0 | 1 | 2 | 3 | 8 | 6 |`, }; async function handler(ctx) { diff --git a/lib/routes/caus/namespace.ts b/lib/routes/caus/namespace.ts index 0f48c051073d3a..52f68de483d495 100644 --- a/lib/routes/caus/namespace.ts +++ b/lib/routes/caus/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '加美财经', url: 'caus.com', + lang: 'zh-CN', }; diff --git a/lib/routes/cbaigui/namespace.ts b/lib/routes/cbaigui/namespace.ts index 47cb2b970082be..5095c30cb37728 100644 --- a/lib/routes/cbaigui/namespace.ts +++ b/lib/routes/cbaigui/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '纪妖', url: 'cbaigui.com', + lang: 'zh-CN', }; diff --git a/lib/routes/cbaigui/utils.ts b/lib/routes/cbaigui/utils.ts index 713d0d67622558..eb43835f155aa9 100644 --- a/lib/routes/cbaigui/utils.ts +++ b/lib/routes/cbaigui/utils.ts @@ -8,7 +8,7 @@ const GetFilterId = async (type, name) => { const { data: filterResponse } = await got(filterApiUrl); - return filterResponse.filter((f) => f.name === name).pop()?.id ?? undefined; + return filterResponse.findLast((f) => f.name === name)?.id ?? undefined; }; export { rootUrl, apiSlug, GetFilterId }; diff --git a/lib/routes/cbc/namespace.ts b/lib/routes/cbc/namespace.ts index 7d44268c5054fa..198963f7c307d5 100644 --- a/lib/routes/cbc/namespace.ts +++ b/lib/routes/cbc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Canadian Broadcasting Corporation', url: 'cbc.ca', + lang: 'en', }; diff --git a/lib/routes/cbirc/namespace.ts b/lib/routes/cbirc/namespace.ts index 3ccd490487bff4..c21982ff11e9d2 100644 --- a/lib/routes/cbirc/namespace.ts +++ b/lib/routes/cbirc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国银行保险监督管理委员会', url: 'cbirc.gov.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/cbnweek/namespace.ts b/lib/routes/cbnweek/namespace.ts index d122276f65f6f5..8f78fc29e263af 100644 --- a/lib/routes/cbnweek/namespace.ts +++ b/lib/routes/cbnweek/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '第一财经杂志', url: 'cbnweek.com', + lang: 'zh-CN', }; diff --git a/lib/routes/cbpanet/index.ts b/lib/routes/cbpanet/index.ts new file mode 100644 index 00000000000000..c21d1d2f2069f8 --- /dev/null +++ b/lib/routes/cbpanet/index.ts @@ -0,0 +1,380 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; + +export const handler = async (ctx) => { + const { bigId = '2', smallId = '11' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + + const rootUrl = 'https://www.cbpanet.com'; + const currentUrl = new URL(`dzp_news.aspx?bigid=${bigId}&smallid=${smallId}`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('div.divmore ul li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const a = item.find('div.zxcont1 a'); + + return { + title: a.text(), + pubDate: parseDate(item.find('div.zxtime1').text(), 'YY/MM/DD'), + link: new URL(a.prop('href'), rootUrl).href, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const description = $$('div.newscont').html(); + + item.title = $$('div.newstlt').text(); + item.description = description; + item.pubDate = timezone( + parseDate( + $$('div.newstime') + .text() + .replace(/发布时间:/, ''), + 'YYYY/M/D HH:mm:ss' + ), + +8 + ); + item.content = { + html: description, + text: $$('div.newscont').text(), + }; + return item; + }) + ) + ); + + const title = $('title').text(); + const image = new URL($('div#logo img').prop('src'), rootUrl).href; + + return { + title, + description: title.split(/-/).pop(), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: title.split(/-/)[0], + language, + }; +}; + +export const route: Route = { + path: '/dzp_news/:bigId?/:smallId?', + name: '资讯', + url: 'cbpanet.com', + maintainers: ['nczitzk'], + handler, + example: '/cbpanet/dzp_news/2/11', + parameters: { + bigId: '分类 id,默认为 `2`,即行业资讯,可在对应分类页 URL 中找到', + smallId: '子分类 id,默认为 `11`,即行业资讯,可在对应分类页 URL 中找到', + }, + description: `::: tip + 若订阅 [行业资讯](https://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=11),网址为 \`https://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=11\`。截取 \`https://www.cbpanet.com/\` 的 \`bigid\` 和 \`smallid\` 的部分作为参数填入,此时路由为 [\`/cbpanet/dzp_news/4/15\`](https://rsshub.app/cbpanet/dzp_news/4/15)。 +::: + +
    +更多分类 + +#### [协会](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) | + +
    + `, + categories: ['new-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.cbpanet.com/dzp_news.aspx'], + target: (_, url) => { + url = new URL(url); + const bigId = url.searchParams.get('bigid'); + const smallId = url.searchParams.get('smallid'); + + return `/dzp_news${bigId ? `/${bigId}${smallId ? `/${smallId}` : ''}` : ''}`; + }, + }, + { + title: '协会 - 协会介绍', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/1/1', + }, + { + title: '协会 - 协会章程', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/1/2', + }, + { + title: '协会 - 理事会', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/1/3', + }, + { + title: '协会 - 内设机构', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/1/4', + }, + { + title: '协会 - 协会通知', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/1/5', + }, + { + title: '协会 - 协会活动', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/1/6', + }, + { + title: '协会 - 出版物', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/1/7', + }, + { + title: '协会 - 会员权利与义务', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/1/30', + }, + { + title: '行业资讯 - 国内资讯', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/2/8', + }, + { + title: '行业资讯 - 海外资讯', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/2/9', + }, + { + title: '行业资讯 - 企业新闻', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/2/10', + }, + { + title: '行业资讯 - 行业资讯', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/2/11', + }, + { + title: '行业资讯 - 热点聚焦', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/2/43', + }, + { + title: '行业资讯 - 今日推荐', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/2/44', + }, + { + title: '原料信息 - 价格行情', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/3/12', + }, + { + title: '原料信息 - 分析预测', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/3/13', + }, + { + title: '原料信息 - 原料信息', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/3/40', + }, + { + title: '原料信息 - 热点聚焦', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/3/45', + }, + { + title: '法规标准 - 法规资讯', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/4/15', + }, + { + title: '法规标准 - 法律法规', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/4/16', + }, + { + title: '法规标准 - 国内标准', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/4/14', + }, + { + title: '法规标准 - 国外标准', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/4/17', + }, + { + title: '法规标准 - 法规聚焦', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/4/46', + }, + { + title: '法规标准 - 今日推荐', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/4/47', + }, + { + title: '技术专区 - 产品介绍', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/5/18', + }, + { + title: '技术专区 - 科技成果', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/5/19', + }, + { + title: '技术专区 - 学术论文', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/5/20', + }, + { + title: '技术专区 - 资料下载', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/5/21', + }, + { + title: '技术专区 - 专家', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/5/50', + }, + { + title: '技术专区 - 民间智库', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/5/57', + }, + { + title: '豆制品消费指南 - 膳食指南', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/6/22', + }, + { + title: '豆制品消费指南 - 营养成分', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/6/23', + }, + { + title: '豆制品消费指南 - 豆食菜谱', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/6/24', + }, + { + title: '豆制品消费指南 - 问与答', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/6/31', + }, + { + title: '豆制品消费指南 - 今日推荐', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/6/48', + }, + { + title: '豆制品消费指南 - 消费热点', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/6/53', + }, + { + title: '营养与健康 - 大豆营养概况', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/7/25', + }, + { + title: '营养与健康 - 大豆食品和人类健康', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/7/26', + }, + { + title: '营养与健康 - 世界豆类日,爱豆大行动', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/7/27', + }, + { + title: '营养与健康 - 谣言粉碎机', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/7/29', + }, + { + title: '营养与健康 - 最新资讯', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/7/41', + }, + { + title: '营养与健康 - 专家视点', + source: ['www.cbpanet.com/dzp_news.aspx'], + target: '/dzp_news/7/49', + }, + ], +}; diff --git a/lib/routes/cbpanet/namespace.ts b/lib/routes/cbpanet/namespace.ts new file mode 100644 index 00000000000000..3469c0451c4f45 --- /dev/null +++ b/lib/routes/cbpanet/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国豆制品网', + url: 'cbpanet.com', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/ccac/namespace.ts b/lib/routes/ccac/namespace.ts index c66abae1f61d28..f999c12262859b 100644 --- a/lib/routes/ccac/namespace.ts +++ b/lib/routes/ccac/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Macau Independent Commission Against Corruption 澳门廉政公署', url: 'ccac.org.mo', + lang: 'zh-HK', }; diff --git a/lib/routes/ccac/news.ts b/lib/routes/ccac/news.ts index dcb5ad24323fd2..94f4cec3ae63fd 100644 --- a/lib/routes/ccac/news.ts +++ b/lib/routes/ccac/news.ts @@ -24,9 +24,9 @@ export const route: Route = { handler, description: `Category - | All | Detected Cases | Investigation Reports or Recommendations | Annual Reports | CCAC's Updates | - | --- | -------------- | ---------------------------------------- | -------------- | -------------- | - | all | case | Persuasion | AnnualReport | PCANews |`, +| All | Detected Cases | Investigation Reports or Recommendations | Annual Reports | CCAC's Updates | +| --- | -------------- | ---------------------------------------- | -------------- | -------------- | +| all | case | Persuasion | AnnualReport | PCANews |`, }; async function handler(ctx) { diff --git a/lib/routes/cccfna/index.ts b/lib/routes/cccfna/index.ts new file mode 100644 index 00000000000000..de3dabbf3cf9aa --- /dev/null +++ b/lib/routes/cccfna/index.ts @@ -0,0 +1,82 @@ +import { Route, DataItem } 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 '@/utils/ofetch'; + +export const route: Route = { + path: '/:category/:type?', + categories: ['government'], + example: '/cccfna/meirigengxin', + parameters: { + category: '文章种类,即一级分类,详情见下表', + type: '文章类型,即二级分类,详情见下表', + }, + radar: [ + { + source: ['www.cccfna.org.cn/:category/:type?'], + }, + ], + description: ` +::: tip +存在**二级分类**的**一级分类**不能单独当作参数,如:\`/cccfna/hangyezixun\` +::: + +文章的目录分级如下: + +- shanghuidongtai(商会通知) +- meirigengxin(每日更新) +- tongzhigonggao(通知公告) +- hangyezixun(行业资讯) + - zhengcedaohang(政策导航) + - yujinxinxi(预警信息) + - shichangdongtai(市场动态) + - gongxuxinxi(供需信息) +- maoyitongji(贸易统计) + - tongjikuaibao(统计快报) + - hangyetongji(行业统计) + - guobiemaoyi(国别贸易) + - maoyizhinan(贸易指南) +- nongchanpinbaogao(农产品报告) + - nongchanpinyuebao(农产品月报) + - zhongdianchanpinyuebao(重点产品月报) + - zhongdianchanpinzoushi(重点产品走势)`, + name: '资讯信息', + maintainers: ['hualiong'], + handler: async (ctx) => { + const { category, type } = ctx.req.param(); + const baseURL = `https://www.cccfna.org.cn/${category}${type ? '/' + type : ''}`; + + const response = await ofetch(baseURL); + const $ = load(response); + + const list: DataItem[] = $('body > script') + .last() + .text() + .match(new RegExp(`https://www.cccfna.org.cn/${category}/.+?.html`, 'g'))! + .slice(0, 15) + .map((link) => ({ title: '', link })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link!, async () => { + const $ = load(await ofetch(item.link!)); + const content = $('.list_cont'); + + item.title = content.find('.title').text(); + item.pubDate = timezone(parseDate(content.find('.tip > .time').text(), '发布时间:YYYY-MM-DD'), +8); + item.description = content.find('#article-content').html()!; + + return item; + }) + ) + ); + + return { + title: $('head > title').text(), + link: baseURL, + item: items as DataItem[], + }; + }, +}; diff --git a/lib/routes/cccfna/namespace.ts b/lib/routes/cccfna/namespace.ts new file mode 100644 index 00000000000000..2e11a76f2606eb --- /dev/null +++ b/lib/routes/cccfna/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国食品土畜进出口商会', + url: 'www.cccfna.org.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/cccmc/index.ts b/lib/routes/cccmc/index.ts new file mode 100644 index 00000000000000..6670bf10195b98 --- /dev/null +++ b/lib/routes/cccmc/index.ts @@ -0,0 +1,263 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +import cache from '@/utils/cache'; +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'; + +export const handler = async (ctx: Context): Promise => { + const { category = 'ywgg/tzgg' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '15', 10); + + const baseUrl: string = 'https://www.cccmc.org.cn'; + const targetUrl: string = new URL(category.endsWith('/') ? category : `${category}/`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + const regex: RegExp = /\{url:'(.*)',title:'(.*)',time:'(.*)'\},/g; + + items = + response + .match(regex) + ?.slice(0, limit) + .map((item): DataItem => { + const matches = item.match(/'(.*?)'/); + + const title: string = matches?.[2] ?? ''; + const pubDateStr: string | undefined = matches?.[3]; + const linkUrl: string | undefined = matches?.[1]; + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }) ?? []; + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.title').text(); + const description: string = $$('div#article-content').html() ?? ''; + const pubDateStr: string | undefined = $$('span.time').text().split(/:/).pop(); + const authorEls: Element[] = $$('span.form, span.from').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $$authorEl: Cheerio = $$(authorEl); + + return { + name: $$authorEl.text().split(/:/).pop() ?? $$authorEl.text(), + }; + }); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + author: authors, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const title: string = $('title').text(); + + return { + title, + description: title.split(/-/)[0].trim(), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('img.logo').attr('src'), + author: title.split(/-/)?.pop()?.trim(), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/:category{.+}?', + name: '通用', + url: 'www.cccmc.org.cn', + maintainers: ['nczitzk'], + handler, + example: '/cccmc/ywgg/tzgg', + parameters: { + category: '分类,默认为 `ywgg/tzgg`,即通知公告,可在对应分类页 URL 中找到, Category, `ywgg/tzgg`,即通知公告 by default', + }, + description: `:::tip +若订阅 [综合政策](https://www.cccmc.org.cn/zcfg/zhzc/),网址为 \`https://www.cccmc.org.cn/zcfg/zhzc/\`,请截取 \`https://www.cccmc.org.cn/\` 到末尾的部分 \`zcfg/zhzc\` 作为 \`category\` 参数填入,此时目标路由为 [\`/cccmc/zcfg/zhzc\`](https://rsshub.app/cccmc/zcfg/zhzc)。 +::: + +
    +更多分类 + +#### [会员之家](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) | +
    +`, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.cccmc.org.cn/:category'], + target: (params) => { + const category: string = params.category; + + return `/cccmc${category ? `/${category}` : ''}`; + }, + }, + { + title: '商业机会 - 供应信息', + source: ['www.cccmc.org.cn/syjh/gyxx/'], + target: '/syjh/gyxx', + }, + { + title: '商业机会 - 需求信息', + source: ['www.cccmc.org.cn/syjh/xqxx/'], + target: '/syjh/xqxx', + }, + { + title: '商业机会 - 合作信息', + source: ['www.cccmc.org.cn/syjh/hzxx/'], + target: '/syjh/hzxx', + }, + { + title: '商会党建 - 党群动态', + source: ['www.cccmc.org.cn/shdj/dqdt/'], + target: '/shdj/dqdt', + }, + { + title: '商会党建 - 党内法规', + source: ['www.cccmc.org.cn/shdj/dnfg/'], + target: '/shdj/dnfg', + }, + { + title: '商会党建 - 青年工作', + source: ['www.cccmc.org.cn/shdj/qngz/'], + target: '/shdj/qngz', + }, + { + title: '行业资讯 - 统计分析', + source: ['www.cccmc.org.cn/hyzx/tjfx/'], + target: '/hyzx/tjfx', + }, + { + title: '行业资讯 - 石油化工', + source: ['www.cccmc.org.cn/hyzx/syhg/'], + target: '/hyzx/syhg', + }, + { + title: '行业资讯 - 金属矿产', + source: ['www.cccmc.org.cn/hyzx/jskc/'], + target: '/hyzx/jskc', + }, + { + title: '行业资讯 - 五金建材', + source: ['www.cccmc.org.cn/hyzx/wjjc/'], + target: '/hyzx/wjjc', + }, + { + title: '会员之家 - 会员之声', + source: ['www.cccmc.org.cn/hyzj/hyzs/'], + target: '/hyzj/hyzs', + }, + { + title: '会员之家 - 会员动态', + source: ['www.cccmc.org.cn/hyzj/hydt/'], + target: '/hyzj/hydt', + }, + { + title: '会员之家 - 会员推介', + source: ['www.cccmc.org.cn/hyzj/hytj/'], + target: '/hyzj/hytj', + }, + { + title: '政策法规 - 综合政策', + source: ['www.cccmc.org.cn/zcfg/zhzc/'], + target: '/zcfg/zhzc', + }, + { + title: '政策法规 - 国内贸易', + source: ['www.cccmc.org.cn/zcfg/gnmy/'], + target: '/zcfg/gnmy', + }, + { + title: '政策法规 - 对外贸易', + source: ['www.cccmc.org.cn/zcfg/dwmy/'], + target: '/zcfg/dwmy', + }, + { + title: '政策法规 - 投资合作', + source: ['www.cccmc.org.cn/zcfg/tzhz/'], + target: '/zcfg/tzhz', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/cccmc/namespace.ts b/lib/routes/cccmc/namespace.ts new file mode 100644 index 00000000000000..aa6edefdc19481 --- /dev/null +++ b/lib/routes/cccmc/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国五矿化工进出口商会', + url: 'www.cccmc.org.cn', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/ccf/ccfcv/index.ts b/lib/routes/ccf/ccfcv/index.ts index 38481ea07bb648..13f99d4e9150e3 100644 --- a/lib/routes/ccf/ccfcv/index.ts +++ b/lib/routes/ccf/ccfcv/index.ts @@ -36,8 +36,8 @@ export const route: Route = { maintainers: ['elxy'], handler, description: `| 学术前沿 | 热点征文 | 学术会议 | - | -------- | -------- | -------- | - | xsqy | rdzw | xshy |`, +| -------- | -------- | -------- | +| xsqy | rdzw | xshy |`, }; async function handler(ctx) { diff --git a/lib/routes/ccf/namespace.ts b/lib/routes/ccf/namespace.ts index 7bc6344228534b..edfb6a17b85d75 100644 --- a/lib/routes/ccf/namespace.ts +++ b/lib/routes/ccf/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国计算机学会', url: 'ccf.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ccf/news.ts b/lib/routes/ccf/news.ts index 1fca4505a8a7f0..e3ddc11b654e91 100644 --- a/lib/routes/ccf/news.ts +++ b/lib/routes/ccf/news.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| CCF 新闻 | CCF 聚焦 | ACM 信息 | - | ----------- | -------- | --------- | - | Media\_list | Focus | ACM\_News |`, +| ----------- | -------- | --------- | +| Media\_list | Focus | ACM\_News |`, }; async function handler(ctx) { diff --git a/lib/routes/ccfa/index.ts b/lib/routes/ccfa/index.ts new file mode 100644 index 00000000000000..c52532a8528d8d --- /dev/null +++ b/lib/routes/ccfa/index.ts @@ -0,0 +1,216 @@ +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'; + +export const handler = async (ctx) => { + const { type = '1' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + + const rootUrl = 'http://www.ccfa.org.cn'; + const currentUrl = new URL(`portal/cn/xiehui_list.jsp?type=${type}`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + let items = $('div.page_right ul li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const a = item.find('a'); + + return { + title: a.text(), + pubDate: parseDate(item.find('span.list_time').text(), 'YYYY/MM/DD'), + link: new URL(a.prop('href'), currentUrl).href, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + if (!item.link.includes('ccfa.org.cn')) { + return item; + } + + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const title = $$('h2#title').text(); + const description = art(path.join(__dirname, 'templates/description.art'), { + intro: $$('div.artical_info_jianjie').html(), + description: $$('div.news_artical_txt').html(), + }); + + const pubDate = + $$('div.artical_info_left') + .text() + .match(/(\d{4}(?:\/\d{2}){2})/)?.[1] ?? undefined; + + item.title = title; + item.description = description; + item.pubDate = pubDate ? parseDate(pubDate, 'YYYY/MM/DD') : item.pubDate; + item.author = $$('div.artical_info_left') + .text() + .split(/来源:/) + .pop(); + item.content = { + html: description, + text: $$('div.news_artical_txt').text(), + }; + + const attachmentEl = + $$('p.download').length === 0 + ? undefined + : $$('div.news_artical_txt a') + .toArray() + .find((a) => $$(a).prop('href')?.includes('downFiles.do')); + + item.enclosure_url = attachmentEl ? new URL($$(attachmentEl).prop('href'), rootUrl) : undefined; + item.enclosure_title = attachmentEl ? $$(attachmentEl).text() : undefined; + + return item; + }) + ) + ); + + const description = $('li.page_tit').contents().last().text().split(/>/).pop(); + const image = new URL($('div.logo img').prop('src'), currentUrl).href; + const author = $('title').text(); + + return { + title: `${author} - ${description}`, + description, + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $('meta[property="og:site_name"]').prop('content'), + }; +}; + +export const route: Route = { + path: '/:type?', + name: '分类', + url: 'www.ccfa.org.cn', + maintainers: ['nczitzk'], + handler, + example: '/ccfa/1', + parameters: { category: '分类,默认为 `1`,即协会动态,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [协会动态](https://www.ccfa.org.cn/portal/cn/xiehui_list.jsp?type=1),网址为 \`https://www.ccfa.org.cn/portal/cn/xiehui_list.jsp?type=1\`。截取 \`https://www.ccfa.org.cn/portal/cn/xiehui_list.jsp?type=\` 到末尾的部分 \`1\` 作为参数填入,此时路由为 [\`/ccfa/1\`](https://rsshub.app/ccfa/1)。 +::: + +| 分类 | ID | +| ------------------------------------------------------------------------- | -------------------------------------- | +| [协会动态](http://www.ccfa.org.cn/portal/cn/xiehui_list.jsp?type=1) | [1](https://rsshub.app/ccfa/1) | +| [行业动态](http://www.ccfa.org.cn/portal/cn/xiehui_list.jsp?type=2) | [2](https://rsshub.app/ccfa/2) | +| [政策/报告/标准](http://www.ccfa.org.cn/portal/cn/hybz_list.jsp?type=33) | [33](https://rsshub.app/ccfa/33) | +| [行业统计](http://www.ccfa.org.cn/portal/cn/lsbq.jsp?type=10003) | [10003](https://rsshub.app/ccfa/10003) | +| [创新案例](http://www.ccfa.org.cn/portal/cn/hybzs_list.jsp?type=10004) | [10004](https://rsshub.app/ccfa/10004) | +| [党建工作](http://www.ccfa.org.cn/portal/cn/xiehui_list.jsp?type=7) | [7](https://rsshub.app/ccfa/7) | +| [新消费论坛](http://www.ccfa.org.cn/portal/cn/xiehui_list.jsp?type=10005) | [10005](https://rsshub.app/ccfa/10005) | + +#### [政策/报告/标准](http://www.ccfa.org.cn/portal/cn/hybz_list.jsp?type=33) + +| 分类 | ID | +| ------------------------------------------------------------------------------- | -------------------------------- | +| [行业报告](http://www.ccfa.org.cn/portal/cn/hybz_list.jsp?type=33) | [33](https://rsshub.app/ccfa/33) | +| [行业标准](http://www.ccfa.org.cn/portal/cn/hybz_list.jsp?type=34) | [34](https://rsshub.app/ccfa/34) | +| [行业政策](http://www.ccfa.org.cn/portal/cn/fangyizhuanqu_list.jsp?type=39) | [39](https://rsshub.app/ccfa/39) | +| [政策权威解读](http://www.ccfa.org.cn/portal/cn/fangyizhuanqu_list.jsp?type=40) | [40](https://rsshub.app/ccfa/40) | + `, + categories: ['new-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: [ + 'www.ccfa.org.cn/portal/cn/xiehui_list.jsp', + 'www.ccfa.org.cn/portal/cn/hybz_list.jsp', + 'www.ccfa.org.cn/portal/cn/lsbq.jsp', + 'www.ccfa.org.cn/portal/cn/hybzs_list.jsp', + 'www.ccfa.org.cn/portal/cn/fangyizhuanqu_list.jsp', + ], + target: (_, url) => { + url = new URL(url); + const type = url.searchParams.get('type'); + + return type ? `/${type}` : ''; + }, + }, + { + title: '协会动态', + source: ['www.ccfa.org.cn/portal/cn/xiehui_list.jsp'], + target: '/1', + }, + { + title: '行业动态', + source: ['www.ccfa.org.cn/portal/cn/xiehui_list.jsp'], + target: '/2', + }, + { + title: '政策/报告/标准', + source: ['www.ccfa.org.cn/portal/cn/hybz_list.jsp'], + target: '/33', + }, + { + title: '行业统计', + source: ['www.ccfa.org.cn/portal/cn/lsbq.jsp'], + target: '/10003', + }, + { + title: '创新案例', + source: ['www.ccfa.org.cn/portal/cn/hybzs_list.jsp'], + target: '/10004', + }, + { + title: '党建工作', + source: ['www.ccfa.org.cn/portal/cn/xiehui_list.jsp'], + target: '/7', + }, + { + title: '新消费论坛', + source: ['www.ccfa.org.cn/portal/cn/xiehui_list.jsp'], + target: '/10005', + }, + { + title: '政策/报告/标准 - 行业报告', + source: ['www.ccfa.org.cn/portal/cn/hybz_list.jsp'], + target: '/33', + }, + { + title: '政策/报告/标准 - 行业标准', + source: ['www.ccfa.org.cn/portal/cn/hybz_list.jsp'], + target: '/34', + }, + { + title: '政策/报告/标准 - 行业政策', + source: ['www.ccfa.org.cn/portal/cn/fangyizhuanqu_list.jsp'], + target: '/39', + }, + { + title: '政策/报告/标准 - 政策权威解读', + source: ['www.ccfa.org.cn/portal/cn/fangyizhuanqu_list.jsp'], + target: '/40', + }, + ], +}; diff --git a/lib/routes/ccfa/namespace.ts b/lib/routes/ccfa/namespace.ts new file mode 100644 index 00000000000000..8c92d2d6b68afd --- /dev/null +++ b/lib/routes/ccfa/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国连锁经营协会', + url: 'ccfa.org.cn', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/ccfa/templates/description.art b/lib/routes/ccfa/templates/description.art new file mode 100644 index 00000000000000..57498ab45a9d86 --- /dev/null +++ b/lib/routes/ccfa/templates/description.art @@ -0,0 +1,7 @@ +{{ if intro }} +
    {{ 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; +} + +function parseArticle($: CheerioAPI): DataItem { + const linkData = parseLinkData($); + let description = parseDescription($('.body-content'), $); + const $articleHeader = $('.article-header__image'); + if ($articleHeader.length) { + description = `
    ${$articleHeader.html()}

    ${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 = `
    ${figure.html()}

    ${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 = `
    ${figure.html()}

    ${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 = (JSON.parse($('script[type="application/ld+json"]').text()))['@graph'][0]; + + return { + title: data.name, + pubDate: parseDate(data.dateModified), + }; + } catch { + // ignore + } +} + +function getVideoIframe($ele: Cheerio) { + const setup = $ele.find('video').data('setup') as VideoSetup; + if (setup) { + const youtubeSource = setup.sources.find((i) => i.type === 'video/youtube'); + if (youtubeSource) { + const videoId = youtubeSource.src.match(/\?v=([^&]+)/)?.[1]; + if (videoId) { + return ``; + } + } + } +} + +function parseDescription($description: Cheerio, $: CheerioAPI) { + $description.find('.desktop-only').remove(); + $description.find('.mobile-only').remove(); + $description.find('.newsletter-tout').remove(); + $description.find('.carousel-gallery').remove(); + $description.find('svg').remove(); + $description.find('.field--image').each((_, ele) => { + $(ele).replaceWith($(ele).find('img')); + }); + $description.find('.video-embed').each((_, ele) => { + const $ele = $(ele); + const videoIframe = getVideoIframe($ele); + if (videoIframe) { + $ele.replaceWith(videoIframe); + } + }); + + const description = $description.html() ?? ''; + + return description; +} + +export async function asyncPoolAll(poolLimit: number, array: readonly IN[], iteratorFn: (generator: IN) => Promise) { + const results: Awaited = []; + for await (const result of asyncPool(poolLimit, array, iteratorFn)) { + results.push(result); + } + return results; +} diff --git a/lib/routes/cgtn/namespace.ts b/lib/routes/cgtn/namespace.ts index 61c80e9b34a63f..3d5cfe086ee5b5 100644 --- a/lib/routes/cgtn/namespace.ts +++ b/lib/routes/cgtn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国环球电视网', url: 'cgtn.com', + lang: 'zh-CN', }; diff --git a/lib/routes/chaincatcher/namespace.ts b/lib/routes/chaincatcher/namespace.ts index e3c714a0f6f93b..babd6f0982137f 100644 --- a/lib/routes/chaincatcher/namespace.ts +++ b/lib/routes/chaincatcher/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '链捕手 ChainCatcher', url: 'chaincatcher.com', + lang: 'zh-CN', }; diff --git a/lib/routes/changba/namespace.ts b/lib/routes/changba/namespace.ts index 44b2ca3e138db8..cbaf6b7c995a55 100644 --- a/lib/routes/changba/namespace.ts +++ b/lib/routes/changba/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '唱吧', url: 'changba.com', + lang: 'zh-CN', }; diff --git a/lib/routes/changba/user.ts b/lib/routes/changba/user.ts index db74c3e405fcbf..0443cd036909d6 100644 --- a/lib/routes/changba/user.ts +++ b/lib/routes/changba/user.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); @@ -11,7 +11,8 @@ const headers = { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Ma export const route: Route = { path: '/:userid', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.Audios, example: '/changba/skp6hhF59n48R-UpqO3izw', parameters: { userid: '用户ID, 可在对应分享页面的 URL 中找到' }, features: { @@ -28,7 +29,7 @@ export const route: Route = { }, ], name: '用户', - maintainers: [], + maintainers: ['kt286', 'xizeyoupan', 'pseudoyu'], handler, }; @@ -101,9 +102,9 @@ async function handler(ctx) { items = items.filter(Boolean); return { - title: $('title').text(), + title: author + ' - 唱吧', link: url, - description: $('meta[name="description"]').attr('content') || $('title').text(), + description: $('meta[name="description"]').attr('content') || author + ' - 唱吧', item: items, image: authorimg, itunes_author: author, diff --git a/lib/routes/chaoxing/namespace.ts b/lib/routes/chaoxing/namespace.ts index e054c9f6680c9a..53af60795d994b 100644 --- a/lib/routes/chaoxing/namespace.ts +++ b/lib/routes/chaoxing/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '超星', url: 'chaoxing.com', + lang: 'zh-CN', }; diff --git a/lib/routes/chaoxing/qk.ts b/lib/routes/chaoxing/qk.ts index ce231c55285a30..a1caddeb62b2fe 100644 --- a/lib/routes/chaoxing/qk.ts +++ b/lib/routes/chaoxing/qk.ts @@ -25,19 +25,19 @@ export const route: Route = { name: '期刊', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 全部期刊可以在 [这里](http://qk.chaoxing.com/space/index) 找到,你也可以从 [学科分类](https://qikan.chaoxing.com/jourclassify) 和 [期刊导航](https://qikan.chaoxing.com/search/openmag) 中发现更多期刊。 如订阅 [**上海文艺**](http://m.chaoxing.com/mqk/list?sw=\&mags=6b5c39b3dd84352be512e29df0297437\&isort=20\&from=space),其 URL 为 \`http://m.chaoxing.com/mqk/list?mags=6b5c39b3dd84352be512e29df0297437\`。\`6b5c39b3dd84352be512e29df0297437\` 即为期刊 id,所得路由为 [\`/chaoxing/qk/6b5c39b3dd84352be512e29df0297437\`](https://rsshub.app/chaoxing/qk/6b5c39b3dd84352be512e29df0297437) - ::: +::: - :::warning +::: warning 你可以设置参数 **需要获取文章全文** 为 \`true\` \`yes\` \`t\` \`y\` 等值(或者忽略这个参数),RSS 的条目会携带期刊中的 **文章全文**,而不仅仅是 **文章概要**。但因为发起访问请求过多会被该网站屏蔽,你可以将其关闭(设置该参数为 \`false\` \`no\` \`f\` \`n\` 等值),这将会大大减少请求次数从而更难触发网站的反爬机制。 路由默认会获取 **30** 个条目。在路由后指定 \`?limit=<条目数量>\` 减少或增加单次获取条目数量,同样可以减少请求次数,如设置为一次获取 **10** 个条目,路由可以更改为 [\`/chaoxing/qk/6b5c39b3dd84352be512e29df0297437?limit=10\`](https://rsshub.app/chaoxing/qk/6b5c39b3dd84352be512e29df0297437?limit=10) 在根据上文设置 **需要获取文章全文** 为不需要时,你可以将 \`limit\` 值增大,从而获取更多的条目,此时因为不获取全文也不会触发反爬机制,如 [\`/chaoxing/qk/6b5c39b3dd84352be512e29df0297437/false?limit=100\`](https://rsshub.app/chaoxing/qk/6b5c39b3dd84352be512e29df0297437/false?limit=100) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/chaping/banner.ts b/lib/routes/chaping/banner.ts index 359f30701dd447..5cf19fce6b1bbe 100644 --- a/lib/routes/chaping/banner.ts +++ b/lib/routes/chaping/banner.ts @@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/banner', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/chaping/banner', parameters: {}, features: { diff --git a/lib/routes/chaping/namespace.ts b/lib/routes/chaping/namespace.ts index c01167a7fbbbe9..696bef8a0083f3 100644 --- a/lib/routes/chaping/namespace.ts +++ b/lib/routes/chaping/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '差评', url: 'chaping.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/chaping/news.ts b/lib/routes/chaping/news.ts index 8dcbc3d5694411..b5febffac0d8b9 100644 --- a/lib/routes/chaping/news.ts +++ b/lib/routes/chaping/news.ts @@ -17,7 +17,7 @@ const titles = { export const route: Route = { path: '/news/:caty?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/chaping/news/15', parameters: { caty: '分类,默认为全部资讯' }, features: { @@ -32,15 +32,15 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 编号 | 分类 | - | ---- | ---------- | - | 15 | 直播 | - | 3 | 科技新鲜事 | - | 7 | 互联网槽点 | - | 5 | 趣味科技 | - | 6 | DEBUG TIME | - | 1 | 游戏 | - | 8 | 视频 | - | 9 | 公里每小时 |`, +| ---- | ---------- | +| 15 | 直播 | +| 3 | 科技新鲜事 | +| 7 | 互联网槽点 | +| 5 | 趣味科技 | +| 6 | DEBUG TIME | +| 1 | 游戏 | +| 8 | 视频 | +| 9 | 公里每小时 |`, }; async function handler(ctx) { diff --git a/lib/routes/chaping/newsflash.ts b/lib/routes/chaping/newsflash.ts index 91868481e9e5ba..2f45d044f2698f 100644 --- a/lib/routes/chaping/newsflash.ts +++ b/lib/routes/chaping/newsflash.ts @@ -1,12 +1,12 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; const host = 'https://chaping.cn'; export const route: Route = { path: '/newsflash', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/chaping/newsflash', parameters: {}, features: { @@ -30,7 +30,7 @@ export const route: Route = { async function handler() { const newflashAPI = `${host}/api/official/information/newsflash?page=1&limit=21`; - const response = await got(newflashAPI).json(); + const response = await ofetch(newflashAPI); const data = response.data; return { diff --git a/lib/routes/cherrytimes/market.ts b/lib/routes/cherrytimes/market.ts new file mode 100644 index 00000000000000..121be3e0beaeda --- /dev/null +++ b/lib/routes/cherrytimes/market.ts @@ -0,0 +1,35 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/market', + categories: ['new-media'], + example: '/cherrytimes/market', + name: 'Market', + maintainers: ['canonnizq'], + + handler: async () => { + const response = await ofetch('https://cherrytimes.it/en/tag/markets?page=1'); + const $ = load(response); + const items = $('div.post-container') + .toArray() + .map((item) => { + const element = $(item); + const a = element.find('a').eq(1); + + return { + title: a.text(), + link: a.attr('href'), + description: element.find('p.excerpt').text(), + category: element.find('a').last().text(), + }; + }); + + return { + title: 'Cherry Times - Market', + link: 'https://cherrytimes.it/en/tag/markets', + item: items, + }; + }, +}; diff --git a/lib/routes/arknights/namespace.ts b/lib/routes/cherrytimes/namespace.ts similarity index 60% rename from lib/routes/arknights/namespace.ts rename to lib/routes/cherrytimes/namespace.ts index 1750ce795b1bb8..addabe52c401d5 100644 --- a/lib/routes/arknights/namespace.ts +++ b/lib/routes/cherrytimes/namespace.ts @@ -1,6 +1,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: '明日方舟', - url: 'ak.arknights.jp', + name: 'Cherry Times', + url: 'cherrytimes.it', }; diff --git a/lib/routes/chiculture/namespace.ts b/lib/routes/chiculture/namespace.ts index 521bad6fcee6a5..40c02be395bd62 100644 --- a/lib/routes/chiculture/namespace.ts +++ b/lib/routes/chiculture/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '通識・現代中國', url: 'chiculture.org.hk', + lang: 'zh-HK', }; diff --git a/lib/routes/chiculture/topic.ts b/lib/routes/chiculture/topic.ts index 4c86dcc1d777cd..c992caa535a5d5 100644 --- a/lib/routes/chiculture/topic.ts +++ b/lib/routes/chiculture/topic.ts @@ -22,8 +22,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 全部 | 現代中國 | 今日香港 | 全球化 | 一周時事通識 | - | ---- | -------- | -------- | ------ | ------------ | - | | 76 | 479 | 480 | 379 |`, +| ---- | -------- | -------- | ------ | ------------ | +| | 76 | 479 | 480 | 379 |`, }; async function handler(ctx) { diff --git a/lib/routes/chikubi/category.ts b/lib/routes/chikubi/category.ts new file mode 100644 index 00000000000000..b640b94f121159 --- /dev/null +++ b/lib/routes/chikubi/category.ts @@ -0,0 +1,41 @@ +import { Route, Data } from '@/types'; +import { getBySlug, getPostsBy } from './utils'; + +export const route: Route = { + path: '/category/:keyword', + categories: ['multimedia'], + example: '/chikubi/category/nipple-lesbian', + parameters: { keyword: 'Keyword' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Category', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'Category', + source: ['chikubi.jp/category/:keyword'], + target: '/category/:keyword', + }, + ], +}; + +async function handler(ctx): Promise { + const baseUrl = 'https://chikubi.jp'; + const { keyword } = ctx.req.param(); + const { id, name } = await getBySlug('category', keyword); + + const items = await getPostsBy('category', id); + + return { + title: `Category: ${name} - chikubi.jp`, + link: `${baseUrl}/category/${keyword}`, + item: items, + }; +} diff --git a/lib/routes/chikubi/index.ts b/lib/routes/chikubi/index.ts new file mode 100644 index 00000000000000..444d2fe0da288e --- /dev/null +++ b/lib/routes/chikubi/index.ts @@ -0,0 +1,66 @@ +import { Route, Data } from '@/types'; +import { getPosts } from './utils'; + +export const route: Route = { + path: '/', + categories: ['multimedia'], + example: '/chikubi', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '最新記事', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: '最新記事', + source: ['chikubi.jp/'], + target: '/', + }, + { + title: '殿堂', + source: ['chikubi.jp/best-nipple-article'], + target: '/best', + }, + { + title: '動畫', + source: ['chikubi.jp/nipple-video'], + target: '/video', + }, + { + title: 'VR', + source: ['chikubi.jp/nipple-video-category/cat-nipple-video-vr'], + target: '/vr', + }, + { + title: '漫畫', + source: ['chikubi.jp/comic'], + target: '/comic', + }, + { + title: '音聲', + source: ['chikubi.jp/voice'], + target: '/voice', + }, + { + title: 'CG・イラスト', + source: ['chikubi.jp/cg'], + target: '/cg', + }, + ], +}; + +async function handler(): Promise { + const items = await getPosts(); + + return { + title: '最新記事 - chikubi.jp', + link: 'https://chikubi.jp', + item: items, + }; +} diff --git a/lib/routes/chikubi/namespace.ts b/lib/routes/chikubi/namespace.ts new file mode 100644 index 00000000000000..787a1627628f94 --- /dev/null +++ b/lib/routes/chikubi/namespace.ts @@ -0,0 +1,16 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '乳首ふぇち', + url: 'chikubi.jp', + description: `::: tip +The content of 乳首ふぇち is divided into two parts: + +Works: Only reposts official product descriptions. +Posts: Contains the website author's thoughts and additional information. + +Sometimes a product may exist in both posts and works. +Sometimes there might be only a single post without any reposted work, and vice versa. +:::`, + lang: 'ja', +}; diff --git a/lib/routes/chikubi/navigation.ts b/lib/routes/chikubi/navigation.ts new file mode 100644 index 00000000000000..b512b7c8aab439 --- /dev/null +++ b/lib/routes/chikubi/navigation.ts @@ -0,0 +1,66 @@ +import { Route, Data } from '@/types'; +import { getBySlug, getPostsBy, processItems } from './utils'; +import parser from '@/utils/rss-parser'; + +export const route: Route = { + path: '/:keyword', + categories: ['multimedia'], + example: '/chikubi', + parameters: { keyword: '導覽列,見下表,默認爲最新' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Navigation', + maintainers: ['SnowAgar25'], + handler, + description: `| 殿堂 | 動畫 | VR | 漫畫 | 音聲 | CG・イラスト | +| ---- | ----- | -- | ----- | ----- | -- | +| best | video | vr | comic | voice | cg |`, +}; + +const navigationItems = { + video: { url: '/nipple-video', title: '動畫' }, + vr: { url: '/nipple-video-category/cat-nipple-video-vr', title: 'VR' }, + comic: { url: '/comic', title: '漫畫' }, + voice: { url: '/voice', title: '音聲' }, + cg: { url: '/cg', title: 'CG' }, +}; + +async function handler(ctx): Promise { + const keyword = ctx.req.param('keyword') ?? ''; + const baseUrl = 'https://chikubi.jp'; + + if (keyword === 'best') { + const { id } = await getBySlug('category', 'nipple-best'); + const items = await getPostsBy('category', id); + + return { + title: '殿堂 - chikubi.jp', + link: `${baseUrl}/best-nipple-article`, + item: items, + }; + } else { + const { url, title } = navigationItems[keyword]; + + const feed = await parser.parseURL(`${baseUrl}${url}/feed`); + + const list = feed.items.map((item) => ({ + title: item.title, + link: item.link, + })); + + // 獲取內文 + const items = await processItems(list); + + return { + title: `${title} - chikubi.jp`, + link: `${baseUrl}${url}`, + item: items, + }; + } +} diff --git a/lib/routes/chikubi/nipple-video-category.ts b/lib/routes/chikubi/nipple-video-category.ts new file mode 100644 index 00000000000000..02042a34ad2212 --- /dev/null +++ b/lib/routes/chikubi/nipple-video-category.ts @@ -0,0 +1,49 @@ +import { Route, Data } from '@/types'; +import { processItems } from './utils'; +import parser from '@/utils/rss-parser'; + +export const route: Route = { + path: '/nipple-video-category/:keyword', + categories: ['multimedia'], + example: '/chikubi/nipple-video-category/cat-nipple-video-god', + parameters: { keyword: 'Keyword' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '動画カテゴリー', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: '動画カテゴリー', + source: ['chikubi.jp/nipple-video-category/:keyword'], + target: '/nipple-video-category/:keyword', + }, + ], +}; + +async function handler(ctx): Promise { + const { keyword } = ctx.req.param(); + const baseUrl = 'https://chikubi.jp'; + const url = `/nipple-video-category/${encodeURIComponent(keyword)}`; + + const feed = await parser.parseURL(`${baseUrl}${url}/feed`); + + const list = feed.items.map((item) => ({ + title: item.title, + link: item.link, + })); + + const items = await processItems(list); + + return { + title: `動画カテゴリー: ${feed.title?.split('-')[0]} - chikubi.jp`, + link: `${baseUrl}${url}`, + item: items, + }; +} diff --git a/lib/routes/chikubi/nipple-video-maker.ts b/lib/routes/chikubi/nipple-video-maker.ts new file mode 100644 index 00000000000000..d563c8cad8e58b --- /dev/null +++ b/lib/routes/chikubi/nipple-video-maker.ts @@ -0,0 +1,49 @@ +import { Route, Data } from '@/types'; +import { processItems } from './utils'; +import parser from '@/utils/rss-parser'; + +export const route: Route = { + path: '/nipple-video-maker/:keyword', + categories: ['multimedia'], + example: '/chikubi/nipple-video-maker/nipple-video-maker-nh', + parameters: { keyword: 'Keyword' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'AVメーカー', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'AVメーカー', + source: ['chikubi.jp/nipple-video-maker/:keyword'], + target: '/nipple-video-maker/:keyword', + }, + ], +}; + +async function handler(ctx): Promise { + const { keyword } = ctx.req.param(); + const baseUrl = 'https://chikubi.jp'; + const url = `/nipple-video-maker/${encodeURIComponent(keyword)}`; + + const feed = await parser.parseURL(`${baseUrl}${url}/feed`); + + const list = feed.items.map((item) => ({ + title: item.title, + link: item.link, + })); + + const items = await processItems(list); + + return { + title: `AVメーカー: ${feed.title?.split('-')[0]} - chikubi.jp`, + link: `${baseUrl}${url}`, + item: items, + }; +} diff --git a/lib/routes/chikubi/search.ts b/lib/routes/chikubi/search.ts new file mode 100644 index 00000000000000..57519077bca051 --- /dev/null +++ b/lib/routes/chikubi/search.ts @@ -0,0 +1,39 @@ +import { Route, Data } from '@/types'; +import { getPosts } from './utils'; +import got from '@/utils/got'; + +export const route: Route = { + path: '/search/:keyword', + categories: ['multimedia'], + example: '/chikubi/search/ギャップ', + parameters: { keyword: 'Keyword' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Search', + maintainers: ['SnowAgar25'], + handler, +}; + +async function handler(ctx): Promise { + const { keyword } = ctx.req.param(); + const baseUrl = 'https://chikubi.jp'; + const searchUrl = `${baseUrl}/wp-json/wp/v2/search?search=${keyword}`; + + const response = await got.get(searchUrl); + const searchResults = response.data; + + const postIds = searchResults.map((item) => item.id.toString()); + const items = await getPosts(postIds); + + return { + title: `Search: ${keyword} - chikubi.jp`, + link: `${baseUrl}/search/${encodeURIComponent(keyword)}`, + item: items, + }; +} diff --git a/lib/routes/chikubi/tag.ts b/lib/routes/chikubi/tag.ts new file mode 100644 index 00000000000000..148514eb2f3e48 --- /dev/null +++ b/lib/routes/chikubi/tag.ts @@ -0,0 +1,41 @@ +import { Route, Data } from '@/types'; +import { getBySlug, getPostsBy } from './utils'; + +export const route: Route = { + path: '/tag/:keyword', + categories: ['multimedia'], + example: '/chikubi/tag/ドリームチケット', + parameters: { keyword: 'Keyword' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Tag', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'Tag', + source: ['chikubi.jp/tag/:keyword'], + target: '/tag/:keyword', + }, + ], +}; + +async function handler(ctx): Promise { + const baseUrl = 'https://chikubi.jp'; + const { keyword } = ctx.req.param(); + const { id, name } = await getBySlug('tag', keyword); + + const items = await getPostsBy('tag', id); + + return { + title: `Tag: ${name} - chikubi.jp`, + link: `${baseUrl}/category/${keyword}`, + item: items, + }; +} diff --git a/lib/routes/chikubi/utils.ts b/lib/routes/chikubi/utils.ts new file mode 100644 index 00000000000000..2edbdea745f69a --- /dev/null +++ b/lib/routes/chikubi/utils.ts @@ -0,0 +1,135 @@ +import { DataItem } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +const CONTENT_TYPES = { + doujin: { + title: '.doujin-title', + description: ['.doujin-detail', '.section', '.area-buy > a.btn'], + }, + video: { + title: '.video-title', + description: ['.video-data', '.section', '.lp-samplearea a.btn'], + }, + article: { + title: '.article_title', + description: ['.article_icatch', '.article_contents'], + }, +}; + +function getContentType(link: string): keyof typeof CONTENT_TYPES { + const typePatterns = { + doujin: ['/cg/', '/comic/', '/voice/'], + video: ['/nipple-video/'], + article: ['/post-'], + }; + + for (const [type, patterns] of Object.entries(typePatterns)) { + if (patterns.some((pattern) => link.includes(pattern))) { + return type as keyof typeof CONTENT_TYPES; + } + } + + throw new Error(`Unknown content type for link: ${link}`); +} + +export async function processItems(list): Promise { + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got(item.link); + const $ = load(detailResponse.data); + + const contentType = getContentType(item.link); + const selectors = CONTENT_TYPES[contentType]; + + const title = $(selectors.title).text().trim() || item.title; + const description = processDescription(selectors.description.map((selector) => $(selector).prop('outerHTML')).join('')); + + const pubDateStr = $('meta[property="article:published_time"]').attr('content'); + const pubDate = pubDateStr ? parseDate(pubDateStr) : undefined; + + return { + title, + description, + link: item.link, + pubDate, + } as DataItem; + }) + ) + ); + + return items.filter((item): item is DataItem => item !== null); +} + +function processDescription(description: string): string { + const $ = load(description); + return $('body') + .children() + .toArray() + .map((el) => $(el).clone().wrap('
    ').parent().html()) + .join(''); +} + +const WP_REST_API_URL = 'https://chikubi.jp/wp-json/wp/v2'; + +export async function getPosts(ids?: string[]): Promise { + const url = `${WP_REST_API_URL}/posts${ids?.length ? `?include=${ids.join(',')}` : ''}`; + + const cachedData = await cache.tryGet(url, async () => { + const response = await got(url); + const data = JSON.parse(response.body); + + if (!Array.isArray(data)) { + throw new TypeError('No posts found for the given IDs'); + } + + return data.map(({ title, link, date, content }) => ({ + title: title.rendered, + link, + pubDate: parseDate(date), + description: processDescription(content.rendered), + })); + }); + + return (Array.isArray(cachedData) ? cachedData : []).filter((item): item is DataItem => item !== null); +} + +const API_TYPES = { + tag: 'tags', + category: 'categories', +}; + +export async function getBySlug(type: T, slug: string): Promise<{ id: number; name: string }> { + const url = `${WP_REST_API_URL}/${API_TYPES[type]}?slug=${encodeURIComponent(slug)}`; + const { body } = await got(url); + const data = JSON.parse(body); + + if (data?.[0]) { + const { id, name } = data[0]; + return { id, name }; + } + throw new Error(`No ${type} found for slug: ${slug}`); +} + +export async function getPostsBy(type: T, id: number): Promise { + const url = `${WP_REST_API_URL}/posts?${API_TYPES[type]}=${id}`; + const cachedData = await cache.tryGet(url, async () => { + const { body } = await got(url); + const data = JSON.parse(body); + + if (Array.isArray(data) && data.length > 0) { + return data.map(({ title, link, date, content }) => ({ + title: title.rendered, + link, + pubDate: parseDate(date), + description: processDescription(content.rendered), + })); + } + return []; + }); + + return (Array.isArray(cachedData) ? cachedData : []).filter((item): item is DataItem => item !== null); +} diff --git a/lib/routes/china/finance/finance.ts b/lib/routes/china/finance/finance.ts index 6ccf25903f7289..29e683502a09fc 100644 --- a/lib/routes/china/finance/finance.ts +++ b/lib/routes/china/finance/finance.ts @@ -38,8 +38,8 @@ export const route: Route = { maintainers: ['KingJem'], handler, description: `| 推荐 | TMT | 金融 | 地产 | 消费 | 医药 | 酒业 | IPO 观察 | - | ------- | --- | ------- | ------ | ------- | ----- | ---- | -------- | - | tuijian | TMT | jinrong | dichan | xiaofei | yiyao | wine | IPO | +| ------- | --- | ------- | ------ | ------- | ----- | ---- | -------- | +| tuijian | TMT | jinrong | dichan | xiaofei | yiyao | wine | IPO | > Note: The default news num is \`30\`. diff --git a/lib/routes/china/namespace.ts b/lib/routes/china/namespace.ts index 16b89b5751feae..b2c9da42f8568d 100644 --- a/lib/routes/china/namespace.ts +++ b/lib/routes/china/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'China.com 中华网', url: 'finance.china.com', + lang: 'zh-CN', }; diff --git a/lib/routes/china/news/highlights/news.ts b/lib/routes/china/news/highlights/news.ts index 54a8349fe8560c..cda5def54276f2 100644 --- a/lib/routes/china/news/highlights/news.ts +++ b/lib/routes/china/news/highlights/news.ts @@ -33,9 +33,9 @@ export const route: Route = { handler, description: `Category of news - | China News | International News | Social News | Breaking News | - | ---------- | ------------------ | ----------- | ------------- | - | domestic | international | social | news100 |`, +| China News | International News | Social News | Breaking News | +| ---------- | ------------------ | ----------- | ------------- | +| domestic | international | social | news100 |`, }; async function handler(ctx) { diff --git a/lib/routes/chinacdc/index.ts b/lib/routes/chinacdc/index.ts new file mode 100644 index 00000000000000..bdfcf4408eb40c --- /dev/null +++ b/lib/routes/chinacdc/index.ts @@ -0,0 +1,505 @@ +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 = 'zxyw' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '11', 10); + + const rootUrl: string = 'https://www.chinacdc.cn'; + const targetUrl: string = new URL(category.endsWith('/') ? category : `${category}/`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang'); + + let items: DataItem[] = $('ul.xw_list li') + .slice(0, limit) + .toArray() + .map((item): DataItem => { + const $item: Cheerio = $(item); + + const aEl: Cheerio = $item.find('a'); + const title: string = aEl.prop('title') === undefined ? aEl.text() : aEl.prop('title'); + + const dateMatch = title.match(/(\d{4}-\d{2}-\d{2})$/); + let pubDate; + let cleanTitle = title; + + if (dateMatch) { + pubDate = parseDate(dateMatch[0]); + cleanTitle = title.replace(/\d{4}-\d{2}-\d{2}$/, '').trim(); + } else { + const spanText = $item.find('span').text().trim(); + pubDate = spanText ? parseDate(spanText) : undefined; + } + + const description: string = art(path.join(__dirname, 'templates/description.art'), { + intro: $item.find('p.zy').text(), + }); + + const imageSrc: string | undefined = $item.find('img').prop('src'); + const imageType: string | undefined = imageSrc?.split(/\./).pop(); + const image: string | undefined = imageSrc ? new URL(imageSrc, targetUrl).href : undefined; + const media: Record> = {}; + + if (imageType && image) { + media[imageType] = { url: image }; + } + + return { + title: cleanTitle, + description, + pubDate, + link: new URL(aEl.prop('href') as string, targetUrl).href, + content: { + html: description, + text: $item.find('p.zy').text(), + }, + image, + banner: image, + language, + media: Object.keys(media).length > 0 ? media : undefined, + }; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link && typeof item.link !== 'string') { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const detailTitle: string = $$('h5').text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.TRS_Editor').html(), + }); + + const detailDate = $$('span.fb em').text().trim(); + const pubDate = detailDate ? parseDate(detailDate) : item.pubDate; + + return { + title: detailTitle || item.title, // Use original title as fallback + description, + link: item.link, + pubDate, + content: { + html: description, + text: $$('div.TRS_Editor').text(), + }, + image: item.image, + banner: item.banner, + language, + media: item.media, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const author: string = $('title').text(); + const title: string = $('div.erjiCurNav').text(); + const feedImage: string = new URL($('img.logo').prop('src') as string, targetUrl).href; + + return { + title: `${author} - ${title}`, + description: title, + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/:category{.+}?', + name: '通用', + url: 'www.chinacdc.cn', + maintainers: ['nczitzk'], + handler, + example: '/chinacdc/zxyw', + parameters: { + category: '分类,默认为 `zxyw`,即中心要闻,可在对应分类页 URL 中找到,Category, `zxyw`,即中心要闻 by default', + }, + description: `::: tip +若订阅 [中心要闻](https://www.chinacdc.cn/zxyw/),网址为 \`https://www.chinacdc.cn/zxyw/\`,请截取 \`https://www.chinacdc.cn/\` 到末尾 \`/\` 的部分 \`zxyw\` 作为 \`category\` 参数填入,此时目标路由为 [\`/chinacdc/zxyw\`](https://rsshub.app/chinacdc/zxyw)。 +::: + +| [中心要闻](https://www.chinacdc.cn/zxyw/) | [通知公告](https://www.chinacdc.cn/tzgg/) | +| ----------------------------------------- | ----------------------------------------- | +| [zxyw](https://rsshub.app/chinacdc/zxyw) | [tzgg](https://rsshub.app/chinacdc/tzgg) | + +
    +更多分类 + +#### [党建园地](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) | + +
    +`, + categories: ['government'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.chinacdc.cn/:category'], + target: (params) => { + const category = params.category; + + return `/chinacdc${category ? `/${category}` : ''}`; + }, + }, + { + title: '中心要闻', + source: ['www.chinacdc.cn/zxyw/'], + target: '/zxyw', + }, + { + title: '通知公告', + source: ['www.chinacdc.cn/tzgg/'], + target: '/tzgg', + }, + { + title: '党建园地 - 廉政文化', + source: ['www.chinacdc.cn/djgz_13611/'], + target: '/dqgz/djgz_13611', + }, + { + title: '党建园地 - 党建工作', + source: ['www.chinacdc.cn/dqgz/'], + target: '/dqgz/djgz', + }, + { + title: '党建园地 - 廉政文化', + source: ['www.chinacdc.cn/djgz_13611/'], + target: '/dqgz/djgz_13611', + }, + { + title: '党建园地 - 工会工作', + source: ['www.chinacdc.cn/ghgz/'], + target: '/dqgz/ghgz', + }, + { + title: '党建园地 - 团青工作', + source: ['www.chinacdc.cn/tqgz/'], + target: '/dqgz/tqgz', + }, + { + title: '党建园地 - 理论学习', + source: ['www.chinacdc.cn/tqgz_13618/'], + target: '/dqgz/tqgz_13618', + }, + { + title: '疾控应急 - 传染病', + source: ['www.chinacdc.cn/jkyj/crb2/'], + target: '/jkyj/crb2', + }, + { + title: '疾控应急 - 突发公共卫生事件', + source: ['www.chinacdc.cn/jkyj/tfggws/'], + target: '/jkyj/tfggws', + }, + { + title: '疾控应急 - 慢性病与伤害防控', + source: ['www.chinacdc.cn/jkyj/mxfcrxjb2/'], + target: '/jkyj/mxfcrxjb2', + }, + { + title: '疾控应急 - 烟草控制', + source: ['www.chinacdc.cn/jkyj/yckz/'], + target: '/jkyj/yckz', + }, + { + title: '疾控应急 - 营养与健康', + source: ['www.chinacdc.cn/jkyj/yyyjk2/'], + target: '/jkyj/yyyjk2', + }, + { + title: '疾控应急 - 环境与健康', + source: ['www.chinacdc.cn/jkyj/hjyjk/'], + target: '/jkyj/hjyjk', + }, + { + title: '疾控应急 - 职业卫生与中毒控制', + source: ['www.chinacdc.cn/jkyj/hjwsyzdkz/'], + target: '/jkyj/hjwsyzdkz', + }, + { + title: '疾控应急 - 放射卫生', + source: ['www.chinacdc.cn/jkyj/fsws/'], + target: '/jkyj/fsws', + }, + { + title: '疾控应急 - 免疫规划', + source: ['www.chinacdc.cn/jkyj/mygh02/'], + target: '/jkyj/mygh02', + }, + { + title: '疾控应急 - 结核病防控', + source: ['www.chinacdc.cn/jkyj/jhbfk/'], + target: '/jkyj/jhbfk', + }, + { + title: '疾控应急 - 寄生虫病', + source: ['www.chinacdc.cn/jkyj/jscb/'], + target: '/jkyj/jscb', + }, + { + title: '科学研究 - 科技进展', + source: ['www.chinacdc.cn/kxyj/kjjz/'], + target: '/kxyj/kjjz', + }, + { + title: '科学研究 - 学术动态', + source: ['www.chinacdc.cn/kxyj/xsdt/'], + target: '/kxyj/xsdt', + }, + { + title: '科学研究 - 科研平台', + source: ['www.chinacdc.cn/kxyj/xsjl/'], + target: '/kxyj/xsjl', + }, + { + title: '科学研究 - 科研亮点', + source: ['www.chinacdc.cn/kxyj/kyld/'], + target: '/kxyj/kyld', + }, + { + title: '科学研究 - 科技政策', + source: ['www.chinacdc.cn/kxyj/kjzc/'], + target: '/kxyj/kjzc', + }, + { + title: '教育培训 - 研究生院', + source: ['www.chinacdc.cn/jypx/yjsy/'], + target: '/jypx/yjsy', + }, + { + title: '教育培训 - 继续教育', + source: ['www.chinacdc.cn/jypx/jxjy/'], + target: '/jypx/jxjy', + }, + { + title: '教育培训 - 博士后', + source: ['www.chinacdc.cn/jypx/bsh/'], + target: '/jypx/bsh', + }, + { + title: '教育培训 - 中国现场流行病学培训项目(CFETP)', + source: ['www.chinacdc.cn/jypx/CFETP/'], + target: '/jypx/CFETP', + }, + { + title: '全球公卫 - 合作伙伴', + source: ['www.chinacdc.cn/qqgw/hzhb/'], + target: '/qqgw/hzhb', + }, + { + title: '全球公卫 - 世界卫生组织合作中心和参比实验室', + source: ['www.chinacdc.cn/qqgw/wszz/'], + target: '/qqgw/wszz', + }, + { + title: '全球公卫 - 国际交流 (港澳台交流)', + source: ['www.chinacdc.cn/qqgw/gjjl/'], + target: '/qqgw/gjjl', + }, + { + title: '全球公卫 - 公共卫生援外与合作', + source: ['www.chinacdc.cn/qqgw/ggws/'], + target: '/qqgw/ggws', + }, + { + title: '人才建设 - 院士风采', + source: ['www.chinacdc.cn/rcjs/ysfc/'], + target: '/rcjs/ysfc', + }, + { + title: '人才建设 - 首席专家', + source: ['www.chinacdc.cn/rcjs/sxzj/'], + target: '/rcjs/sxzj', + }, + { + title: '人才建设 - 人才队伍', + source: ['www.chinacdc.cn/rcjs/rcdw/'], + target: '/rcjs/rcdw', + }, + { + title: '人才建设 - 人才招聘', + source: ['www.chinacdc.cn/rcjs/rczp/'], + target: '/rcjs/rczp', + }, + { + title: '健康数据 - 全国法定传染病疫情情况', + source: ['www.chinacdc.cn/jksj/jksj01/'], + target: '/jksj/jksj01', + }, + { + title: '健康数据 - 全国新型冠状病毒感染疫情情况', + source: ['www.chinacdc.cn/jksj/xgbdyq/'], + target: '/jksj/xgbdyq', + }, + { + title: '健康数据 - 重点传染病和突发公共卫生事件风险评估报告', + source: ['www.chinacdc.cn/jksj/jksj02/'], + target: '/jksj/jksj02', + }, + { + title: '健康数据 - 全球传染病事件风险评估报告', + source: ['www.chinacdc.cn/jksj/jksj03/'], + target: '/jksj/jksj03', + }, + { + title: '健康数据 - 全国预防接种异常反应监测信息概况', + source: ['www.chinacdc.cn/jksj/jksj04_14209/'], + target: '/jksj/jksj04_14209', + }, + { + title: '健康数据 - 流感监测周报', + source: ['www.chinacdc.cn/jksj/jksj04_14249/'], + target: '/jksj/jksj04_14249', + }, + { + title: '健康数据 - 全国急性呼吸道传染病哨点监测情况', + source: ['www.chinacdc.cn/jksj/jksj04_14275/'], + target: '/jksj/jksj04_14275', + }, + { + title: '健康数据 - 健康报告', + source: ['www.chinacdc.cn/jksj/jksj04/'], + target: '/jksj/jksj04', + }, + { + title: '健康科普 - 传染病', + source: ['www.chinacdc.cn/jkkp/crb/'], + target: '/jkkp/crb', + }, + { + title: '健康科普 - 慢性非传染性疾病', + source: ['www.chinacdc.cn/jkkp/mxfcrb/'], + target: '/jkkp/mxfcrb', + }, + { + title: '健康科普 - 免疫规划', + source: ['www.chinacdc.cn/jkkp/mygh/'], + target: '/jkkp/mygh', + }, + { + title: '健康科普 - 公共卫生事件', + source: ['www.chinacdc.cn/jkkp/ggws/'], + target: '/jkkp/ggws', + }, + { + title: '健康科普 - 烟草控制', + source: ['www.chinacdc.cn/jkkp/yckz/'], + target: '/jkkp/yckz', + }, + { + title: '健康科普 - 营养与健康', + source: ['www.chinacdc.cn/jkkp/yyjk/'], + target: '/jkkp/yyjk', + }, + { + title: '健康科普 - 环境健康', + source: ['www.chinacdc.cn/jkkp/hjjk/'], + target: '/jkkp/hjjk', + }, + { + title: '健康科普 - 职业健康与中毒控制', + source: ['www.chinacdc.cn/jkkp/zyjk/'], + target: '/jkkp/zyjk', + }, + { + title: '健康科普 - 放射卫生', + source: ['www.chinacdc.cn/jkkp/fsws/'], + target: '/jkkp/fsws', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/chinacdc/namespace.ts b/lib/routes/chinacdc/namespace.ts new file mode 100644 index 00000000000000..5708fce38f3ee7 --- /dev/null +++ b/lib/routes/chinacdc/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国疾病预防控制中心', + url: 'www.chinacdc.cn', + categories: ['government'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/chinacdc/templates/description.art b/lib/routes/chinacdc/templates/description.art new file mode 100644 index 00000000000000..57498ab45a9d86 --- /dev/null +++ b/lib/routes/chinacdc/templates/description.art @@ -0,0 +1,7 @@ +{{ if intro }} +
    {{ 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 | -
    `, + 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 | +
    `, }; async function handler(ctx) { @@ -170,10 +170,15 @@ async function handler(ctx) { const apiArticleUrl = new URL('gxportal/xfpt/portal/viewArticleById', rootUrl).href; const currentUrl = new URL(`gxportal/xfgl/portal/list.html?columnId=${id}`, rootUrl).href; - const { data: response } = await got.post(apiUrl, { - form: { - params: encodeURI(`{"columnId":"${id}"}`), + const response = await ofetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', }, + body: new URLSearchParams({ + params: encodeURI(`{"columnId":"${id}"}`), + }), + parseResponse: JSON.parse, }); let $ = load(response.articleListHtml); @@ -195,10 +200,15 @@ async function handler(ctx) { items = await Promise.all( items.map((item) => cache.tryGet(item.link, async () => { - const { data: detailResponse } = await got.post(apiArticleUrl, { - form: { - params: encodeURI(`{"articleId":"${item.guid}","columnId":"${id}"}`), + const detailResponse = await ofetch(apiArticleUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', }, + body: new URLSearchParams({ + params: encodeURI(`{"articleId":"${item.guid}","columnId":"${id}"}`), + }), + parseResponse: JSON.parse, }); const articleContent = detailResponse.article_content; @@ -220,7 +230,7 @@ async function handler(ctx) { const subtitle = $('div.head-tit').text(); - const { data: currentResponse } = await got(currentUrl); + const currentResponse = await ofetch(currentUrl); $ = load(currentResponse); diff --git a/lib/routes/chinaisa/namespace.ts b/lib/routes/chinaisa/namespace.ts index 2db657594928e6..88e6053481e53f 100644 --- a/lib/routes/chinaisa/namespace.ts +++ b/lib/routes/chinaisa/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国钢铁工业协会', url: 'chinaisa.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/chinamoney/namespace.ts b/lib/routes/chinamoney/namespace.ts index 5b81c2db6c88c2..be76b9182a3dfd 100644 --- a/lib/routes/chinamoney/namespace.ts +++ b/lib/routes/chinamoney/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国货币网', url: 'chinamoney.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/chinamoney/notice.ts b/lib/routes/chinamoney/notice.ts index bd088724caa3de..8fb5c22eee1d09 100644 --- a/lib/routes/chinamoney/notice.ts +++ b/lib/routes/chinamoney/notice.ts @@ -23,36 +23,36 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `
    - 市场公告 +市场公告 外汇市场公告 - | 最新 | 市场公告通知 | 中心会员公告 | 会员信息公告 | - | ---- | ------------ | ------------ | ------------ | - | 2834 | 2835 | 2836 | 2837 | +| 最新 | 市场公告通知 | 中心会员公告 | 会员信息公告 | +| ---- | ------------ | ------------ | ------------ | +| 2834 | 2835 | 2836 | 2837 | 本币市场公告 - | 最新 | 市场公告通知 | 中心会员公告 | 会员信息公告 | - | -------------- | ------------ | ------------ | ------------ | - | 2839,2840,2841 | 2839 | 2840 | 2841 | +| 最新 | 市场公告通知 | 中心会员公告 | 会员信息公告 | +| -------------- | ------------ | ------------ | ------------ | +| 2839,2840,2841 | 2839 | 2840 | 2841 | 央行业务公告 - | 最新 | 公开市场操作 | 中央国库现金管理 | - | --------- | ------------ | ---------------- | - | 2845,2846 | 2845 | 2846 | -
    +| 最新 | 公开市场操作 | 中央国库现金管理 | +| --------- | ------------ | ---------------- | +| 2845,2846 | 2845 | 2846 | + -
    - 本币市场 +
    +本币市场 贷款市场报价利率 - | LPR 市场公告 | - | ------------ | - | 3686 | -
    `, +| LPR 市场公告 | +| ------------ | +| 3686 | +
    `, }; async function handler(ctx) { diff --git a/lib/routes/chinanews/index.ts b/lib/routes/chinanews/index.ts index c9f2e470bce571..3a66077f252f08 100644 --- a/lib/routes/chinanews/index.ts +++ b/lib/routes/chinanews/index.ts @@ -34,7 +34,7 @@ async function handler(ctx) { title: $(item).text(), })) .get() - .slice(0, ctx.req.query('limit') ? (Number.parseInt(ctx.req.query('limit')) > 125 ? 125 : Number.parseInt(ctx.req.query('limit'))) : 50); + .slice(0, ctx.req.query('limit') ? Math.min(Number.parseInt(ctx.req.query('limit')), 125) : 50); const items = await Promise.all( list.map((item) => diff --git a/lib/routes/chinanews/namespace.ts b/lib/routes/chinanews/namespace.ts index bb524d2573b035..0bf769dc34e399 100644 --- a/lib/routes/chinanews/namespace.ts +++ b/lib/routes/chinanews/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国新闻网', url: 'chinanews.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/chinania/index.ts b/lib/routes/chinania/index.ts new file mode 100644 index 00000000000000..b49960a7b84fb1 --- /dev/null +++ b/lib/routes/chinania/index.ts @@ -0,0 +1,240 @@ +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 = 'xiehuidongtai/xiehuitongzhi' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 25; + + const rootUrl = 'https://www.chinania.org.cn'; + const currentUrl = new URL(`html/${category.endsWith('/') ? category : `${category}/`}`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('ul.notice_list_ul li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + return { + title: item.find('p').first().text(), + pubDate: parseDate(item.find('p').last().text()), + link: item.find('a').prop('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.article_title p').first().text(); + const description = $$('div.article_content').html(); + + item.title = title; + item.description = description; + item.pubDate = parseDate($$('div.article_title p').last().text().split(':')); + item.author = $$("meta[name='keywords']").prop('content'); + item.content = { + html: description, + text: $$('div.article_content').text(), + }; + item.language = language; + + return item; + }) + ) + ); + + const title = $('title').text(); + const image = new URL($('img.logo').prop('src'), rootUrl).href; + + return { + title, + description: title.split(/-/)[0]?.trim(), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $("meta[name='keywords']").prop('content'), + language, + }; +}; + +export const route: Route = { + path: '/:category{.+}?', + name: '分类', + url: 'www.chinania.org.cn', + maintainers: ['nczitzk'], + handler, + example: '/chinania/xiehuidongtai/xiehuitongzhi', + parameters: { category: '分类,默认为 `xiehuidongtai/xiehuitongzhi`,即协会通知,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [协会通知](https://www.chinania.org.cn/html/xiehuidongtai/xiehuitongzhi/),网址为 \`https://www.chinania.org.cn/html/xiehuidongtai/xiehuitongzhi/\`。截取 \`https://www.chinania.org.cn/html\` 到末尾 \`/\` 的部分 \`xiehuidongtai/xiehuitongzhi\` 作为参数填入,此时路由为 [\`/chinania/xiehuidongtai/xiehuitongzhi\`](https://rsshub.app/chinania/xiehuidongtai/xiehuitongzhi)。 +::: + +
    +更多分类 + +#### [协会动态](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) | + +
    + `, + categories: ['new-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.chinania.org.cn/html/:category'], + target: (params) => { + const category = params.category; + + return category ? `/${category}` : ''; + }, + }, + { + title: '协会动态 - 协会动态', + source: ['www.chinania.org.cn/html/xiehuidongtai/xiehuidongtai/'], + target: '/xiehuidongtai/xiehuidongtai', + }, + { + title: '协会动态 - 协会通知', + source: ['www.chinania.org.cn/html/xiehuidongtai/xiehuitongzhi/'], + target: '/xiehuidongtai/xiehuitongzhi', + }, + { + title: '协会动态 - 有色企业50强', + source: ['www.chinania.org.cn/html/xiehuidongtai/youseqiye50qiang/'], + target: '/xiehuidongtai/youseqiye50qiang', + }, + { + title: '党建工作 - 协会党建', + source: ['www.chinania.org.cn/html/djgz/xiehuidangjian/'], + target: '/djgz/xiehuidangjian', + }, + { + title: '党建工作 - 行业党建', + source: ['www.chinania.org.cn/html/djgz/hangyedangjian/'], + target: '/djgz/hangyedangjian', + }, + { + title: '会议展览 - 会展通知', + source: ['www.chinania.org.cn/html/hyzl/huiyizhanlan/'], + target: '/hyzl/huiyizhanlan', + }, + { + title: '会议展览 - 会展报道', + source: ['www.chinania.org.cn/html/hyzl/huizhanbaodao/'], + target: '/hyzl/huizhanbaodao', + }, + { + title: '行业新闻 - 时政要闻', + source: ['www.chinania.org.cn/html/hangyexinwen/shizhengyaowen/'], + target: '/hangyexinwen/shizhengyaowen', + }, + { + title: '行业新闻 - 要闻', + source: ['www.chinania.org.cn/html/hangyexinwen/yaowen/'], + target: '/hangyexinwen/yaowen', + }, + { + title: '行业新闻 - 行业新闻', + source: ['www.chinania.org.cn/html/hangyexinwen/guoneixinwen/'], + target: '/hangyexinwen/guoneixinwen', + }, + { + title: '行业新闻 - 资讯', + source: ['www.chinania.org.cn/html/hangyexinwen/zixun/'], + target: '/hangyexinwen/zixun', + }, + { + title: '行业统计 - 行业分析', + source: ['www.chinania.org.cn/html/hangyetongji/tongji/'], + target: '/hangyetongji/tongji', + }, + { + title: '行业统计 - 数据统计', + source: ['www.chinania.org.cn/html/hangyetongji/chanyeshuju/'], + target: '/hangyetongji/chanyeshuju', + }, + { + title: '行业统计 - 景气指数', + source: ['www.chinania.org.cn/html/hangyetongji/jqzs/'], + target: '/hangyetongji/jqzs', + }, + { + title: '人力资源 - 相关通知', + source: ['www.chinania.org.cn/html/renliziyuan/xiangguantongzhi/'], + target: '/renliziyuan/xiangguantongzhi', + }, + { + title: '人力资源 - 人事招聘', + source: ['www.chinania.org.cn/html/renliziyuan/renshizhaopin/'], + target: '/renliziyuan/renshizhaopin', + }, + { + title: '政策法规 - 政策法规', + source: ['www.chinania.org.cn/html/zcfg/zhengcefagui/'], + target: '/zcfg/zhengcefagui', + }, + ], +}; diff --git a/lib/routes/chinania/namespace.ts b/lib/routes/chinania/namespace.ts new file mode 100644 index 00000000000000..cb413dfefe9f8b --- /dev/null +++ b/lib/routes/chinania/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国有色金属工业网', + url: 'chinania.org.cn', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/chinaratings/credit-research.ts b/lib/routes/chinaratings/credit-research.ts new file mode 100644 index 00000000000000..7aa14528b74a4f --- /dev/null +++ b/lib/routes/chinaratings/credit-research.ts @@ -0,0 +1,148 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +import cache from '@/utils/cache'; +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'; + +export const handler = async (ctx: Context): Promise => { + const { category = 'Industry/Comment' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '15', 10); + + const baseUrl: string = 'https://www.chinaratings.com.cn'; + const targetUrl: string = new URL(`CreditResearch/${category.endsWith('/') ? category : `${category}/`}`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = 'zh-CN'; + + let items: DataItem[] = []; + + items = $('div.contRight ul.list li') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio = $(el); + const $aEl: Cheerio = $el.find('a'); + + const title: string = $aEl.text(); + const pubDateStr: string | undefined = $el.find('span').text(); + const linkUrl: string | undefined = $aEl.attr('href'); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl ? new URL(linkUrl, targetUrl).href : undefined, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.newshead h2, div.title h3').text(); + const description: string = $$('div.news div.content').html() ?? ''; + + const metaStr: string = $$('div.newshead p span, div.title p span').text(); + const pubDateStr: string | undefined = metaStr?.match(/(\d{4}-\d{2}-\d{2})/)?.[1]; + const authors: DataItem['author'] = metaStr?.match(/来源:(.*?)/)?.[1]; + const upDatedStr: string | undefined = pubDateStr; + + let processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + author: authors, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + const docUrl: string | undefined = detailResponse.match(/(\/upload\/docs\/\d{4}-\d{2}-\d{2}\/doc_\d+)"/)?.[1]; + const enclosureUrl: string | undefined = docUrl ? `${new URL(docUrl, baseUrl).href}.pdf` : undefined; + + if (enclosureUrl) { + processedItem = { + ...processedItem, + enclosure_url: enclosureUrl, + enclosure_type: 'application/pdf', + enclosure_title: title, + }; + } + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const title: string = $('title').text(); + + return { + title, + link: targetUrl, + item: items, + allowEmpty: true, + image: $('a.logo_c').attr('href') ? new URL($('a.logo_c').attr('href') as string, targetUrl).href : undefined, + author: title.split(/-/).pop(), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/CreditResearch/:category{.+}?', + name: '中债研究', + url: 'www.chinaratings.com.cn', + maintainers: ['nczitzk'], + handler, + example: '/chinaratings/CreditResearch', + parameters: { + category: '分类,默认为 `Industry/Comment`,即行业评论,可在对应分类页 URL 中找到', + }, + description: `:::tip +若订阅 [行业评论](https://www.chinaratings.com.cn/CreditResearch/Industry/Comment/),网址为 \`https://www.chinaratings.com.cn/CreditResearch/Industry/Comment/\`,请截取 \`https://www.chinaratings.com.cn/CreditResearch/\` 到末尾 \`/\` 的部分 \`Industry/Comment\` 作为 \`category\` 参数填入,此时目标路由为 [\`/chinaratings/CreditResearch/Industry/Comment\`](https://rsshub.app/chinaratings/CreditResearch/Industry/Comment)。 +::: +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.chinaratings.com.cn/CreditResearch/:category'], + target: (params) => { + const category: string = params.category; + + return `/chinaratings/CreditResearch${category ? `/${category}` : ''}`; + }, + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/chinaratings/namespace.ts b/lib/routes/chinaratings/namespace.ts new file mode 100644 index 00000000000000..68d8755a93c1e0 --- /dev/null +++ b/lib/routes/chinaratings/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中债资信评估有限责任公司', + url: 'chinaratings.com.cn', + categories: ['finance'], + description: '', +}; diff --git a/lib/routes/chinathinktanks/namespace.ts b/lib/routes/chinathinktanks/namespace.ts index cae24032c2d322..7d2d62e0597a2b 100644 --- a/lib/routes/chinathinktanks/namespace.ts +++ b/lib/routes/chinathinktanks/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国智库网', url: 'www.chinathinktanks.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/chinathinktanks/viewpoint.ts b/lib/routes/chinathinktanks/viewpoint.ts index d539702a049680..9cdd0e718ed243 100644 --- a/lib/routes/chinathinktanks/viewpoint.ts +++ b/lib/routes/chinathinktanks/viewpoint.ts @@ -26,52 +26,52 @@ export const route: Route = { maintainers: ['Aeliu'], handler, description: `| \`:id\` | 专题名称 | - | ----- | -------- | - | 2 | 党的建设 | - | 3 | 社会 | - | 4 | 生态 | - | 5 | 政治 | - | 6 | 经济 | - | 7 | 文化 | - | 9 | 热点专题 | - | 10 | 国际关系 | - | 13 | 国外智库 | - | 46 | 智库报告 | - | 57 | 智库要闻 | - | 126 | 世界经济 | - | 127 | 宏观经济 | - | 128 | 区域经济 | - | 129 | 产业企业 | - | 130 | 三农问题 | - | 131 | 财政金融 | - | 132 | 科技创新 | - | 133 | 民主 | - | 134 | 法治 | - | 135 | 行政 | - | 136 | 国家治理 | - | 137 | 社会事业 | - | 138 | 社会保障 | - | 139 | 民族宗教 | - | 140 | 人口就业 | - | 141 | 社会治理 | - | 142 | 文化产业 | - | 143 | 公共文化 | - | 144 | 文化体制 | - | 145 | 文化思想 | - | 146 | 资源 | - | 147 | 能源 | - | 148 | 环境 | - | 149 | 生态文明 | - | 150 | 思想建设 | - | 151 | 作风建设 | - | 152 | 组织建设 | - | 153 | 制度建设 | - | 154 | 反腐倡廉 | - | 155 | 中国外交 | - | 156 | 全球治理 | - | 157 | 大国关系 | - | 158 | 地区政治 | - | 181 | 执政能力 |`, +| ----- | -------- | +| 2 | 党的建设 | +| 3 | 社会 | +| 4 | 生态 | +| 5 | 政治 | +| 6 | 经济 | +| 7 | 文化 | +| 9 | 热点专题 | +| 10 | 国际关系 | +| 13 | 国外智库 | +| 46 | 智库报告 | +| 57 | 智库要闻 | +| 126 | 世界经济 | +| 127 | 宏观经济 | +| 128 | 区域经济 | +| 129 | 产业企业 | +| 130 | 三农问题 | +| 131 | 财政金融 | +| 132 | 科技创新 | +| 133 | 民主 | +| 134 | 法治 | +| 135 | 行政 | +| 136 | 国家治理 | +| 137 | 社会事业 | +| 138 | 社会保障 | +| 139 | 民族宗教 | +| 140 | 人口就业 | +| 141 | 社会治理 | +| 142 | 文化产业 | +| 143 | 公共文化 | +| 144 | 文化体制 | +| 145 | 文化思想 | +| 146 | 资源 | +| 147 | 能源 | +| 148 | 环境 | +| 149 | 生态文明 | +| 150 | 思想建设 | +| 151 | 作风建设 | +| 152 | 组织建设 | +| 153 | 制度建设 | +| 154 | 反腐倡廉 | +| 155 | 中国外交 | +| 156 | 全球治理 | +| 157 | 大国关系 | +| 158 | 地区政治 | +| 181 | 执政能力 |`, }; async function handler(ctx) { diff --git a/lib/routes/chinaventure/index.ts b/lib/routes/chinaventure/index.ts index fa99ff0e4541bc..5503dd54aeca7d 100644 --- a/lib/routes/chinaventure/index.ts +++ b/lib/routes/chinaventure/index.ts @@ -43,8 +43,8 @@ export const route: Route = { handler, url: 'chinaventure.com.cn/', description: `| 推荐 | 商业深度 | 资本市场 | 5G | 健康 | 教育 | 地产 | 金融 | 硬科技 | 新消费 | - | ---- | -------- | -------- | -- | ---- | ---- | ---- | ---- | ------ | ------ | - | | 78 | 80 | 83 | 111 | 110 | 112 | 113 | 114 | 116 |`, +| ---- | -------- | -------- | -- | ---- | ---- | ---- | ---- | ------ | ------ | +| | 78 | 80 | 83 | 111 | 110 | 112 | 113 | 114 | 116 |`, }; async function handler(ctx) { @@ -64,7 +64,7 @@ async function handler(ctx) { link: rootUrl + $(item).attr('href'), })) .get() - .slice(0, ctx.req.query('limit') ? (Number.parseInt(ctx.req.query('limit')) > 20 ? 20 : Number.parseInt(ctx.req.query('limit'))) : 20); + .slice(0, ctx.req.query('limit') ? Math.min(Number.parseInt(ctx.req.query('limit')), 20) : 20); const items = await Promise.all( list.map((item) => diff --git a/lib/routes/chinaventure/namespace.ts b/lib/routes/chinaventure/namespace.ts index f133780a9adaa2..7ff31daa6dd80a 100644 --- a/lib/routes/chinaventure/namespace.ts +++ b/lib/routes/chinaventure/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '投中网', url: 'chinaventure.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/chinawriter/namespace.ts b/lib/routes/chinawriter/namespace.ts index 0f473e04db8e9d..d5d4237567a4ac 100644 --- a/lib/routes/chinawriter/namespace.ts +++ b/lib/routes/chinawriter/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国作家网', url: 'chinawriter.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/chlinlearn/daily-blog.ts b/lib/routes/chlinlearn/daily-blog.ts new file mode 100644 index 00000000000000..36a03081de4fcf --- /dev/null +++ b/lib/routes/chlinlearn/daily-blog.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 +import { parseDate } from '@/utils/parse-date'; // 解析日期的工具函数 +import timezone from '@/utils/timezone'; +import CryptoJS from 'crypto-js'; + +export const route: Route = { + path: '/daily-blog', + name: '值得一读技术博客', + maintainers: ['huyyi'], + categories: ['programming'], + example: '/chlinlearn/daily-blog', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['daily-blog.chlinlearn.top/blogs/*'], + target: '/chlinlearn/daily-blog', + }, + ], + handler: async () => { + const r = CryptoJS.lib.WordArray.random(8).toString(CryptoJS.enc.Hex); + const n = Date.now(); + const o = CryptoJS.SHA256('pHVp671B0tLkW40KCwyPrb6W1GEMEGyT' + r + n).toString(CryptoJS.enc.Hex); + const data = await ofetch('https://daily-blog.chlinlearn.top/api/daily-blog/getBlogs/new?type=new&pageNum=1&pageSize=20', { + headers: { + Referer: 'https://daily-blog.chlinlearn.top/blogs/1', + 'x-req-nonce': r, + 'x-req-timestamp': n, + 'x-req-key': o, + }, + }); + const items = data.rows.map((item) => ({ + title: item.title, + link: item.url, + author: item.author, + img: item.icon, + pubDate: timezone(parseDate(item.publishTime), +8), + })); + return { + // 源标题 + title: '值得一读技术博客', + // 源链接 + link: 'https://daily-blog.chlinlearn.top/blogs/1', + // 源文章 + item: items, + }; + }, +}; diff --git a/lib/routes/chlinlearn/namespcae.ts b/lib/routes/chlinlearn/namespcae.ts new file mode 100644 index 00000000000000..ad9bf5e74179f8 --- /dev/null +++ b/lib/routes/chlinlearn/namespcae.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'chlinlearn 的技术博客', + url: 'daily-blog.chlinlearn.top', + lang: 'zh-CN', +}; diff --git a/lib/routes/chongdiantou/index.ts b/lib/routes/chongdiantou/index.ts new file mode 100644 index 00000000000000..d798231e9a8b49 --- /dev/null +++ b/lib/routes/chongdiantou/index.ts @@ -0,0 +1,57 @@ +import { Route } from '@/types'; +import { namespace } from './namespace'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/', + categories: namespace.categories, + example: '/chongdiantou', + radar: [ + { + source: ['www.chongdiantou.com'], + }, + ], + name: '最新资讯', + maintainers: ['Geraldxm'], + handler, + url: 'www.chongdiantou.com', +}; + +async function handler() { + const response = await ofetch('https://www.chongdiantou.com/nice-json/front-end/home-load-more'); + let items = []; + + items = response.data.map((item) => ({ + title: item.title, + link: item.link, + image: item.cover, + pubDate: new Date(item.time), + category: item.cat.name, + })); + + items = await Promise.all( + items.map( + async (item) => + await cache.tryGet(item.link, async () => { + try { + const response = await ofetch(item.link); + const $ = load(response); + item.description = $('.post-content').html() || 'No content found'; + } catch { + item.description = 'Failed to fetch content'; + } + return item; + }) + ) + ); + + return { + title: '充电头网 - 最新资讯', + description: '充电头网新闻资讯', + link: 'https://www.chongdiantou.com', + image: 'https://static.chongdiantou.com/wp-content/uploads/2021/02/2021021806172389.png', + item: items, + }; +} diff --git a/lib/routes/chongdiantou/namespace.ts b/lib/routes/chongdiantou/namespace.ts new file mode 100644 index 00000000000000..b67d38e1b65017 --- /dev/null +++ b/lib/routes/chongdiantou/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '充电头网', + url: 'www.chongdiantou.com', + categories: ['new-media'], + lang: 'zh-CN', + description: '充电头网是国内最早进行消费类电源技术及其周边配件(快充、充电头、充电器、无线充、车充、车载充电器、数据线、充电线材、移动电源及电芯、USB插排)评测、拆解的专业机构。', +}; diff --git a/lib/routes/chsi/kyzx.ts b/lib/routes/chsi/kyzx.ts index 393a61f154c3d2..3955db9766ceb1 100644 --- a/lib/routes/chsi/kyzx.ts +++ b/lib/routes/chsi/kyzx.ts @@ -29,12 +29,12 @@ export const route: Route = { maintainers: ['yanbot-team'], handler, description: `| \`:type\` | 专题名称 | - | ------- | -------- | - | fstj | 复试调剂 | - | kydt | 考研动态 | - | zcdh | 政策导航 | - | kyrw | 考研人物 | - | jyxd | 经验心得 |`, +| ------- | -------- | +| fstj | 复试调剂 | +| kydt | 考研动态 | +| zcdh | 政策导航 | +| kyrw | 考研人物 | +| jyxd | 经验心得 |`, }; async function handler(ctx) { diff --git a/lib/routes/chsi/namespace.ts b/lib/routes/chsi/namespace.ts index ce6dd8cc888855..4b8aa5f9388644 100644 --- a/lib/routes/chsi/namespace.ts +++ b/lib/routes/chsi/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国研究生招生信息网', url: 'yz.chsi.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/chuanliu/namespace.ts b/lib/routes/chuanliu/namespace.ts index 0d7d7f0c522de2..9403b914063918 100644 --- a/lib/routes/chuanliu/namespace.ts +++ b/lib/routes/chuanliu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '川流', url: 'chuanliu.org', + lang: 'zh-CN', }; diff --git a/lib/routes/chuapp/chuapp.ts b/lib/routes/chuapp/chuapp.ts new file mode 100644 index 00000000000000..99e277de418a3a --- /dev/null +++ b/lib/routes/chuapp/chuapp.ts @@ -0,0 +1,147 @@ +import { Context } from 'hono'; +import { load } from 'cheerio'; +import { Data, Route, DataItem } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/:category?', + categories: ['game'], + example: '/chuapp/daily', + parameters: { + category: '栏目分类,见下表', + }, + description: ` + | \`category\` | 栏目分类 | + | ------------ | ------- | + | \`daily\` | 每日聚焦 | + | \`pcz\` | 最好玩 | + | \`night\` | 触乐夜话 | + | \`news\` | 动态资讯 | + `, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '分类', + maintainers: ['dousha'], + radar: [ + { + source: ['chuapp.com/category/:category'], + target: '/:category', + }, + { + source: ['chuapp.com/tag/index/id/20369.html'], + target: '/night', + }, + ], + handler, +}; + +const baseUrl = 'https://www.chuapp.com'; +const pathLut: Record = { + daily: { + title: '每日聚焦', + suffix: '/category/daily', + }, + pcz: { + title: '最好玩', + suffix: '/category/pcz', + }, + night: { + title: '触乐夜话', + suffix: '/tag/index/id/20369.html', + }, + news: { + // route from the old implementation + title: '动态资讯', + suffix: '/category/zsyx', + }, + zsyx: { + // route for radar + title: '动态资讯', + suffix: '/category/zsyx', + }, +}; + +type InvalidArticle = { + title?: string; + link?: string; +}; + +type ValidArticle = { + title: string; + link: string; +}; + +type RawArticle = InvalidArticle | ValidArticle; + +function isValidArticle(article: RawArticle): article is ValidArticle { + return 'title' in article && 'link' in article && article.title !== null && article.link !== null; +} + +function toJavaScriptTimestamp(x: string | number | null | undefined): number { + return x ? Number(x) * 1000 : 0; +} + +async function handler(ctx: Context): Promise { + const { category = 'night' } = ctx.req.param(); + const subpath = pathLut[category]; + if (!subpath) { + return null; + } + + const targetUrl = `${baseUrl}${subpath.suffix}`; + const response = await ofetch(targetUrl); + const $ = load(response); + + const articles: RawArticle[] = $('a.fn-clear') + .toArray() + .map((element) => ({ + title: $(element).attr('title'), + link: $(element).attr('href'), + })); + + const processedItems: Promise[] = articles + .filter((article: RawArticle): article is ValidArticle => isValidArticle(article)) + .map((article: ValidArticle) => { + if (article.link.startsWith('/')) { + return article; + } + + return { + title: article.title, + link: `/${article.link}`, + }; + }) + .map((article: ValidArticle) => { + const fullArticleUrl = `${baseUrl}${article.link}`; + return cache.tryGet(fullArticleUrl, async () => { + const res = await ofetch(fullArticleUrl); + const s = load(res); + + const item: DataItem = { + title: article.title, + link: article.link, + description: s('.content .the-content').html() || '', + pubDate: parseDate(toJavaScriptTimestamp(s('.friendly_time').attr('data-time'))), + author: s('.author-time .fn-left').text() || '', + }; + + return item; + }) as Promise; + }); + + const items = await Promise.all(processedItems); + + return { + title: `触乐 - ${subpath.title}`, + link: targetUrl, + item: items, + }; +} diff --git a/lib/routes/chuapp/namespace.ts b/lib/routes/chuapp/namespace.ts new file mode 100644 index 00000000000000..b48690963c133a --- /dev/null +++ b/lib/routes/chuapp/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '触乐', + url: 'chuapp.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/chub/characters.ts b/lib/routes/chub/characters.ts index 0f34350658848c..473eb5f53282c5 100644 --- a/lib/routes/chub/characters.ts +++ b/lib/routes/chub/characters.ts @@ -4,7 +4,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/characters', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/chub/characters', name: 'Characters', maintainers: ['flameleaf'], diff --git a/lib/routes/chub/namespace.ts b/lib/routes/chub/namespace.ts index c9d38725fd3095..a86b6ea903835f 100644 --- a/lib/routes/chub/namespace.ts +++ b/lib/routes/chub/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Chub', url: 'chub.ai', + lang: 'en', }; diff --git a/lib/routes/cib/namespace.ts b/lib/routes/cib/namespace.ts index 61f44354475073..58b30f521486fa 100644 --- a/lib/routes/cib/namespace.ts +++ b/lib/routes/cib/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国兴业银行', url: 'cib.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/cib/whpj.ts b/lib/routes/cib/whpj.ts index b8dbca0b6ff5aa..240e462a707c67 100644 --- a/lib/routes/cib/whpj.ts +++ b/lib/routes/cib/whpj.ts @@ -31,8 +31,8 @@ export const route: Route = { handler, url: 'cib.com.cn/', description: `| 短格式 | 现汇买卖 | 现钞买卖 | 现汇买入 | 现汇卖出 | 现钞买入 | 现钞卖出 | - | ------ | -------- | -------- | -------- | -------- | -------- | -------- | - | short | xh | xc | xhmr | xhmc | xcmr | xcmc |`, +| ------ | -------- | -------- | -------- | -------- | -------- | -------- | +| short | xh | xc | xhmr | xhmc | xcmr | xcmc |`, }; async function handler(ctx) { diff --git a/lib/routes/ciidbnu/index.ts b/lib/routes/ciidbnu/index.ts index c669c9eead600b..9093f42fb1bd8a 100644 --- a/lib/routes/ciidbnu/index.ts +++ b/lib/routes/ciidbnu/index.ts @@ -23,8 +23,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 社会动态 | 院内新闻 | 学术观点 | 文献书籍 | 工作论文 | 专题讨论 | - | -------- | -------- | -------- | -------- | -------- | -------- | - | 1 | 5 | 3 | 4 | 6 | 8 |`, +| -------- | -------- | -------- | -------- | -------- | -------- | +| 1 | 5 | 3 | 4 | 6 | 8 |`, }; async function handler(ctx) { diff --git a/lib/routes/ciidbnu/namespace.ts b/lib/routes/ciidbnu/namespace.ts index ec7fad9cfc7b43..a9d8d6b95e859e 100644 --- a/lib/routes/ciidbnu/namespace.ts +++ b/lib/routes/ciidbnu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国收入分配研究院', url: 'ciidbnu.org', + lang: 'zh-CN', }; diff --git a/lib/routes/cisia/index.ts b/lib/routes/cisia/index.ts new file mode 100644 index 00000000000000..49d31e88cb6977 --- /dev/null +++ b/lib/routes/cisia/index.ts @@ -0,0 +1,264 @@ +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 { id = '9' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + + const domain = 'www.cisia.org'; + const rootUrl = `http://${domain}`; + const currentUrl = new URL(`site/term/${id}.html`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + let items = $('ul.list_first li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const a = item.find('a'); + + return { + title: a.text(), + pubDate: parseDate(item.find('span.time').text()), + link: new URL(a.prop('href'), rootUrl).href, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + if (!/^https?:\/\/www\.cisia\.org(\/[^\s]*)?$/.test(item.link)) { + return item; + } + + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const title = $$('div.TextTitle').text(); + const description = $$('div.NewsText').html(); + const pubDate = $$('div.shar') + .text() + .match(/(\d{4}-\d{2}-\d{2})/)?.[1]; + + item.title = title; + item.description = description; + item.pubDate = pubDate ? parseDate(pubDate) : item.pubDate; + item.author = $$('meta[name="Description"]').prop('content'); + item.content = { + html: description, + text: $$('div.NewsText').text(), + }; + + return item; + }) + ) + ); + + const image = new URL($('div.logo img').prop('src'), rootUrl).href; + + return { + title: $('title').text(), + description: $('meta[name="Description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $('meta[name="Keywords"]').prop('content'), + }; +}; + +export const route: Route = { + path: '/:id?', + name: '栏目', + url: 'www.cisia.org', + maintainers: ['nczitzk'], + handler, + example: '/cisia/9', + parameters: { id: '栏目 id,默认为 `9`,即协会动态,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [市场信息](http://www.cisia.org/site/term/12.html),网址为 \`http://www.cisia.org/site/term/12.html\`。截取 \`https://www.cisia.org/site/term/\` 到末尾 \`.html\` 的部分 \`12\` 作为参数填入,此时路由为 [\`/cisia/12\`](https://rsshub.app/cisia/12)。 +::: + +
    +更多分类 + +#### [分支机构信息](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) | + +
    + `, + categories: ['government'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.cisia.org/site/term/:id'], + target: (params) => { + const id = params.id.replace(/\.html/, ''); + + return id ? `/${id}` : ''; + }, + }, + { + title: '分支机构信息 - 企业动态', + source: ['www.cisia.org/site/term/17.html'], + target: '/17', + }, + { + title: '分支机构信息 - 产品展示', + source: ['www.cisia.org/site/term/18.html'], + target: '/18', + }, + { + title: '新闻中心 - 协会动态', + source: ['www.cisia.org/site/term/9.html'], + target: '/9', + }, + { + title: '新闻中心 - 行业新闻', + source: ['www.cisia.org/site/term/10.html'], + target: '/10', + }, + { + title: '新闻中心 - 通知公告', + source: ['www.cisia.org/site/term/11.html'], + target: '/11', + }, + { + title: '新闻中心 - 市场信息', + source: ['www.cisia.org/site/term/12.html'], + target: '/12', + }, + { + title: '政策法规 - 宏观聚焦', + source: ['www.cisia.org/site/term/20.html'], + target: '/20', + }, + { + title: '政策法规 - 技术园区', + source: ['www.cisia.org/site/term/396.html'], + target: '/396', + }, + { + title: '合作交流 - 国际交流', + source: ['www.cisia.org/site/term/23.html'], + target: '/23', + }, + { + title: '合作交流 - 行业交流', + source: ['www.cisia.org/site/term/24.html'], + target: '/24', + }, + { + title: '合作交流 - 企业调研', + source: ['www.cisia.org/site/term/25.html'], + target: '/25', + }, + { + title: '合作交流 - 会展信息', + source: ['www.cisia.org/site/term/84.html'], + target: '/84', + }, + { + title: '合作交流 - 宣传专题', + source: ['www.cisia.org/site/term/430.html'], + target: '/430', + }, + { + title: '党建工作 - 党委文件', + source: ['www.cisia.org/site/term/27.html'], + target: '/27', + }, + { + title: '党建工作 - 学习园地', + source: ['www.cisia.org/site/term/28.html'], + target: '/28', + }, + { + title: '党建工作 - 两会专题', + source: ['www.cisia.org/site/term/443.html'], + target: '/443', + }, + { + title: '网上服务平台 - 前沿科技', + source: ['www.cisia.org/site/term/31.html'], + target: '/31', + }, + { + title: '网上服务平台 - 新材料新技术', + source: ['www.cisia.org/site/term/133.html'], + target: '/133', + }, + { + title: '网上服务平台 - 文件共享', + source: ['www.cisia.org/site/term/30.html'], + target: '/30', + }, + { + title: '会员社区 - 会员分布', + source: ['www.cisia.org/site/term/35.html'], + target: '/35', + }, + { + title: '会员社区 - 会员风采', + source: ['www.cisia.org/site/term/68.html'], + target: '/68', + }, + ], +}; diff --git a/lib/routes/cisia/namespace.ts b/lib/routes/cisia/namespace.ts new file mode 100644 index 00000000000000..530fbdbb2e2cad --- /dev/null +++ b/lib/routes/cisia/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国无机盐工业协会', + url: 'www.cisia.org', + categories: ['government'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/civitai/discussions.ts b/lib/routes/civitai/discussions.ts index 5c1ced8ed4f73f..9801bab58a685e 100644 --- a/lib/routes/civitai/discussions.ts +++ b/lib/routes/civitai/discussions.ts @@ -30,7 +30,7 @@ export const route: Route = { name: 'Model discussions', maintainers: ['DIYgod'], handler, - description: `:::warning + description: `::: warning Need to configure \`CIVITAI_COOKIE\` to obtain image information of NSFW models. :::`, }; diff --git a/lib/routes/civitai/namespace.ts b/lib/routes/civitai/namespace.ts index 7c989889b50163..1f08e212946ef7 100644 --- a/lib/routes/civitai/namespace.ts +++ b/lib/routes/civitai/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Civitai', url: 'civitai.com', + lang: 'en', }; diff --git a/lib/routes/ciweimao/namespace.ts b/lib/routes/ciweimao/namespace.ts index 09b28e073b7f86..4fb85ea9a099c8 100644 --- a/lib/routes/ciweimao/namespace.ts +++ b/lib/routes/ciweimao/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '刺猬猫', url: 'wap.ciweimao.com', + lang: 'zh-CN', }; diff --git a/lib/routes/cjlu/namespace.ts b/lib/routes/cjlu/namespace.ts new file mode 100644 index 00000000000000..3a5d1362df88b3 --- /dev/null +++ b/lib/routes/cjlu/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'China Jiliang University', + url: 'www.cjlu.edu.cn', + zh: { + name: '中国计量大学', + }, + lang: 'zh-CN', +}; diff --git a/lib/routes/cjlu/yjsy/index.ts b/lib/routes/cjlu/yjsy/index.ts new file mode 100644 index 00000000000000..300e2cd5641f3a --- /dev/null +++ b/lib/routes/cjlu/yjsy/index.ts @@ -0,0 +1,107 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import timezone from '@/utils/timezone'; + +const host = 'https://yjsy.cjlu.edu.cn/'; + +const titleMap = new Map([ + ['yjstz', '中量大研究生院 —— 研究生通知'], + ['jstz', '中量大研究生院 —— 教师通知'], +]); + +export const route: Route = { + path: '/yjsy/:cate', + categories: ['university'], + example: '/cjlu/yjsy/yjstz', + parameters: { + cate: '订阅的类型,支持 yjstz(研究生通知)和 jstz(教师通知)', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + title: '研究生通知', + source: ['yjsy.cjlu.edu.cn/index/yjstz/:suffix', 'yjsy.cjlu.edu.cn/index/yjstz.htm'], + target: '/yjsy/yjstz', + }, + { + title: '教师通知', + source: ['yjsy.cjlu.edu.cn/index/jstz/:suffix', 'yjsy.cjlu.edu.cn/index/jstz.htm'], + target: '/yjsy/jstz', + }, + ], + name: '研究生院', + maintainers: ['chrisis58'], + handler, + description: `| 研究生通知 | 教师通知 | +| -------- | -------- | +| yjstz | jstz |`, +}; + +async function handler(ctx) { + const cate = ctx.req.param('cate'); + + const response = await ofetch(`${cate}.htm`, { + baseURL: `${host}/index/`, + responseType: 'text', + }); + + const $ = load(response); + + const list = $('div.grid685.right div.body ul') + .find('li') + .toArray() + .map((element) => { + const item = $(element); + + const a = item.find('a').first(); + + const timeStr = item.find('span').first().text().trim(); + const href = a.attr('href') ?? ''; + const route = href.startsWith('../') ? href.replace(/^\.\.\//, '') : href; + + return { + title: a.attr('title') ?? titleMap.get(cate), + pubDate: timezone(parseDate(timeStr, 'YYYY/MM/DD'), +8), + link: `${host}${route}`, + description: '', + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + if (!item.link || item.link === host) { + return item; + } + + const res = await ofetch(item.link, { + responseType: 'text', + }); + const $ = load(res); + + const content = $('#vsb_content').html() ?? ''; + const attachments = $('form[name="_newscontent_fromname"] div ul').html() ?? ''; + + item.description = `${content}
    ${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 }} +
    + {{ image.alt }} +
    + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} +
    {{ 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) | + +
    + `, + categories: ['new-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.cngold.org.cn/:category?'], + target: (params) => { + const category = params.category; + + return category ? `/${category}` : ''; + }, + }, + { + title: '政策法规 - 法律法规', + source: ['www.cngold.org.cn/policies-245.html'], + target: '/policies-245', + }, + { + title: '政策法规 - 产业政策', + source: ['www.cngold.org.cn/policies-262.html'], + target: '/policies-262', + }, + { + title: '政策法规 - 黄金标准', + source: ['www.cngold.org.cn/policies-281.html'], + target: '/policies-281', + }, + { + title: '行业培训 - 黄金投资分析师', + source: ['www.cngold.org.cn/training-242.html'], + target: '/training-242', + }, + { + title: '行业培训 - 教育部1+X', + source: ['www.cngold.org.cn/training-246.html'], + target: '/training-246', + }, + { + title: '行业培训 - 矿业权评估师', + source: ['www.cngold.org.cn/training-338.html'], + target: '/training-338', + }, + { + title: '行业培训 - 其他培训', + source: ['www.cngold.org.cn/training-247.html'], + target: '/training-247', + }, + { + title: '黄金科技 - 黄金协会科学技术奖', + source: ['www.cngold.org.cn/technology-318.html'], + target: '/technology-318', + }, + { + title: '黄金科技 - 科学成果评价', + source: ['www.cngold.org.cn/technology-319.html'], + target: '/technology-319', + }, + { + title: '黄金科技 - 新技术推广', + source: ['www.cngold.org.cn/technology-320.html'], + target: '/technology-320', + }, + { + title: '黄金科技 - 黄金技术大会', + source: ['www.cngold.org.cn/technology-350.html'], + target: '/technology-350', + }, + ], +}; diff --git a/lib/routes/cngold/namespace.ts b/lib/routes/cngold/namespace.ts new file mode 100644 index 00000000000000..e57011db559475 --- /dev/null +++ b/lib/routes/cngold/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国黄金协会', + url: 'cngold.org.cn', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/cnjxol/namespace.ts b/lib/routes/cnjxol/namespace.ts index 54419866214158..89cf3dc1c36cf4 100644 --- a/lib/routes/cnjxol/namespace.ts +++ b/lib/routes/cnjxol/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南湖清风', url: 'cnjxol.com', + lang: 'zh-CN', }; diff --git a/lib/routes/cnki/author.ts b/lib/routes/cnki/author.ts index 8efedd5f7e3f9b..94b6d8400ff9e7 100644 --- a/lib/routes/cnki/author.ts +++ b/lib/routes/cnki/author.ts @@ -1,16 +1,16 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; -import utils from './utils'; - -const rootUrl = 'https://kns.cnki.net'; +import { parseDate } from '@/utils/parse-date'; +import { ProcessItem } from './utils'; export const route: Route = { - path: '/author/:code', + name: '作者', + maintainers: ['Derekmini', 'harveyqiu'], categories: ['journal'], - example: '/cnki/author/000042423923', - parameters: { code: '作者对应code,可以在网址中得到' }, + path: '/author/:name/:company', + parameters: { name: '作者姓名', company: '作者单位' }, features: { requireConfig: false, requirePuppeteer: false, @@ -19,62 +19,134 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, - name: '作者期刊文献', - description: `:::tip + example: '/cnki/author/丁晓东/中国人民大学', + description: `::: tip 可能仅限中国大陆服务器访问,以实际情况为准。 - :::`, - maintainers: ['harveyqiu', 'Derekmini'], +:::`, handler, }; async function handler(ctx) { - const code = ctx.req.param('code'); + const name = ctx.req.param('name'); + const company = ctx.req.param('company'); + const host = 'https://kns.cnki.net'; + const link = `${host}/kns8s/AdvSearch?classid=WD0FTY92`; - const authorInfoUrl = `${rootUrl}/kcms/detail/knetsearch.aspx?sfield=au&code=${code}`; - const res = await got(authorInfoUrl); - const $ = load(res.data); - const authorName = $('#showname').text(); - const companyName = $('body > div.wrapper > div.main > div.container.full-screen > div > div:nth-child(3) > h3:nth-child(2) > span > a').text(); + const params = new URLSearchParams(); + params.append('boolSearch', 'true'); + params.append( + 'QueryJson', + JSON.stringify({ + Platform: '', + Resource: 'CROSSDB', + Classid: 'WD0FTY92', + Products: '', + QNode: { + QGroup: [ + { + Key: 'Subject', + Title: '', + Logic: 0, + Items: [], + ChildItems: [ + { + Key: 'input[data-tipid=gradetxt-1]', + Title: '作者', + Logic: 0, + Items: [ + { + Key: 'input[data-tipid=gradetxt-1]', + Title: '作者', + Logic: 0, + Field: 'AU', + Operator: 'DEFAULT', + Value: name, + Value2: '', + }, + ], + ChildItems: [], + }, + { + Key: 'input[data-tipid=gradetxt-2]', + Title: '作者单位', + Logic: 0, + Items: [ + { + Key: 'input[data-tipid=gradetxt-2]', + Title: '作者单位', + Logic: 0, + Field: 'AF', + Operator: 'FUZZY', + Value: company, + Value2: '', + }, + ], + ChildItems: [], + }, + ], + }, + { + Key: 'ControlGroup', + Title: '', + Logic: 0, + Items: [], + ChildItems: [], + }, + ], + }, + ExScope: '0', + SearchType: 3, + Rlang: 'CHINESE', + KuaKuCode: 'YSTT4HG0,LSTPFY1C,JUP3MUPD,MPMFIG1A,EMRPGLPA,WQ0UVIAA,BLZOG7CK,PWFIRAGL,NN3FJMUV,NLBO1Z6R', + }) + ); + params.append('pageNum', '1'); + params.append('pageSize', '20'); + params.append('sortField', 'PT'); + params.append('sortType', 'desc'); + params.append('dstyle', 'listmode'); + params.append('productStr', 'YSTT4HG0,LSTPFY1C,RMJLXHZ3,JQIRZIYA,JUP3MUPD,1UR4K4HZ,BPBAFJ5S,R79MZMCB,MPMFIG1A,EMRPGLPA,J708GVCE,ML4DRIDX,WQ0UVIAA,NB3BWEHK,XVLO76FD,HR1YT1Z9,BLZOG7CK,PWFIRAGL,NN3FJMUV,NLBO1Z6R,'); + params.append('aside', `(作者:${name}(精确))AND(作者单位:${company}(模糊))`); + params.append('searchFrom', '资源范围:总库; 时间范围:更新时间:不限;'); + params.append('CurPage', '1'); - const res2 = await got(`${rootUrl}/kns8/Detail`, { - searchParams: { - sdb: 'CAPJ', - sfield: '作者', - skey: authorName, - scode: code, - acode: code, + const response = await ofetch(`${host}/kns8s/brief/grid`, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded;charset=utf-8', + referer: `${host}/kns8s/AdvSearch?classid=WD0FTY92`, }, - followRedirect: false, + body: params.toString(), }); - const authorPageUrl = res2.headers.location; - - const regex = /v=([^&]+)/; - const code2 = authorPageUrl.match(regex)[1]; - - const url = `${rootUrl}/restapi/knowledge-api/v1/experts/relations/resources?v=${code2}&sequence=PT&size=10&sort=desc&start=1&resource=CJFD`; + const $ = load(response); + const list = $('tr') + .toArray() + .slice(1) + .map((item) => { + const title = $(item).find('a.fz14').text(); + const filename = $(item).find('a.icon-collect').attr('data-filename'); + const link = `https://cnki.net/kcms/detail/detail.aspx?filename=${filename}&dbcode=CJFD`; + const pubDate = parseDate($(item).find('td.date').text(), 'YYYY-MM-DD'); + return { + title, + link, + pubDate, + }; + }); - const res3 = await got(url, { headers: { Referer: authorPageUrl } }); - const publications = res3.data.data.data; - - const list = publications.map((publication) => { - const metadata = publication.metadata; - const { value: title = '' } = metadata.find((md) => md.name === 'TI') || {}; - const { value: date = '' } = metadata.find((md) => md.name === 'PT') || {}; - const { value: filename = '' } = metadata.find((md) => md.name === 'FN') || {}; - - return { - title, - link: `https://cnki.net/kcms/detail/detail.aspx?filename=${filename}&dbcode=CJFD`, - author: authorName, - pubDate: date, - }; - }); + const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => ProcessItem(item)))); - const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => utils.ProcessItem(item)))); + const processedItems = items + .filter((item): item is Record => item !== null && typeof item === 'object') + .map((item) => ({ + title: item.title || '', + link: item.link, + pubDate: item.pubDate, + })); return { - title: `知网 ${authorName} ${companyName}`, - link: authorInfoUrl, - item: items, + title: `知网 ${name} ${company}`, + link, + item: processedItems, }; } diff --git a/lib/routes/cnki/journals.ts b/lib/routes/cnki/journals.ts index 844baaa4a72753..e2034dfdcb49d5 100644 --- a/lib/routes/cnki/journals.ts +++ b/lib/routes/cnki/journals.ts @@ -4,6 +4,8 @@ import got from '@/utils/got'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; import { ProcessItem } from './utils'; +import parser from '@/utils/rss-parser'; +import logger from '@/utils/logger'; const rootUrl = 'https://navi.cnki.net'; @@ -26,12 +28,39 @@ export const route: Route = { }, ], name: '期刊', - maintainers: ['Fatpandac', 'Derekmini'], + maintainers: ['Fatpandac', 'Derekmini', 'pseudoyu'], handler, }; async function handler(ctx) { const name = ctx.req.param('name'); + const rssUrl = `https://rss.cnki.net/kns/rss.aspx?Journal=${name}&Virtual=knavi`; + + const rssResponse = await got.get(rssUrl); + + try { + const feed = await parser.parseString(rssResponse.data); + + if (feed.items && feed.items.length !== 0) { + const items = feed.items.map((item) => ({ + title: item.title, + description: item.content, + pubDate: parseDate(item.pubDate), + link: item.link, + author: item.author, + })); + + return { + title: feed.title, + link: feed.link, + description: feed.description, + item: items, + }; + } + } catch (error) { + logger.error(error); + } + const journalUrl = `${rootUrl}/knavi/journals/${name}/detail`; const title = await got.get(journalUrl).then((res) => load(res.data)('head > title').text()); diff --git a/lib/routes/cnki/namespace.ts b/lib/routes/cnki/namespace.ts index 290ad000ed0d1d..09d2d9db76e1f1 100644 --- a/lib/routes/cnki/namespace.ts +++ b/lib/routes/cnki/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国知网', url: 'navi.cnki.net', + lang: 'zh-CN', }; diff --git a/lib/routes/cnki/utils.ts b/lib/routes/cnki/utils.ts index 5b2e7dbe00f50b..62c6df3a9bf4df 100644 --- a/lib/routes/cnki/utils.ts +++ b/lib/routes/cnki/utils.ts @@ -11,12 +11,12 @@ const ProcessItem = async (item) => { const $ = load(detailResponse.data); item.description = art(path.join(__dirname, 'templates/desc.art'), { author: $('h3.author > span') - .map((_, item) => $(item).text()) - .get() + .toArray() + .map((item) => $(item).text()) .join(' '), company: $('a.author') - .map((_, item) => $(item).text()) - .get() + .toArray() + .map((item) => $(item).text()) .join(' '), content: $('div.row > span.abstract-text').parent().text(), }); @@ -24,4 +24,4 @@ const ProcessItem = async (item) => { return item; }; -export default { ProcessItem }; +export { ProcessItem }; diff --git a/lib/routes/cnljxh/namespace.ts b/lib/routes/cnljxh/namespace.ts index 74ef7189f0afef..e95d6cf25747f8 100644 --- a/lib/routes/cnljxh/namespace.ts +++ b/lib/routes/cnljxh/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国炼焦行业协会', url: 'cnljxh.com', + lang: 'zh-CN', }; diff --git a/lib/routes/cntheory/namespace.ts b/lib/routes/cntheory/namespace.ts index 45fb3aff48a326..8c0f5aee607326 100644 --- a/lib/routes/cntheory/namespace.ts +++ b/lib/routes/cntheory/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '理论网', url: 'paper.cntheory.com', + lang: 'zh-CN', }; diff --git a/lib/routes/cntv/column.ts b/lib/routes/cntv/column.ts index cf6da8dabb132a..dcc4002b16cfb3 100644 --- a/lib/routes/cntv/column.ts +++ b/lib/routes/cntv/column.ts @@ -29,16 +29,16 @@ export const route: Route = { maintainers: ['WhoIsSure', 'Fatpandac'], handler, url: 'navi.cctv.com/', - description: `:::tip + description: `::: tip 栏目 ID 查找示例: 打开栏目具体某一期页面,F12 控制台输入\`column_id\`得到栏目 ID。 ::: 栏目 - | 新闻联播 | 新闻周刊 | 天下足球 | - | -------------------- | -------------------- | -------------------- | - | TOPC1451528971114112 | TOPC1451559180488841 | TOPC1451551777876756 |`, +| 新闻联播 | 新闻周刊 | 天下足球 | +| -------------------- | -------------------- | -------------------- | +| TOPC1451528971114112 | TOPC1451559180488841 | TOPC1451551777876756 |`, }; async function handler(ctx) { diff --git a/lib/routes/cntv/namespace.ts b/lib/routes/cntv/namespace.ts index 90d6b24fb4eff2..7551ee1493e4ee 100644 --- a/lib/routes/cntv/namespace.ts +++ b/lib/routes/cntv/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'CNTV', url: 'navi.cctv.com', + lang: 'zh-CN', }; diff --git a/lib/routes/codeforces/contests.ts b/lib/routes/codeforces/contests.ts index b78317506ce1b9..978ad8a491bc3d 100644 --- a/lib/routes/codeforces/contests.ts +++ b/lib/routes/codeforces/contests.ts @@ -2,7 +2,7 @@ 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 path from 'node:path'; import { art } from '@/utils/render'; @@ -46,7 +46,7 @@ export const route: Route = { }; async function handler() { - const contestsData = await got.get(contestAPI).json(); + const contestsData = await ofetch(contestAPI); const contests = contestsData.result; const items = contests diff --git a/lib/routes/codeforces/namespace.ts b/lib/routes/codeforces/namespace.ts index eb07b18e31155a..5cd8b7e721a30f 100644 --- a/lib/routes/codeforces/namespace.ts +++ b/lib/routes/codeforces/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Codeforces', url: 'codeforces.com', + lang: 'en', }; diff --git a/lib/routes/codeforces/recent-actions.ts b/lib/routes/codeforces/recent-actions.ts index 11d5434a169928..2baaf8c823fc2f 100644 --- a/lib/routes/codeforces/recent-actions.ts +++ b/lib/routes/codeforces/recent-actions.ts @@ -1,5 +1,5 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; export const route: Route = { @@ -30,7 +30,7 @@ export const route: Route = { async function handler(ctx) { const minRating = ctx.req.param('minrating') || 1; - const rsp = await got.get('https://codeforces.com/api/recentActions?maxCount=100').json(); + const rsp = await ofetch('https://codeforces.com/api/recentActions?maxCount=100'); const actions = rsp.result.map((action) => { const pubDate = new Date(action.timeSeconds * 1000); diff --git a/lib/routes/cohere/index.ts b/lib/routes/cohere/index.ts new file mode 100644 index 00000000000000..a55552b13b9404 --- /dev/null +++ b/lib/routes/cohere/index.ts @@ -0,0 +1,54 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: ['/blog'], + name: 'Blog', + url: 'cohere.com/blog', + maintainers: ['Loongphy'], + handler, + example: '/cohere/blog', + description: 'Cohere is a platform for building AI applications.', + categories: ['blog'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['cohere.com'], + }, + ], +}; + +async function handler() { + const { posts: data } = await ofetch('https://cohere-ai.ghost.io/ghost/api/content/posts', { + params: { + key: '572d288a9364f8e4186af1d60a', + limit: 'all', + include: ['authors', 'tags'], + filter: 'tag:-hash-hidden+tag:-llmu', + }, + }); + + const items = data.map((item) => ({ + title: item.title, + link: 'https://cohere.com/blog/' + item.slug, + description: item.excerpt, + pubDate: parseDate(item.published_at), + author: item.authors.map((author) => author.name).join(', '), + category: item.tags.map((tag) => tag.name), + })); + + return { + title: 'The Cohere Blog', + link: 'https://cohere.com/blog', + item: items, + }; +} diff --git a/lib/routes/cohere/namespace.ts b/lib/routes/cohere/namespace.ts new file mode 100644 index 00000000000000..72d0868fdb0c80 --- /dev/null +++ b/lib/routes/cohere/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Cohere', + url: 'cohere.com', + lang: 'en', +}; diff --git a/lib/routes/coindesk/index.ts b/lib/routes/coindesk/consensus-magazine.ts similarity index 50% rename from lib/routes/coindesk/index.ts rename to lib/routes/coindesk/consensus-magazine.ts index 014e99ca572ade..3ae06e973c3cec 100644 --- a/lib/routes/coindesk/index.ts +++ b/lib/routes/coindesk/consensus-magazine.ts @@ -1,6 +1,8 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; import { load } from 'cheerio'; +import { parseItem } from './utils'; const rootUrl = 'https://www.coindesk.com'; export const route: Route = { @@ -27,29 +29,23 @@ export const route: Route = { url: 'coindesk.com/', }; -async function handler(ctx) { - const channel = ctx.req.param('channel') ?? 'consensus-magazine'; +async function handler() { + const channel = 'consensus-magazine'; - const response = await got.get(`${rootUrl}/${channel}/`); - const $ = load(response.data); - const content = JSON.parse( - $('#fusion-metadata') - .text() - .match(/Fusion\.contentCache=(.*?);Fusion\.layout/)[1] - ); + const response = await ofetch(`${rootUrl}/${channel}`); + const $ = load(response); - const o1 = content['websked-collections']; - // Object key names are different every week - const articles = o1[Object.keys(o1)[2]]; + const list = $('div h2') + .toArray() + .map((item) => { + const $item = $(item); + return { + title: $item.text(), + link: rootUrl + $item.parent().attr('href'), + }; + }); - const list = articles.data; - - const items = list.map((item) => ({ - title: item.headlines.basic, - link: rootUrl + item.canonical_url, - description: item.subheadlines.basic, - pubDate: item.display_date, - })); + const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => parseItem(item)))); return { title: 'CoinDesk Consensus Magazine', diff --git a/lib/routes/coindesk/namespace.ts b/lib/routes/coindesk/namespace.ts index 9aa1ef1d7c703d..0884bc330fb314 100644 --- a/lib/routes/coindesk/namespace.ts +++ b/lib/routes/coindesk/namespace.ts @@ -1,6 +1,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'CoinDesk Consensus Magazine', + name: 'CoinDesk', url: 'coindesk.com', + lang: 'en', + description: 'CoinDesk is a news site specializing in bitcoin and digital currencies, delivering news, analysis, and information about the blockchain ecosystem.', }; diff --git a/lib/routes/coindesk/news.ts b/lib/routes/coindesk/news.ts new file mode 100644 index 00000000000000..42adf1da8126e6 --- /dev/null +++ b/lib/routes/coindesk/news.ts @@ -0,0 +1,47 @@ +import { Route, Data, DataItem } from '@/types'; +import cache from '@/utils/cache'; +import parser from '@/utils/rss-parser'; +import { parseItem } from './utils'; + +export const route: Route = { + path: '/news', + categories: ['finance'], + example: '/coindesk/news', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'News', + maintainers: ['pseudoyu'], + handler, + radar: [ + { + source: ['coindesk.com/'], + target: '/news', + }, + ], + description: 'Get latest news from CoinDesk with full text.', +}; + +async function handler(): Promise { + const rssUrl = 'https://feeds.feedburner.com/Coindesk'; + const feed = await parser.parseURL(rssUrl); + + const items = await Promise.all(feed.items.map((item) => cache.tryGet(item.link, () => parseItem(item)))); + + // Filter out null items + const validItems = items.filter((item): item is DataItem => item !== null); + + return { + title: feed.title || 'CoinDesk News', + link: feed.link || 'https://coindesk.com', + description: feed.description || 'Latest news from CoinDesk', + language: feed.language || 'en', + item: validItems, + }; +} diff --git a/lib/routes/coindesk/utils.ts b/lib/routes/coindesk/utils.ts new file mode 100644 index 00000000000000..d43d91f8ef5817 --- /dev/null +++ b/lib/routes/coindesk/utils.ts @@ -0,0 +1,26 @@ +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +export const parseItem = async (item) => { + const response = await ofetch(item.link); + const $ = load(response); + const ldJson = JSON.parse($('script[type="application/ld+json"]').text()); + + $('.article-ad, #strategy-rules-player-wrapper, [data-module-name="newsletter-article-sign-up-module"], div.flex.flex-col.gap-2').remove(); + const cover = $('.article-content-wrapper figure'); + cover.find('img').attr('src', cover.find('img').attr('url')?.split('?')[0]); + cover.find('img').removeAttr('style srcset url'); + + item.description = + cover.parent().html() + + $('.document-body') + .toArray() + .map((item) => $(item).html()) + .join(''); + item.pubDate = parseDate(ldJson.datePublished); + item.author = ldJson.author.map((a) => ({ name: a.name })); + item.image = ldJson.image.url.split('?')[0]; + + return item; +}; diff --git a/lib/routes/cointelegraph/index.ts b/lib/routes/cointelegraph/index.ts new file mode 100644 index 00000000000000..a1fe1db5e7fd8a --- /dev/null +++ b/lib/routes/cointelegraph/index.ts @@ -0,0 +1,106 @@ +import { Route, Data, DataItem } 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: '/cointelegraph', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'News', + maintainers: ['pseudoyu'], + handler, + radar: [ + { + source: ['cointelegraph.com/'], + target: '/', + }, + ], + description: 'Get latest news from Cointelegraph with full text.', +}; + +async function handler(): Promise { + const rssUrl = 'https://cointelegraph.com/rss'; + const feed = await parser.parseURL(rssUrl); + + const items = await Promise.all( + feed.items + .filter((item) => item.link && /\/news|\/explained|\/innovation-circle/.test(item.link)) + .map((item) => ({ + ...item, + link: item.link?.split('?')[0], + })) + .map((item) => + cache.tryGet(item.link!, async () => { + const link = item.link!; + + // Extract full text + const fullText = await extractFullText(link); + + if (!fullText) { + logger.warn(`Failed to extract content from ${link}`); + } + + // Create article item + return { + title: item.title || 'Untitled', + description: fullText || item.content, + pubDate: item.pubDate ? parseDate(item.pubDate) : undefined, + link, + author: item.creator || 'CoinTelegraph', + category: item.categories?.map((c) => c.trim()) || [], + image: item.enclosure?.url, + } as DataItem; + }) + ) + ); + + // Filter out null items + const validItems = items.filter((item): item is DataItem => item !== null); + + return { + title: feed.title || 'CoinTelegraph News', + link: feed.link || 'https://cointelegraph.com', + description: feed.description || 'Latest news from CoinTelegraph', + language: feed.language || 'en', + item: validItems, + }; +} + +async function extractFullText(url: string): Promise { + try { + const response = await ofetch(url); + const $ = load(response); + const nuxtData = $('script:contains("window.__NUXT__")').text(); + const fullText = JSON.parse(nuxtData.match(/\.fullText=(".*?");/)?.[1] || '{}'); + const cover = $('.post-cover__image'); + + // Remove unwanted elements + cover.find('source').remove(); + cover.find('img').removeAttr('srcset'); + cover.find('img').attr( + 'src', + cover + .find('img') + .attr('src') + ?.match(/(https:\/\/s3\.cointelegraph\.com\/.+)/)?.[1] || '' + ); + + return cover.html() + fullText || null; + } catch (error) { + logger.error(`Error fetching article content: ${error}`); + return null; + } +} diff --git a/lib/routes/cointelegraph/namespace.ts b/lib/routes/cointelegraph/namespace.ts new file mode 100644 index 00000000000000..739ef8456b2200 --- /dev/null +++ b/lib/routes/cointelegraph/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Cointelegraph', + url: 'cointelegraph.com', + lang: 'en', +}; diff --git a/lib/routes/colamanga/manga.ts b/lib/routes/colamanga/manga.ts index 933e31bb95bc75..ca391d42f5491b 100644 --- a/lib/routes/colamanga/manga.ts +++ b/lib/routes/colamanga/manga.ts @@ -60,7 +60,7 @@ async function handler(ctx: Context) { }); const response = await page.content(); - browser.close(); + await browser.close(); const $ = load(response); diff --git a/lib/routes/colamanga/namespace.ts b/lib/routes/colamanga/namespace.ts index fbb20562d32be3..c135c4585205b7 100644 --- a/lib/routes/colamanga/namespace.ts +++ b/lib/routes/colamanga/namespace.ts @@ -6,4 +6,5 @@ export const namespace: Namespace = { zh: { name: '可乐漫画', }, + lang: 'zh-CN', }; diff --git a/lib/routes/collabo-cafe/category.ts b/lib/routes/collabo-cafe/category.ts new file mode 100644 index 00000000000000..844de43fcf0e53 --- /dev/null +++ b/lib/routes/collabo-cafe/category.ts @@ -0,0 +1,37 @@ +import { Data, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { Context } from 'hono'; +import { parseItems } from './parser'; + +export const handler = async (ctx: Context): Promise => { + const { category } = ctx.req.param(); + const baseUrl = `https://collabo-cafe.com/events/category/${category}`; + const res = await ofetch(baseUrl); + const $ = load(res); + const items = parseItems($); + + return { + title: '分类', + link: baseUrl, + item: items, + }; +}; + +export const route: Route = { + path: '/category/:category', + categories: ['anime'], + example: '/collabo-cafe/category/cafe', + parameters: { category: 'Category, refer to the original website (ジャンル別)' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '分类', + maintainers: ['cokemine'], + handler, +}; diff --git a/lib/routes/collabo-cafe/index.ts b/lib/routes/collabo-cafe/index.ts new file mode 100644 index 00000000000000..bb511df12732a0 --- /dev/null +++ b/lib/routes/collabo-cafe/index.ts @@ -0,0 +1,35 @@ +import { Data, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseItems } from './parser'; + +export const handler = async (): Promise => { + const baseUrl = 'https://collabo-cafe.com/'; + const res = await ofetch(baseUrl); + const $ = load(res); + const items = parseItems($); + + return { + title: '全部文章', + link: baseUrl, + item: items, + }; +}; + +export const route: Route = { + path: '/', + categories: ['anime'], + example: '/collabo-cafe/', + parameters: undefined, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '全部文章', + maintainers: ['cokemine'], + handler, +}; diff --git a/lib/routes/collabo-cafe/namespace.ts b/lib/routes/collabo-cafe/namespace.ts new file mode 100644 index 00000000000000..02b5c98c88cfb1 --- /dev/null +++ b/lib/routes/collabo-cafe/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'コラボカフェ', + url: 'collabo-cafe.com', + description: 'コラボカフェ - アニメ・漫画・ゲームのコラボ情報一覧まとめ', + lang: 'ja', + categories: ['anime'], +}; diff --git a/lib/routes/collabo-cafe/parser.ts b/lib/routes/collabo-cafe/parser.ts new file mode 100644 index 00000000000000..e7f894d69c58b3 --- /dev/null +++ b/lib/routes/collabo-cafe/parser.ts @@ -0,0 +1,29 @@ +import { type DataItem } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import { CheerioAPI } from 'cheerio'; + +export function parseItems($: CheerioAPI): DataItem[] { + return $('div.top-post-list article') + .toArray() + .map((el) => { + const $el = $(el); + const a = $el.find('a').first(); + const title = a.attr('title')!; + const link = a.attr('href'); + const pubDate = parseDate($el.find('span.date.gf.updated').text()); + const author = $el.find('span.author span.fn').text(); + const category = [$el.find('span.cat-name').text()]; + const description = $el.find('div.description p').text(); + const image = $el.find('img').attr('data-src'); + return { + title, + link, + pubDate, + author, + category, + description, + image, + banner: image, + }; + }); +} diff --git a/lib/routes/collabo-cafe/tag.ts b/lib/routes/collabo-cafe/tag.ts new file mode 100644 index 00000000000000..a47b6bc56a2c0d --- /dev/null +++ b/lib/routes/collabo-cafe/tag.ts @@ -0,0 +1,37 @@ +import { Data, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { Context } from 'hono'; +import { parseItems } from './parser'; + +export const handler = async (ctx: Context): Promise => { + const { tag } = ctx.req.param(); + const baseUrl = `https://collabo-cafe.com/events/tag/${tag}`; + const res = await ofetch(baseUrl); + const $ = load(res); + const items = parseItems($); + + return { + title: '标签', + link: baseUrl, + item: items, + }; +}; + +export const route: Route = { + path: '/tag/:tag', + categories: ['anime'], + example: '/collabo-cafe/tag/ikebukuro', + parameters: { tag: 'Tag, refer to the original website (開催地域別)' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '标签', + maintainers: ['cokemine'], + handler, +}; diff --git a/lib/routes/comicat/namespace.ts b/lib/routes/comicat/namespace.ts index 8082c946090a15..99df68b06a16e9 100644 --- a/lib/routes/comicat/namespace.ts +++ b/lib/routes/comicat/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Comicat', url: 'comicat.org', + lang: 'zh-CN', }; diff --git a/lib/routes/comicskingdom/namespace.ts b/lib/routes/comicskingdom/namespace.ts index ed4fd59b4757da..713239031fc500 100644 --- a/lib/routes/comicskingdom/namespace.ts +++ b/lib/routes/comicskingdom/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Comics Kingdom', url: 'comicskingdom.com', + lang: 'en', }; diff --git a/lib/routes/consumer/index.ts b/lib/routes/consumer/index.ts index edc7e1be19c475..83b0e249e5ba44 100644 --- a/lib/routes/consumer/index.ts +++ b/lib/routes/consumer/index.ts @@ -28,15 +28,15 @@ export const route: Route = { url: 'consumer.org.hk/', description: `分类 - | 测试及调查 | 生活资讯 | 投诉实录 | 议题评论 | - | ---------- | -------- | --------- | -------- | - | test | life | complaint | topic | +| 测试及调查 | 生活资讯 | 投诉实录 | 议题评论 | +| ---------- | -------- | --------- | -------- | +| test | life | complaint | topic | 语言 - | 简体中文 | 繁体中文 | - | -------- | -------- | - | sc | tc |`, +| 简体中文 | 繁体中文 | +| -------- | -------- | +| sc | tc |`, }; async function handler(ctx) { diff --git a/lib/routes/consumer/namespace.ts b/lib/routes/consumer/namespace.ts index 524ebe75a2082e..765ba45eaec198 100644 --- a/lib/routes/consumer/namespace.ts +++ b/lib/routes/consumer/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '消费者委员会', url: 'consumer.org.hk', + lang: 'zh-CN', }; diff --git a/lib/routes/consumer/shopping-guide.ts b/lib/routes/consumer/shopping-guide.ts index c9352d9d696bd0..f0fc9b221f53d1 100644 --- a/lib/routes/consumer/shopping-guide.ts +++ b/lib/routes/consumer/shopping-guide.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| 冷知識 | 懶人包 | 特集 | 銀髮一族 | 飲食煮意 | 科技達人 | 健康美容 | 規劃人生 | 消閒娛樂 | 家品家電 | 親子時光 | 綠色生活 | - | ------ | ------ | -------- | ------------------ | ---------------- | ---------- | ----------------- | --------------------------- | ------------------------- | --------------- | --------------- | ------------ | - | trivia | tips | features | silver-hair-market | food-and-cooking | tech-savvy | health-and-beauty | life-and-financial-planning | leisure-and-entertainment | home-appliances | family-and-kids | green-living |`, +| ------ | ------ | -------- | ------------------ | ---------------- | ---------- | ----------------- | --------------------------- | ------------------------- | --------------- | --------------- | ------------ | +| trivia | tips | features | silver-hair-market | food-and-cooking | tech-savvy | health-and-beauty | life-and-financial-planning | leisure-and-entertainment | home-appliances | family-and-kids | green-living |`, }; async function handler(ctx) { diff --git a/lib/routes/cool18/namespace.ts b/lib/routes/cool18/namespace.ts index 2e72b336222253..7a41d2ea4643d4 100644 --- a/lib/routes/cool18/namespace.ts +++ b/lib/routes/cool18/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '禁忌书屋', url: 'cool18.com', + lang: 'zh-CN', }; diff --git a/lib/routes/coolapk/dyh.ts b/lib/routes/coolapk/dyh.ts index 8b386a72505c26..4bc6e7d323340f 100644 --- a/lib/routes/coolapk/dyh.ts +++ b/lib/routes/coolapk/dyh.ts @@ -9,7 +9,13 @@ export const route: Route = { example: '/coolapk/dyh/1524', parameters: { dyhId: '看看号ID' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'ALLOW_USER_HOTLINK_TEMPLATE', + optional: true, + description: '设置为`true`并添加`image_hotlink_template`参数来代理图片', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -19,9 +25,9 @@ export const route: Route = { name: '看看号', maintainers: ['xizeyoupan'], handler, - description: `:::tip + description: `::: tip 仅限于采集**站内订阅**的看看号的内容。看看号 ID 可在看看号界面右上分享 - 复制链接得到。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/coolapk/hot.ts b/lib/routes/coolapk/hot.ts index e12624ad9fe4b2..2bfeec1e359810 100644 --- a/lib/routes/coolapk/hot.ts +++ b/lib/routes/coolapk/hot.ts @@ -72,7 +72,13 @@ export const route: Route = { example: '/coolapk/hot', parameters: { type: '默认为`jrrm`', period: '默认为`daily`' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'ALLOW_USER_HOTLINK_TEMPLATE', + optional: true, + description: '设置为`true`并添加`image_hotlink_template`参数来代理图片', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -83,16 +89,16 @@ export const route: Route = { maintainers: ['xizeyoupan'], handler, description: `| 参数名称 | 今日热门 | 点赞榜 | 评论榜 | 收藏榜 | 酷图榜 | - | -------- | -------- | ------ | ------ | ------ | ------ | - | type | jrrm | dzb | plb | scb | ktb | +| -------- | -------- | ------ | ------ | ------ | ------ | +| type | jrrm | dzb | plb | scb | ktb | - | 参数名称 | 日榜 | 周榜 | - | -------- | ----- | ------ | - | period | daily | weekly | +| 参数名称 | 日榜 | 周榜 | +| -------- | ----- | ------ | +| period | daily | weekly | - :::tip +::: tip 今日热门没有周榜,酷图榜日榜的参数会变成周榜,周榜的参数会变成月榜。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/coolapk/huati.ts b/lib/routes/coolapk/huati.ts index 7fc9820e895ffe..67dc7d951ce692 100644 --- a/lib/routes/coolapk/huati.ts +++ b/lib/routes/coolapk/huati.ts @@ -9,7 +9,13 @@ export const route: Route = { example: '/coolapk/huati/iPhone', parameters: { tag: '话题名称' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'ALLOW_USER_HOTLINK_TEMPLATE', + optional: true, + description: '设置为`true`并添加`image_hotlink_template`参数来代理图片', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, diff --git a/lib/routes/coolapk/namespace.ts b/lib/routes/coolapk/namespace.ts index f9969354ad5a3c..7c3147c6cbc8af 100644 --- a/lib/routes/coolapk/namespace.ts +++ b/lib/routes/coolapk/namespace.ts @@ -3,4 +3,11 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '酷安', url: 'coolapk.com', + description: ` +::: tip +即日起,多数路由图片防盗链。 +需要将 \`ALLOW_USER_HOTLINK_TEMPLATE\` 环境变量设置为 \`true\` ,然后配置\`image_hotlink_template\` 。 +详见 [#16715](https://github.com/DIYgod/RSSHub/issues/16715) +:::`, + lang: 'zh-CN', }; diff --git a/lib/routes/coolapk/toutiao.ts b/lib/routes/coolapk/toutiao.ts index e4a6fd8104c4bf..bd163a88087bdc 100644 --- a/lib/routes/coolapk/toutiao.ts +++ b/lib/routes/coolapk/toutiao.ts @@ -8,7 +8,13 @@ export const route: Route = { example: '/coolapk/toutiao', parameters: { type: '默认为history' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'ALLOW_USER_HOTLINK_TEMPLATE', + optional: true, + description: '设置为`true`并添加`image_hotlink_template`参数来代理图片', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -19,8 +25,8 @@ export const route: Route = { maintainers: ['xizeyoupan'], handler, description: `| 参数名称 | 历史头条 | 最新 | - | -------- | -------- | ------ | - | type | history | latest |`, +| -------- | -------- | ------ | +| type | history | latest |`, }; async function handler(ctx) { diff --git a/lib/routes/coolapk/tuwen.ts b/lib/routes/coolapk/tuwen.ts index ece427b0d6abbd..9c9d699cf7a062 100644 --- a/lib/routes/coolapk/tuwen.ts +++ b/lib/routes/coolapk/tuwen.ts @@ -3,12 +3,18 @@ import got from '@/utils/got'; import utils from './utils'; export const route: Route = { - path: ['/tuwen/:type?', '/tuwen-xinxian'], + path: ['/tuwen/:type?'], categories: ['social-media'], example: '/coolapk/tuwen', parameters: { type: '默认为hot' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'ALLOW_USER_HOTLINK_TEMPLATE', + optional: true, + description: '设置为`true`并添加`image_hotlink_template`参数来代理图片', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -19,22 +25,21 @@ export const route: Route = { maintainers: ['xizeyoupan'], handler, description: `| 参数名称 | 编辑精选 | 最新 | - | -------- | -------- | ------ | - | type | hot | latest |`, +| -------- | -------- | ------ | +| type | hot | latest |`, }; async function handler(ctx) { const type = ctx.req.param('type') || 'hot'; - const requestPath = ctx.req.path; let feedTitle; const fullUrl = new URL('/v6/page/dataList', utils.base_url); - if (requestPath.startsWith('/coolapk/tuwen-xinxian') || type === 'latest') { + if (type === 'latest') { // 实时 fullUrl.searchParams.append('url', `/feed/digestList?${new URLSearchParams('cacheExpires=300&type=12&message_status=all&is_html_article=1&filterEmptyPicture=1&filterTag=二手交易,酷安自贸区,薅羊毛小分队').toString()}`); fullUrl.searchParams.append('title', '新鲜图文'); fullUrl.searchParams.append('subTitle', ''); feedTitle = '酷安 - 新鲜图文'; - } else if (requestPath.startsWith('/coolapk/tuwen')) { + } else { // 精选 fullUrl.searchParams.append('url', `#/feed/digestList?${new URLSearchParams('type=12&is_html_article=1&recommend=3,4').toString()}`); fullUrl.searchParams.append('title', '图文'); diff --git a/lib/routes/coolapk/user-dynamic.ts b/lib/routes/coolapk/user-dynamic.ts index 965df483525cc9..a23425839d561b 100644 --- a/lib/routes/coolapk/user-dynamic.ts +++ b/lib/routes/coolapk/user-dynamic.ts @@ -9,7 +9,13 @@ export const route: Route = { example: '/coolapk/user/3177668/dynamic', parameters: { uid: '在个人界面右上分享-复制链接获取' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'ALLOW_USER_HOTLINK_TEMPLATE', + optional: true, + description: '设置为`true`并添加`image_hotlink_template`参数来代理图片', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, diff --git a/lib/routes/coomer/artist.ts b/lib/routes/coomer/artist.ts deleted file mode 100644 index a16dab6fa3f8eb..00000000000000 --- a/lib/routes/coomer/artist.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Route } from '@/types'; -import fetchItems from './utils'; - -export const route: Route = { - path: '/artist/:id', - categories: ['multimedia'], - example: '/coomer/artist/belledelphine', - parameters: { id: 'Artist id, can be found in URL' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['coomer.party/onlyfans/user/:id', 'coomer.party/'], - }, - ], - name: 'Artist', - maintainers: ['nczitzk'], - handler, -}; - -async function handler(ctx) { - const id = ctx.req.param('id'); - - const currentUrl = `onlyfans/user/${id}`; - - return await fetchItems(ctx, currentUrl); -} diff --git a/lib/routes/coomer/index.ts b/lib/routes/coomer/index.ts new file mode 100644 index 00000000000000..70a52c830ad3ff --- /dev/null +++ b/lib/routes/coomer/index.ts @@ -0,0 +1,157 @@ +import { Route } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +export const route: Route = { + path: '/:source?/:id?', + categories: ['multimedia'], + example: '/coomer', + parameters: { source: 'Source, see below, Posts by default', id: 'User id, can be found in URL' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['coomer.su/:source/user/:id', 'coomer.su/'], + }, + ], + name: 'Posts', + maintainers: ['nczitzk', 'AiraNadih'], + handler, + description: `Sources + +| Posts | OnlyFans | Fansly | CandFans | +| ----- | -------- | ------- | -------- | +| posts | onlyfans | fansly | candfans | + +::: tip + When \`posts\` is selected as the value of the parameter **source**, the parameter **id** does not take effect. + There is an optinal parameter **limit** which controls the number of posts to fetch, default value is 25. +:::`, +}; + +async function handler(ctx) { + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 25; + const source = ctx.req.param('source') ?? 'posts'; + const id = ctx.req.param('id'); + const isPosts = source === 'posts'; + + const rootUrl = 'https://coomer.su'; + const apiUrl = `${rootUrl}/api/v1`; + const currentUrl = isPosts ? `${apiUrl}/posts` : `${apiUrl}/${source}/user/${id}`; + + const headers = { + cookie: '__ddg2=sBQ4uaaGecmfEUk7', + }; + + const response = await got({ + method: 'get', + url: currentUrl, + headers, + }); + const responseData = isPosts ? response.data.posts : response.data; + + const author = isPosts ? '' : await getAuthor(currentUrl, headers); + const title = isPosts ? 'Coomer Posts' : `Posts of ${author} from ${source} | Coomer`; + const image = isPosts ? `${rootUrl}/favicon.ico` : `https://img.coomer.su/icons/${source}/${id}`; + const items = responseData + .filter((i) => i.content || i.attachments) + .slice(0, limit) + .map((i) => { + i.files = []; + if ('path' in i.file) { + i.files.push({ + name: i.file.name, + path: i.file.path, + extension: i.file.path.replace(/.*\./, '').toLowerCase(), + }); + } + for (const attachment of i.attachments) { + i.files.push({ + name: attachment.name, + path: attachment.path, + extension: attachment.path.replace(/.*\./, '').toLowerCase(), + }); + } + const filesHTML = art(path.join(__dirname, 'templates', 'source.art'), { i }); + let $ = load(filesHTML); + const coomerFiles = $('img, a, audio, video').map(function () { + return $(this).prop('outerHTML')!; + }); + let desc = ''; + if (i.content) { + desc += `
    ${i.content}
    `; + } + $ = load(desc); + let count = 0; + const regex = /downloads.fanbox.cc/; + $('a').each(function () { + const link = $(this).attr('href'); + if (regex.test(link!)) { + count++; + $(this).replaceWith(coomerFiles[count]); + } + }); + desc = (coomerFiles.length > 0 ? coomerFiles[0] : '') + $.html(); + for (const coomerFile of coomerFiles.slice(count + 1)) { + desc += coomerFile; + } + + let enclosureInfo = {}; + load(desc)('audio source, video source').each(function () { + const src = $(this).attr('src') ?? ''; + const mimeType = + { + m4a: 'audio/mp4', + mp3: 'audio/mpeg', + mp4: 'video/mp4', + }[src.replace(/.*\./, '').toLowerCase()] || null; + + if (mimeType === null) { + return; + } + + enclosureInfo = { + enclosure_url: new URL(src, rootUrl).toString(), + enclosure_type: mimeType, + }; + }); + + return { + title: i.title || parseDate(i.published), + description: desc, + author, + pubDate: parseDate(i.published), + guid: `${apiUrl}/${i.service}/user/${i.user}/post/${i.id}`, + link: `${rootUrl}/${i.service}/user/${i.user}/post/${i.id}`, + ...enclosureInfo, + }; + }); + + return { + title, + image, + link: isPosts ? `${rootUrl}/posts` : `${rootUrl}/${source}/user/${id}`, + item: items, + }; +} + +async function getAuthor(currentUrl, headers) { + const profileResponse = await got({ + method: 'get', + url: `${currentUrl}/profile`, + headers, + }); + return profileResponse.data.name; +} diff --git a/lib/routes/coomer/namespace.ts b/lib/routes/coomer/namespace.ts index df39e0d9550313..9271eec4f2b641 100644 --- a/lib/routes/coomer/namespace.ts +++ b/lib/routes/coomer/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Coomer', - url: 'coomer.party', + url: 'coomer.su', + lang: 'en', }; diff --git a/lib/routes/coomer/posts.ts b/lib/routes/coomer/posts.ts deleted file mode 100644 index 85a6d067efe419..00000000000000 --- a/lib/routes/coomer/posts.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Route } from '@/types'; -import fetchItems from './utils'; - -export const route: Route = { - path: '/posts', - categories: ['multimedia'], - example: '/coomer/posts', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['coomer.party/posts', 'coomer.party/'], - }, - ], - name: 'Recent Posts', - maintainers: ['nczitzk'], - handler, - url: 'coomer.party/posts', -}; - -async function handler(ctx) { - const currentUrl = 'posts'; - - return await fetchItems(ctx, currentUrl); -} diff --git a/lib/routes/coomer/templates/source.art b/lib/routes/coomer/templates/source.art new file mode 100644 index 00000000000000..47f87559e10634 --- /dev/null +++ b/lib/routes/coomer/templates/source.art @@ -0,0 +1,24 @@ +{{ if i.files }} + {{ each i.files file }} + {{ if file.extension === 'jpg' || file.extension === 'png' || file.extension === 'webp' || file.extension === 'jpeg' || file.extension === 'jfif' }} + + {{ 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 }}

    {{ i.embed.description }}

    {{ /if }} + {{ /if }} +{{ /if }} diff --git a/lib/routes/coomer/utils.ts b/lib/routes/coomer/utils.ts deleted file mode 100644 index 50483bea018590..00000000000000 --- a/lib/routes/coomer/utils.ts +++ /dev/null @@ -1,57 +0,0 @@ -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import { parseDate } from '@/utils/parse-date'; - -const fetchItems = async (ctx, currentUrl) => { - const rootUrl = 'https://coomer.party'; - currentUrl = `${rootUrl}/${currentUrl}`; - - const response = await got({ - method: 'get', - url: currentUrl, - }); - - const $ = load(response.data); - - let items = $('.card-list__items') - .find('a') - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 25) - .toArray() - .map((item) => { - item = $(item); - - return { - link: `${rootUrl}${item.attr('href')}`, - }; - }); - - items = await Promise.all( - items.map((item) => - cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - - const content = load(detailResponse.data); - - content('.ad-container').remove(); - - item.author = content('.post__user-name').text(); - item.title = content('.post__title span').first().text(); - item.pubDate = parseDate(content('.timestamp').attr('datetime')); - item.description = content('.post__body').html(); - - return item; - }) - ) - ); - - return { - title: $('title').text(), - link: currentUrl, - item: items, - }; -}; -export default fetchItems; diff --git a/lib/routes/copernicium/index.ts b/lib/routes/copernicium/index.ts index aceeeaff813281..d88bc793ccb0ab 100644 --- a/lib/routes/copernicium/index.ts +++ b/lib/routes/copernicium/index.ts @@ -6,7 +6,7 @@ import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: '/:category', + path: '/:category?', categories: ['new-media'], radar: [{ source: ['www.copernicium.tw'] }], name: '分类', @@ -17,25 +17,33 @@ export const route: Route = { }; async function handler(ctx) { - const CATEGORY_TO_ARG_MAP = new Map([ - ['环球视角', '4_1'], - ['人文叙述', '4_3'], - ['观点评论', '4_5'], - ['专题报道', '4_7'], - ]); - if (!CATEGORY_TO_ARG_MAP.get(ctx.req.param().category)) { - throw new Error('The requested category does not exist or is not supported.'); + const category = ctx.req.param('category'); + let res; + if (category) { + const CATEGORY_TO_ARG_MAP = new Map([ + ['环球视角', '4_1'], + ['人文叙述', '4_3'], + ['观点评论', '4_5'], + ['专题报道', '4_7'], + ]); + if (!CATEGORY_TO_ARG_MAP.get(category)) { + throw new Error('The requested category does not exist or is not supported.'); + } + const reqArgs = { + args: { + _jcp: CATEGORY_TO_ARG_MAP.get(category), + m31pageno: 1, + }, + type: 0, + }; + res = await ofetch('http://www.copernicium.tw/nr.jsp', { + query: { _reqArgs: reqArgs }, + }); + } else { + res = await ofetch('http://www.copernicium.tw/sys-nr/', { + query: { _reqArgs: { args: {}, type: 15 } }, + }); } - const reqArgs = { - args: { - _jcp: CATEGORY_TO_ARG_MAP.get(ctx.req.param().category), - m31pageno: 1, - }, - type: 0, - }; - const res = await ofetch(`https://www.copernicium.tw/nr.jsp`, { - query: { _reqArgs: reqArgs }, - }); const $ = load(res); const list = $('.J_newsResultLine a.mixNewsStyleTitle') .toArray() @@ -60,8 +68,8 @@ async function handler(ctx) { ) ); return { - title: `日新说 - ${ctx.req.param().category}`, - link: 'https://www.copernicium.tw', + title: `日新说 - ${category ?? '全部文章'}`, + link: 'http://www.copernicium.tw', item: items, }; } diff --git a/lib/routes/copernicium/namespace.ts b/lib/routes/copernicium/namespace.ts index 66ba8040fa32e7..fe1ccb9acbd52c 100644 --- a/lib/routes/copernicium/namespace.ts +++ b/lib/routes/copernicium/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '日新说', url: 'www.copernicium.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/copymanga/comic.ts b/lib/routes/copymanga/comic.ts index 233b46435038cb..eab05f7ce381fc 100644 --- a/lib/routes/copymanga/comic.ts +++ b/lib/routes/copymanga/comic.ts @@ -25,7 +25,7 @@ export const route: Route = { supportScihub: false, }, name: '漫画更新', - maintainers: ['btdwv', 'marvolo666', 'yan12125'], + maintainers: ['btdwv', 'marvolo666'], handler, }; @@ -34,9 +34,9 @@ async function handler(ctx) { // 用于控制返回的章节数量 const chapterCnt = Number(ctx.req.param('chapterCnt') || 10); // 直接调用拷贝漫画的接口 - const host = 'copymanga.site'; + const host = 'www.mangacopy.com'; const baseUrl = `https://${host}`; - const apiBaseUrl = `https://api.${host}`; + const apiBaseUrl = `https://${host}`; const strBaseUrl = `${apiBaseUrl}/api/v3/comic/${id}/group/default/chapters`; const iReqLimit = 500; // 获取漫画列表 @@ -76,6 +76,7 @@ async function handler(ctx) { chapters = chapters .map(({ comic_path_word, uuid, name, size, datetime_created, ordered /* , index*/ }) => ({ link: `${baseUrl}/comic/${comic_path_word}/chapter/${uuid}`, + guid: `https://copymanga.site/comic/${comic_path_word}/chapter/${uuid}`, uuid, title: name, size, @@ -117,6 +118,7 @@ async function handler(ctx) { return { link: chapter.link, + guid: chapter.guid, title: chapter.title, description: art(path.join(__dirname, './templates/comic.art'), { size: chapter.size, diff --git a/lib/routes/copymanga/namespace.ts b/lib/routes/copymanga/namespace.ts index 972ed6507b31ef..b09d2ad3909110 100644 --- a/lib/routes/copymanga/namespace.ts +++ b/lib/routes/copymanga/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '拷贝漫画', url: 'copymanga.com', + lang: 'zh-CN', }; diff --git a/lib/routes/cosplaytele/article.ts b/lib/routes/cosplaytele/article.ts new file mode 100644 index 00000000000000..c5a534cceb448b --- /dev/null +++ b/lib/routes/cosplaytele/article.ts @@ -0,0 +1,13 @@ +import { parseDate } from '@/utils/parse-date'; +import { WPPost } from './types'; + +function loadArticle(item: WPPost) { + return { + title: item.title.rendered, + description: item.content.rendered, + pubDate: parseDate(item.date_gmt), + link: item.link, + }; +} + +export default loadArticle; diff --git a/lib/routes/cosplaytele/category.ts b/lib/routes/cosplaytele/category.ts new file mode 100644 index 00000000000000..755f01fd388d88 --- /dev/null +++ b/lib/routes/cosplaytele/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: '/cosplaytele/category/cosplay', + parameters: { category: 'Category' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['cosplaytele.com/category/:category'], + target: '/category/:category', + }, + ], + name: 'Category', + maintainers: ['AiraNadih'], + handler, + url: 'cosplaytele.com/', +}; + +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/cosplaytele/const.ts b/lib/routes/cosplaytele/const.ts new file mode 100644 index 00000000000000..c196c2ab8b4595 --- /dev/null +++ b/lib/routes/cosplaytele/const.ts @@ -0,0 +1,4 @@ +const SUB_NAME_PREFIX = 'CosplayTele'; +const SUB_URL = 'https://cosplaytele.com/'; + +export { SUB_NAME_PREFIX, SUB_URL }; diff --git a/lib/routes/cosplaytele/latest.ts b/lib/routes/cosplaytele/latest.ts new file mode 100644 index 00000000000000..b986a8d731229e --- /dev/null +++ b/lib/routes/cosplaytele/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: '/cosplaytele', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['cosplaytele.com/'], + target: '', + }, + ], + name: 'Latest', + maintainers: ['AiraNadih'], + handler, + url: 'cosplaytele.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/cosplaytele/namespace.ts b/lib/routes/cosplaytele/namespace.ts new file mode 100644 index 00000000000000..bcba7214fa5629 --- /dev/null +++ b/lib/routes/cosplaytele/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'CosplayTele', + url: 'cosplaytele.com', + description: 'Cosplaytele - Fast - Security - Free', + lang: 'en', +}; diff --git a/lib/routes/cosplaytele/popular.ts b/lib/routes/cosplaytele/popular.ts new file mode 100644 index 00000000000000..fe2b20577f73b5 --- /dev/null +++ b/lib/routes/cosplaytele/popular.ts @@ -0,0 +1,75 @@ +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: '/cosplaytele/popular/3', + parameters: { period: 'Days' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['cosplaytele.com/:period'], + target: '/popular/:period', + }, + ], + name: 'Popular', + maintainers: ['AiraNadih'], + handler, + url: 'cosplaytele.com/', +}; + +function getPeriodConfig(period) { + if (period === '1') { + return { + url: `${SUB_URL}24-hours/`, + range: 'daily', + title: `${SUB_NAME_PREFIX} - Top views in 24 hours`, + }; + } + return { + url: `${SUB_URL}${period}-day/`, + range: `last${period}days`, + title: `${SUB_NAME_PREFIX} - Top views in ${period} days`, + }; +} + +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/cosplaytele/tag.ts b/lib/routes/cosplaytele/tag.ts new file mode 100644 index 00000000000000..41676a57cfef65 --- /dev/null +++ b/lib/routes/cosplaytele/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: '/cosplaytele/tag/aqua', + parameters: { tag: 'Tag' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['cosplaytele.com/tag/:tag'], + target: '/tag/:tag', + }, + ], + name: 'Tag', + maintainers: ['AiraNadih'], + handler, + url: 'cosplaytele.com/', +}; + +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/cosplaytele/types.ts b/lib/routes/cosplaytele/types.ts new file mode 100644 index 00000000000000..d3ea3ac2a8cc26 --- /dev/null +++ b/lib/routes/cosplaytele/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/counter-strike/namespace.ts b/lib/routes/counter-strike/namespace.ts new file mode 100644 index 00000000000000..98157da3ccbe4a --- /dev/null +++ b/lib/routes/counter-strike/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Counter Strike', + url: 'counter-strike.net', + categories: ['game'], + description: '', +}; diff --git a/lib/routes/counter-strike/news.ts b/lib/routes/counter-strike/news.ts new file mode 100644 index 00000000000000..688ef3303455fd --- /dev/null +++ b/lib/routes/counter-strike/news.ts @@ -0,0 +1,188 @@ +import { Route } from '@/types'; +import type { BBobCoreTagNodeTree, PresetFactory } from '@bbob/types'; + +import got from '@/utils/got'; +import { load } from 'cheerio'; +import bbobHTML from '@bbob/html'; +import presetHTML5 from '@bbob/preset-html5'; +import { parseDate } from '@/utils/parse-date'; + +const swapLinebreak = (tree: BBobCoreTagNodeTree) => + tree.walk((node) => { + if (typeof node === 'string' && node === '\n') { + return { + tag: 'br', + content: null, + }; + } + return node; + }); + +const customPreset: PresetFactory = presetHTML5.extend((tags) => ({ + ...tags, + url: (node) => ({ + tag: 'a', + attrs: { + href: Object.keys(node.attrs as Record)[0], + rel: 'noopener', + target: '_blank', + }, + content: node.content, + }), + video: (node, { render }) => ({ + tag: 'video', + attrs: { + controls: '', + preload: 'metadata', + poster: node.attrs?.poster, + }, + content: render( + Object.entries({ + webm: 'video/webm', + mp4: 'video/mp4', + }).map(([key, type]) => ({ + tag: 'source', + attrs: { + src: node.attrs?.[key], + type, + }, + })) + ), + }), +})); + +export const handler = async (ctx) => { + const { category = 'all', language = 'english' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 100; + + const rootUrl = 'https://www.counter-strike.net'; + const apiRootUrl = 'https://store.steampowered.com'; + const cdnRootUrl = 'https://media.st.dl.eccdnx.com'; + const currentUrl = new URL(`news${category && category !== 'all' ? `/${category}` : ''}${language ? `?l=${language}` : ''}`, rootUrl).href; + const apiUrl = new URL('events/ajaxgetpartnereventspageable/', apiRootUrl).href; + + const { data: response } = await got(apiUrl, { + searchParams: { + clan_accountid: 0, + appid: 730, + offset: 0, + count: limit, + l: language, + }, + }); + + const items = response.events + .filter((item) => (category === 'updates' ? item.event_type === 12 : item.event_type)) + .slice(0, limit) + .map((item) => { + const title = item.event_name; + const description = bbobHTML(item.announcement_body.body, [customPreset(), swapLinebreak]); + const guid = `counter-strike-news-${item.gid}`; + + return { + title, + description, + pubDate: parseDate(item.announcement_body.posttime, 'X'), + link: new URL(`newsentry/${item.gid}`, rootUrl).href, + category: item.announcement_body.tags, + guid, + id: guid, + content: { + html: description, + text: item.announcement_body.body, + }, + updated: parseDate(item.announcement_body.updatetime, 'X'), + }; + }); + + const { data: currentResponse } = await got(currentUrl); + + const $ = load(currentResponse); + + const author = 'Counter Strike'; + const image = new URL('apps/csgo/images/dota_react//blog/default_cover.jpg', cdnRootUrl).href; + + return { + title: `${author} - ${category === 'updates' ? 'Updates' : 'News'}`, + description: $('title').text(), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author, + language, + }; +}; + +export const route: Route = { + path: '/news/:category?/:language?', + name: 'News', + url: 'www.counter-strike.net', + maintainers: ['nczitzk'], + handler, + example: '/counter-strike/news', + parameters: { category: 'Category, `updates` or `all`, `all` by default', language: 'Language, english by default, see below for more languages' }, + description: `::: tip + If you subscribe to [Updates in English](https://www.counter-strike.net/news/updates?l=english),where the URL is \`https://www.counter-strike.net/news/updates?l=english\`, extract the \`l\`, which is \`english\`, and use it as the parameter to fill in. Therefore, the route will be [\`/counter-strike/news/updates/english\`](https://rsshub.app/counter-strike/news/updates/english). +::: + +
    +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 | + +
    + `, + categories: ['game'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.counter-strike.net/news/:category'], + target: (params, url) => { + url = new URL(url); + const category = params.category; + const language = url.searchParams.get('l'); + + return `/news${category ? `/${category}${language ? `/${language}` : ''}` : ''}`; + }, + }, + ], +}; diff --git a/lib/routes/cpcaauto/index.ts b/lib/routes/cpcaauto/index.ts index 357b14204dc0a4..c55f5e7328bc0e 100644 --- a/lib/routes/cpcaauto/index.ts +++ b/lib/routes/cpcaauto/index.ts @@ -81,43 +81,43 @@ export const route: Route = { handler, example: '/cpcaauto/news/news', parameters: { type: '分类,默认为 news,可在对应分类页 URL 中找到', id: 'id,默认为 news,可在对应分类页 URL 中找到' }, - description: `:::tip + description: `::: tip 若订阅 [行业新闻 > 国内乘用车](http://cpcaauto.com/news.php?types=news&anid=10),网址为 \`http://cpcaauto.com/news.php?types=news&anid=10\`。截取 \`types\` 和 \`anid\` 的部分 \`\` 作为参数填入,此时路由为 [\`/cpcaauto/news/news/10\`](https://rsshub.app/cpcaauto/news/news/10)。 - ::: +::: - #### [行业新闻](http://cpcaauto.com/news.php?types=news) +#### [行业新闻](http://cpcaauto.com/news.php?types=news) - | [国内乘用车](http://cpcaauto.com/news.php?types=news&anid=10) | [进口及国外乘用车](http://cpcaauto.com/news.php?types=news&anid=64) | [后市场](http://cpcaauto.com/news.php?types=news&anid=44) | [商用车](http://cpcaauto.com/news.php?types=news&anid=62) | - | ----------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | - | [news/10](https://rsshub.app/cpcaauto/news/news/10) | [news/64](https://rsshub.app/cpcaauto/news/news/64) | [news/44](https://rsshub.app/cpcaauto/news/news/44) | [news/62](https://rsshub.app/cpcaauto/news/news/62) | +| [国内乘用车](http://cpcaauto.com/news.php?types=news&anid=10) | [进口及国外乘用车](http://cpcaauto.com/news.php?types=news&anid=64) | [后市场](http://cpcaauto.com/news.php?types=news&anid=44) | [商用车](http://cpcaauto.com/news.php?types=news&anid=62) | +| ----------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | +| [news/10](https://rsshub.app/cpcaauto/news/news/10) | [news/64](https://rsshub.app/cpcaauto/news/news/64) | [news/44](https://rsshub.app/cpcaauto/news/news/44) | [news/62](https://rsshub.app/cpcaauto/news/news/62) | - #### [车市解读](http://cpcaauto.com/news.php?types=csjd) +#### [车市解读](http://cpcaauto.com/news.php?types=csjd) - | [周度](http://cpcaauto.com/news.php?types=csjd&anid=128) | [月度](http://cpcaauto.com/news.php?types=csjd&anid=129) | [指数](http://cpcaauto.com/news.php?types=csjd&anid=130) | [预测](http://cpcaauto.com/news.php?types=csjd&anid=131) | - | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | - | [csjd/128](https://rsshub.app/cpcaauto/news/csjd/128) | [csjd/129](https://rsshub.app/cpcaauto/news/csjd/129) | [csjd/130](https://rsshub.app/cpcaauto/news/csjd/130) | [csjd/131](https://rsshub.app/cpcaauto/news/csjd/131) | +| [周度](http://cpcaauto.com/news.php?types=csjd&anid=128) | [月度](http://cpcaauto.com/news.php?types=csjd&anid=129) | [指数](http://cpcaauto.com/news.php?types=csjd&anid=130) | [预测](http://cpcaauto.com/news.php?types=csjd&anid=131) | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| [csjd/128](https://rsshub.app/cpcaauto/news/csjd/128) | [csjd/129](https://rsshub.app/cpcaauto/news/csjd/129) | [csjd/130](https://rsshub.app/cpcaauto/news/csjd/130) | [csjd/131](https://rsshub.app/cpcaauto/news/csjd/131) | - #### [发布会报告](http://cpcaauto.com/news.php?types=bgzl) +#### [发布会报告](http://cpcaauto.com/news.php?types=bgzl) - | [上海市场上牌数](http://cpcaauto.com/news.php?types=bgzl&anid=119) | [京城车市](http://cpcaauto.com/news.php?types=bgzl&anid=122) | [进口车市场分析](http://cpcaauto.com/news.php?types=bgzl&anid=120) | [二手车市场分析](http://cpcaauto.com/news.php?types=bgzl&anid=121) | [价格指数](http://cpcaauto.com/news.php?types=bgzl&anid=124) | - | ---------------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------- | - | [bgzl/119](https://rsshub.app/cpcaauto/news/bgzl/119) | [bgzl/122](https://rsshub.app/cpcaauto/news/bgzl/122) | [bgzl/120](https://rsshub.app/cpcaauto/news/bgzl/120) | [bgzl/121](https://rsshub.app/cpcaauto/news/bgzl/121) | [bgzl/124](https://rsshub.app/cpcaauto/news/bgzl/124) | +| [上海市场上牌数](http://cpcaauto.com/news.php?types=bgzl&anid=119) | [京城车市](http://cpcaauto.com/news.php?types=bgzl&anid=122) | [进口车市场分析](http://cpcaauto.com/news.php?types=bgzl&anid=120) | [二手车市场分析](http://cpcaauto.com/news.php?types=bgzl&anid=121) | [价格指数](http://cpcaauto.com/news.php?types=bgzl&anid=124) | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------- | +| [bgzl/119](https://rsshub.app/cpcaauto/news/bgzl/119) | [bgzl/122](https://rsshub.app/cpcaauto/news/bgzl/122) | [bgzl/120](https://rsshub.app/cpcaauto/news/bgzl/120) | [bgzl/121](https://rsshub.app/cpcaauto/news/bgzl/121) | [bgzl/124](https://rsshub.app/cpcaauto/news/bgzl/124) | - | [热点评述](http://cpcaauto.com/news.php?types=bgzl&anid=125) | [新能源月报](http://cpcaauto.com/news.php?types=bgzl&anid=126) | [商用车月报](http://cpcaauto.com/news.php?types=bgzl&anid=127) | [政策分析](http://cpcaauto.com/news.php?types=bgzl&anid=123) | - | ---------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ---------------------------------------------------------------- | - | [bgzl/125](https://rsshub.app/cpcaauto/news/bgzl/125) | [bgzl/126](https://rsshub.app/cpcaauto/news/bgzl/126) | [bgzl/127](https://rsshub.app/cpcaauto/news/bgzl/127) | [bgzl/123](https://rsshub.app/cpcaauto/news/bgzl/123) | +| [热点评述](http://cpcaauto.com/news.php?types=bgzl&anid=125) | [新能源月报](http://cpcaauto.com/news.php?types=bgzl&anid=126) | [商用车月报](http://cpcaauto.com/news.php?types=bgzl&anid=127) | [政策分析](http://cpcaauto.com/news.php?types=bgzl&anid=123) | +| ---------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ---------------------------------------------------------------- | +| [bgzl/125](https://rsshub.app/cpcaauto/news/bgzl/125) | [bgzl/126](https://rsshub.app/cpcaauto/news/bgzl/126) | [bgzl/127](https://rsshub.app/cpcaauto/news/bgzl/127) | [bgzl/123](https://rsshub.app/cpcaauto/news/bgzl/123) | - #### [经济与政策](http://cpcaauto.com/news.php?types=meeting) +#### [经济与政策](http://cpcaauto.com/news.php?types=meeting) - | [一周经济](http://cpcaauto.com/news.php?types=meeting&anid=46) | [一周政策](http://cpcaauto.com/news.php?types=meeting&anid=47) | - | ------------------------------------------------------------------ | ------------------------------------------------------------------ | - | [meeting/46](https://rsshub.app/cpcaauto/news/meeting/46) | [meeting/47](https://rsshub.app/cpcaauto/news/meeting/47) | +| [一周经济](http://cpcaauto.com/news.php?types=meeting&anid=46) | [一周政策](http://cpcaauto.com/news.php?types=meeting&anid=47) | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| [meeting/46](https://rsshub.app/cpcaauto/news/meeting/46) | [meeting/47](https://rsshub.app/cpcaauto/news/meeting/47) | - #### [乘联会论坛](http://cpcaauto.com/news.php?types=yjsy) +#### [乘联会论坛](http://cpcaauto.com/news.php?types=yjsy) - | [论坛文章](http://cpcaauto.com/news.php?types=yjsy&anid=49) | [两会](http://cpcaauto.com/news.php?types=yjsy&anid=111) | [车展看点](http://cpcaauto.com/news.php?types=yjsy&anid=113) | - | --------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- | - | [yjsy/49](https://rsshub.app/cpcaauto/news/yjsy/49) | [yjsy/111](https://rsshub.app/cpcaauto/news/yjsy/111) | [yjsy/113](https://rsshub.app/cpcaauto/news/yjsy/113) | +| [论坛文章](http://cpcaauto.com/news.php?types=yjsy&anid=49) | [两会](http://cpcaauto.com/news.php?types=yjsy&anid=111) | [车展看点](http://cpcaauto.com/news.php?types=yjsy&anid=113) | +| --------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- | +| [yjsy/49](https://rsshub.app/cpcaauto/news/yjsy/49) | [yjsy/111](https://rsshub.app/cpcaauto/news/yjsy/111) | [yjsy/113](https://rsshub.app/cpcaauto/news/yjsy/113) | `, categories: ['new-media'], @@ -143,112 +143,112 @@ export const route: Route = { }, { title: '行业新闻 - 国内乘用车', - source: ['cpcaauto.com/news.php?types=news&anid=10'], + source: ['cpcaauto.com/news.php'], target: '/news/news/10', }, { title: '行业新闻 - 进口及国外乘用车', - source: ['cpcaauto.com/news.php?types=news&anid=64'], + source: ['cpcaauto.com/news.php'], target: '/news/news/64', }, { title: '行业新闻 - 后市场', - source: ['cpcaauto.com/news.php?types=news&anid=44'], + source: ['cpcaauto.com/news.php'], target: '/news/news/44', }, { title: '行业新闻 - 商用车', - source: ['cpcaauto.com/news.php?types=news&anid=62'], + source: ['cpcaauto.com/news.php'], target: '/news/news/62', }, { title: '车市解读 - 周度', - source: ['cpcaauto.com/news.php?types=csjd&anid=128'], + source: ['cpcaauto.com/news.php'], target: '/news/csjd/128', }, { title: '车市解读 - 月度', - source: ['cpcaauto.com/news.php?types=csjd&anid=129'], + source: ['cpcaauto.com/news.php'], target: '/news/csjd/129', }, { title: '车市解读 - 指数', - source: ['cpcaauto.com/news.php?types=csjd&anid=130'], + source: ['cpcaauto.com/news.php'], target: '/news/csjd/130', }, { title: '车市解读 - 预测', - source: ['cpcaauto.com/news.php?types=csjd&anid=131'], + source: ['cpcaauto.com/news.php'], target: '/news/csjd/131', }, { title: '发布会报告 - 上海市场上牌数', - source: ['cpcaauto.com/news.php?types=bgzl&anid=119'], + source: ['cpcaauto.com/news.php'], target: '/news/bgzl/119', }, { title: '发布会报告 - 京城车市', - source: ['cpcaauto.com/news.php?types=bgzl&anid=122'], + source: ['cpcaauto.com/news.php'], target: '/news/bgzl/122', }, { title: '发布会报告 - 进口车市场分析', - source: ['cpcaauto.com/news.php?types=bgzl&anid=120'], + source: ['cpcaauto.com/news.php'], target: '/news/bgzl/120', }, { title: '发布会报告 - 二手车市场分析', - source: ['cpcaauto.com/news.php?types=bgzl&anid=121'], + source: ['cpcaauto.com/news.php'], target: '/news/bgzl/121', }, { title: '发布会报告 - 价格指数', - source: ['cpcaauto.com/news.php?types=bgzl&anid=124'], + source: ['cpcaauto.com/news.php'], target: '/news/bgzl/124', }, { title: '发布会报告 - 热点评述', - source: ['cpcaauto.com/news.php?types=bgzl&anid=125'], + source: ['cpcaauto.com/news.php'], target: '/news/bgzl/125', }, { title: '发布会报告 - 新能源月报', - source: ['cpcaauto.com/news.php?types=bgzl&anid=126'], + source: ['cpcaauto.com/news.php'], target: '/news/bgzl/126', }, { title: '发布会报告 - 商用车月报', - source: ['cpcaauto.com/news.php?types=bgzl&anid=127'], + source: ['cpcaauto.com/news.php'], target: '/news/bgzl/127', }, { title: '发布会报告 - 政策分析', - source: ['cpcaauto.com/news.php?types=bgzl&anid=123'], + source: ['cpcaauto.com/news.php'], target: '/news/bgzl/123', }, { title: '经济与政策 - 一周经济', - source: ['cpcaauto.com/news.php?types=meeting&anid=46'], + source: ['cpcaauto.com/news.php'], target: '/news/meeting/46', }, { title: '经济与政策 - 一周政策', - source: ['cpcaauto.com/news.php?types=meeting&anid=47'], + source: ['cpcaauto.com/news.php'], target: '/news/meeting/47', }, { title: '乘联会论坛 - 论坛文章', - source: ['cpcaauto.com/news.php?types=yjsy&anid=49'], + source: ['cpcaauto.com/news.php'], target: '/news/yjsy/49', }, { title: '乘联会论坛 - 两会', - source: ['cpcaauto.com/news.php?types=yjsy&anid=111'], + source: ['cpcaauto.com/news.php'], target: '/news/yjsy/111', }, { title: '乘联会论坛 - 车展看点', - source: ['cpcaauto.com/news.php?types=yjsy&anid=113'], + source: ['cpcaauto.com/news.php'], target: '/news/yjsy/113', }, ], diff --git a/lib/routes/cpcaauto/namespace.ts b/lib/routes/cpcaauto/namespace.ts index f6367802314c69..d91dd318a2da39 100644 --- a/lib/routes/cpcaauto/namespace.ts +++ b/lib/routes/cpcaauto/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: '中国汽车流通协会汽车市场研究分会', categories: ['new-media'], description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/cpcey/index.ts b/lib/routes/cpcey/index.ts index f739febf10462d..251fc0b5da4248 100644 --- a/lib/routes/cpcey/index.ts +++ b/lib/routes/cpcey/index.ts @@ -34,8 +34,8 @@ export const route: Route = { maintainers: ['Fatpandac'], handler, description: `| 新闻稿 | 消费资讯 | - | :----: | :------: | - | xwg | xfzx |`, +| :----: | :------: | +| xwg | xfzx |`, }; async function handler(ctx) { diff --git a/lib/routes/cpcey/namespace.ts b/lib/routes/cpcey/namespace.ts index 97a8ed0bafaa48..09597fd6ba5eea 100644 --- a/lib/routes/cpcey/namespace.ts +++ b/lib/routes/cpcey/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '台湾行政院消费者保护会', url: 'cpc.ey.gov.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/cpta/handler.ts b/lib/routes/cpta/handler.ts new file mode 100644 index 00000000000000..d5f34af89a4a7b --- /dev/null +++ b/lib/routes/cpta/handler.ts @@ -0,0 +1,136 @@ +import { DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import asyncPool from 'tiny-async-pool'; + +type NewsCategory = { + title: string; + baseUrl: string; + description: string; +}; + +const WEBSITE_URL = 'http://www.cpta.com.cn'; + +const NEWS_TYPES: Record = { + notice: { + title: '通知公告', + baseUrl: 'http://www.cpta.com.cn/notice.html', + description: '中国人事考试网 考试通知公告汇总', + }, + performance: { + title: '成绩公布', + baseUrl: 'http://www.cpta.com.cn/performance.html', + description: '中国人事考试网 考试成绩公布汇总', + }, +}; + +const handler: Route['handler'] = async (ctx) => { + const category = ctx.req.param('category'); + const BASE_URL = NEWS_TYPES[category].baseUrl; + // Fetch the index page + const { data: listResponse } = await got(BASE_URL); + const $ = load(listResponse); + + // Select all list items containing news information + const ITEM_SELECTOR = 'ul[class*="list_14"] > li:has(*)'; + const listItems = $(ITEM_SELECTOR); + + // Map through each list item to extract details + const contentLinkList = listItems + .toArray() + .map((element) => { + const title = $(element).find('a').attr('title')!; + const date = $(element).find('i').text()!.replaceAll(/[[\]]/g, ''); + const relativeLink = $(element).find('a').attr('href')!; + const absoluteLink = new URL(relativeLink, WEBSITE_URL).href; + return { + title, + date, + link: absoluteLink, + }; + }) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .slice(0, 10); + + const fetchDataItem = (item: { title: string; date: string; link: string }) => + cache.tryGet(item.link, async () => { + const CONTENT_SELECTOR = '#p_content'; + const { data: contentResponse } = await got(item.link); + const contentPage = load(contentResponse); + const content = contentPage(CONTENT_SELECTOR).html() || ''; + return { + title: item.title, + pubDate: item.date, + link: item.link, + description: content, + category: ['study'], + guid: item.link, + id: item.link, + image: 'https://www.gov.cn/images/gtrs_logo_lt.png', + content: { + html: content, + text: content, + }, + updated: item.date, + language: 'zh-CN', + } as DataItem; + }); + + const dataItems: DataItem[] = []; + + for await (const item of await asyncPool(1, contentLinkList, fetchDataItem)) { + dataItems.push(item as DataItem); + } + + return { + title: `中国人事考试网-${NEWS_TYPES[category].title}`, + description: NEWS_TYPES[category].description, + link: BASE_URL, + image: 'https://www.gov.cn/images/gtrs_logo_lt.png', + item: dataItems, + allowEmpty: true, + language: 'zh-CN', + feedLink: `https://rsshub.app/cpta/${category}`, + id: `https://rsshub.app/cpta/${category}`, + }; +}; + +export const route: Route = { + path: '/:category', + name: '中国人事考试网发布', + maintainers: ['PrinOrange'], + parameters: { + category: '栏目参数,可见下表描述。', + }, + description: ` +| Category | Title | Description | +|-------------|-----------|-------------------------------------| +| notice | 通知公告 | 中国人事考试网 考试通知公告汇总 | +| performance | 成绩公布 | 中国人事考试网 考试成绩公布汇总 | +`, + handler, + categories: ['study'], + features: { + requireConfig: false, + requirePuppeteer: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + supportRadar: true, + antiCrawler: true, + }, + radar: [ + { + title: '中国人事考试网通知公告', + source: ['www.cpta.com.cn/notice.html', 'www.cpta.com.cn'], + target: `/notice`, + }, + { + title: '中国人事考试网成绩发布', + source: ['www.cpta.com.cn/performance.html', 'www.cpta.com.cn'], + target: `/performance`, + }, + ], + example: '/cpta/notice', +}; diff --git a/lib/routes/cpta/namespace.ts b/lib/routes/cpta/namespace.ts new file mode 100644 index 00000000000000..a0b0f00132143f --- /dev/null +++ b/lib/routes/cpta/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国人事考试网', + url: 'www.cpta.com.cn', +}; diff --git a/lib/routes/cpuid/namespace.ts b/lib/routes/cpuid/namespace.ts index 34cb36eb27dc61..edec8166320d5b 100644 --- a/lib/routes/cpuid/namespace.ts +++ b/lib/routes/cpuid/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'CPUID', url: 'cpuid.com', + lang: 'en', }; diff --git a/lib/routes/cqgas/namespace.ts b/lib/routes/cqgas/namespace.ts index 4a38128d4dfd26..65f74fae4b20a3 100644 --- a/lib/routes/cqgas/namespace.ts +++ b/lib/routes/cqgas/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '重庆燃气', url: 'cqgas.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/cqwu/index.ts b/lib/routes/cqwu/index.ts index b5504871221191..9b1123fb15b485 100644 --- a/lib/routes/cqwu/index.ts +++ b/lib/routes/cqwu/index.ts @@ -31,8 +31,8 @@ export const route: Route = { maintainers: ['Fatpandac'], handler, description: `| 通知公告 | 学术活动公告 | - | -------- | ------------ | - | notify | academiceve |`, +| -------- | ------------ | +| notify | academiceve |`, }; async function handler(ctx) { diff --git a/lib/routes/cqwu/namespace.ts b/lib/routes/cqwu/namespace.ts index 52f7bfdf57bf08..5bfa1a98a756f2 100644 --- a/lib/routes/cqwu/namespace.ts +++ b/lib/routes/cqwu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '重庆文理学院', url: 'www.cqwu.net', + lang: 'zh-CN', }; diff --git a/lib/routes/crac/index.ts b/lib/routes/crac/index.ts index 3120f4e3c12c68..444d9e30f2ba17 100644 --- a/lib/routes/crac/index.ts +++ b/lib/routes/crac/index.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['Misaka13514'], handler, description: `| 新闻动态 | 通知公告 | 政策法规 | 常见问题 | 资料下载 | English | 业余中继台 | 科普专栏 | - | -------- | -------- | -------- | -------- | -------- | ------- | ---------- | -------- | - | 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 |`, +| -------- | -------- | -------- | -------- | -------- | ------- | ---------- | -------- | +| 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 |`, }; async function handler(ctx) { diff --git a/lib/routes/crac/namespace.ts b/lib/routes/crac/namespace.ts index fd9bb283b50e91..6150c24204ce56 100644 --- a/lib/routes/crac/namespace.ts +++ b/lib/routes/crac/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国无线电协会业余无线电分会', url: 'www.crac.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/creative-comic/book.ts b/lib/routes/creative-comic/book.ts index e5e0359f2e8633..5edb198a5dd780 100644 --- a/lib/routes/creative-comic/book.ts +++ b/lib/routes/creative-comic/book.ts @@ -6,7 +6,7 @@ import cache from '@/utils/cache'; import { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; import path from 'node:path'; -import { getUuid, getBook, getChapter, getChapters, getImgEncrypted, getImgKey, decrypt, getRealKey, siteHost } from './utils'; +import { getUuid, getBook, getChapter, getChapters, getImgEncrypted, getImgKey, decrypt, getRealKey, apiHost } from './utils'; export const route: Route = { path: '/book/:id/:coverOnly?/:quality?', @@ -62,7 +62,7 @@ async function handler(ctx) { const realKey = getRealKey(imgKey); const encrypted = await getImgEncrypted(p.id, quality); - return cache.tryGet(`${siteHost}/fs/chapter_content/encrypt/${p.id}/${quality}`, () => decrypt(encrypted, realKey)); + return cache.tryGet(`${apiHost}/fs/chapter_content/encrypt/${p.id}/${quality}`, () => decrypt(encrypted, realKey)); }) ); } diff --git a/lib/routes/creative-comic/namespace.ts b/lib/routes/creative-comic/namespace.ts index e50247a26e739c..e7a32068df1107 100644 --- a/lib/routes/creative-comic/namespace.ts +++ b/lib/routes/creative-comic/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'CCC 創作集', url: 'creative-comic.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/creative-comic/utils.ts b/lib/routes/creative-comic/utils.ts index dba027f1e91938..14558977dc9e15 100644 --- a/lib/routes/creative-comic/utils.ts +++ b/lib/routes/creative-comic/utils.ts @@ -82,4 +82,4 @@ const getRealKey = (imgKey, token = DEFAULT_TOKEN) => { }; }; -export { getBook, getChapter, getChapters, getImgEncrypted, getImgKey, getUuid, decrypt, token2Key, getRealKey }; +export { apiHost, getBook, getChapter, getChapters, getImgEncrypted, getImgKey, getUuid, decrypt, token2Key, getRealKey }; diff --git a/lib/routes/crossbell/namespace.ts b/lib/routes/crossbell/namespace.ts index aad3ef39ab0ac1..3b6da44f1f8829 100644 --- a/lib/routes/crossbell/namespace.ts +++ b/lib/routes/crossbell/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Crossbell', url: 'crossbell.io', + lang: 'en', }; diff --git a/lib/routes/cryptoslate/index.ts b/lib/routes/cryptoslate/index.ts new file mode 100644 index 00000000000000..978411c3353e96 --- /dev/null +++ b/lib/routes/cryptoslate/index.ts @@ -0,0 +1,98 @@ +import { Route, Data } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import logger from '@/utils/logger'; +import parser from '@/utils/rss-parser'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/', + categories: ['finance'], + example: '/cryptoslate', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'News', + maintainers: ['pseudoyu'], + handler, + radar: [ + { + source: ['cryptoslate.com/'], + target: '/', + }, + ], + description: 'Get latest news from CryptoSlate.', +}; + +async function handler(ctx): Promise { + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20; + const rssUrl = 'https://cryptoslate.com/feed/'; + + const feed = await parser.parseURL(rssUrl); + + const items = feed.items + .filter((item) => !item.link?.includes('/feed') && !item.link?.includes('#respond')) + .slice(0, limit) + .map((item) => { + if (!item.link) { + return {}; + } + + try { + // Clean URL by removing query parameters + const cleanUrl = item.link.split('?')[0]; + + return { + title: item.title || 'Untitled', + link: cleanUrl, + pubDate: item.pubDate ? parseDate(item.pubDate) : undefined, + description: extractFullTextFromRSS(item), + author: item.creator || 'CryptoSlate', + category: item.categories || [], + guid: item.guid || item.link, + image: item.enclosure?.url, + }; + } catch (error: any) { + logger.warn(`Couldn't process article from CryptoSlate: ${item.link}: ${error.message}`); + return {}; + } + }); + + // Filter out empty items + const filteredItems = items.filter((item) => item && Object.keys(item).length > 0); + + return { + title: feed.title || 'CryptoSlate', + link: feed.link || 'https://cryptoslate.com', + description: feed.description || 'Latest news from CryptoSlate', + item: filteredItems, + language: feed.language || 'en', + image: feed.image?.url, + } as Data; +} + +function extractFullTextFromRSS(entry: any): string | null { + try { + const contentEncoded = entry['content:encoded'] || entry['content:encodedSnippet'] || entry.content || entry.contentSnippet; + + if (!contentEncoded) { + return null; + } + + const $ = load(contentEncoded); + + // Remove unwanted elements + $('img').remove(); + $('figure').remove(); + + return $.html() || null; + } catch (error) { + logger.error(`Error extracting full text from RSS: ${error}`); + return null; + } +} diff --git a/lib/routes/cryptoslate/namespace.ts b/lib/routes/cryptoslate/namespace.ts new file mode 100644 index 00000000000000..1db76ea238ddc2 --- /dev/null +++ b/lib/routes/cryptoslate/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'CryptoSlate', + url: 'cryptoslate.com', + lang: 'en', +}; diff --git a/lib/routes/cs/index.ts b/lib/routes/cs/index.ts index b4446df9efc8ca..bcde4c99b1968d 100644 --- a/lib/routes/cs/index.ts +++ b/lib/routes/cs/index.ts @@ -19,52 +19,52 @@ export const route: Route = { parameters: { category: '分类,见下表,默认为首页' }, maintainers: ['nczitzk'], description: `| 要闻 | 公司 | 市场 | 基金 | - | ---- | ---- | ---- | ---- | - | xwzx | ssgs | gppd | tzjj | +| ---- | ---- | ---- | ---- | +| xwzx | ssgs | gppd | tzjj | - | 科创 | 产经 | 期货 | 海外 | - | ---- | ------ | -------- | ------ | - | 5g | cj2020 | zzqh2020 | hw2020 | +| 科创 | 产经 | 期货 | 海外 | +| ---- | ------ | -------- | ------ | +| 5g | cj2020 | zzqh2020 | hw2020 | -
    - 更多栏目 +
    +更多栏目 - #### 要闻 +#### 要闻 - | 财经要闻 | 观点评论 | 民生消费 | - | -------- | -------- | --------- | - | 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 | -
    `, +| 中证快讯 7x24 | IPO 鉴真 | 公司能见度 | +| ------------- | -------- | ---------- | +| sylm/jsbd | yc/ipojz | yc/gsnjd | +
    `, handler, }; diff --git a/lib/routes/cs/namespace.ts b/lib/routes/cs/namespace.ts index 3a5d2464c68351..67c12aadd2cf31 100644 --- a/lib/routes/cs/namespace.ts +++ b/lib/routes/cs/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: '中证网', url: 'cs.com.cn', categories: ['finance'], + lang: 'zh-CN', }; diff --git a/lib/routes/cs/video.ts b/lib/routes/cs/video.ts index b45cab9cfccf78..090f665610a24c 100644 --- a/lib/routes/cs/video.ts +++ b/lib/routes/cs/video.ts @@ -19,7 +19,7 @@ export const route: Route = { }, name: '中证视频', description: `| 今日聚焦 | 传闻求证 | 高端访谈 | 投教课堂 | 直播汇 | - | -------- | -------- | -------- | -------- | ------ |`, +| -------- | -------- | -------- | -------- | ------ |`, maintainers: ['nczitzk'], handler, }; diff --git a/lib/routes/cs/zzkx.ts b/lib/routes/cs/zzkx.ts index 0786a25591907a..783ce264b52589 100644 --- a/lib/routes/cs/zzkx.ts +++ b/lib/routes/cs/zzkx.ts @@ -10,5 +10,5 @@ function handler(ctx) { // https://www.cs.com.cn/sylm/jsbd/ const redirectTo = '/cs/sylm/jsbd'; - ctx.redirect(redirectTo); + ctx.set('redirect', redirectTo); } diff --git a/lib/routes/csdn/blog.ts b/lib/routes/csdn/blog.ts index 4b1997e756eb39..3f5fd52cc76c08 100644 --- a/lib/routes/csdn/blog.ts +++ b/lib/routes/csdn/blog.ts @@ -23,35 +23,39 @@ export const route: Route = { }, ], name: 'User Feed', - maintainers: [], + maintainers: ['Jkker'], handler, }; async function handler(ctx) { const user = ctx.req.param('user'); - const rootUrl = 'https://blog.csdn.net'; + const rootUrl = 'https://rss.csdn.net'; const blogUrl = `${rootUrl}/${user}`; - const rssUrl = blogUrl + '/rss/list'; + const rssUrl = blogUrl + '/rss/map'; const feed = await rssParser.parseURL(rssUrl); const items = await Promise.all( feed.items.map((item) => cache.tryGet(item.link, async () => { - const response = await got({ - method: 'get', - url: item.link, - }); + try { + const response = await got({ + method: 'get', + url: item.link, + }); - const $ = load(response.data); + const $ = load(response.data); - const description = $('#content_views').html(); + const description = $('#content_views').html(); - return { - ...item, - description, - }; + return { + ...item, + description, + }; + } catch { + return item; + } }) ) ); diff --git a/lib/routes/csdn/namespace.ts b/lib/routes/csdn/namespace.ts index 00752316d6e8b5..e1c9c0436055ae 100644 --- a/lib/routes/csdn/namespace.ts +++ b/lib/routes/csdn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'CSDN', url: 'blog.csdn.net', + lang: 'zh-CN', }; diff --git a/lib/routes/cssn/namespace.ts b/lib/routes/cssn/namespace.ts index 62e345b2895a34..dd902e8114f45a 100644 --- a/lib/routes/cssn/namespace.ts +++ b/lib/routes/cssn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Chinese Social Science Net', url: 'iolaw.cssn.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/cste/index.ts b/lib/routes/cste/index.ts index f1dc4882e977cd..2ba81cbb238d7a 100644 --- a/lib/routes/cste/index.ts +++ b/lib/routes/cste/index.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 通知公告 | 学会新闻 | 科协简讯 | 学科动态 | 往事钩沉 | - | -------- | -------- | -------- | -------- | -------- | - | 16 | 18 | 19 | 20 | 21 |`, +| -------- | -------- | -------- | -------- | -------- | +| 16 | 18 | 19 | 20 | 21 |`, }; async function handler(ctx) { diff --git a/lib/routes/cste/namespace.ts b/lib/routes/cste/namespace.ts index 48521384c5e87c..841fe827dc3a34 100644 --- a/lib/routes/cste/namespace.ts +++ b/lib/routes/cste/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/csu/cse.ts b/lib/routes/csu/cse.ts index 9876514eabf6a0..287ff19c061827 100644 --- a/lib/routes/csu/cse.ts +++ b/lib/routes/csu/cse.ts @@ -31,8 +31,8 @@ export const route: Route = { maintainers: ['j1g5awi'], handler, description: `| 类型 | 学院新闻 | 通知公告 | 学术信息 | 学工动态 | 科研动态 | - | ---- | -------- | -------- | -------- | -------- | -------- | - | 参数 | xyxw | tzgg | xsxx | xgdt | kydt |`, +| ---- | -------- | -------- | -------- | -------- | -------- | +| 参数 | xyxw | tzgg | xsxx | xgdt | kydt |`, }; async function handler(ctx) { diff --git a/lib/routes/csu/mail.ts b/lib/routes/csu/mail.ts index f8aeaf5628579d..0beb31e3296d88 100644 --- a/lib/routes/csu/mail.ts +++ b/lib/routes/csu/mail.ts @@ -28,8 +28,8 @@ export const route: Route = { maintainers: ['j1g5awi'], handler, description: `| 类型 | 校长信箱 | 党委信箱 | - | ---- | -------- | -------- | - | 参数 | 01 | 02 |`, +| ---- | -------- | -------- | +| 参数 | 01 | 02 |`, }; async function handler(ctx) { diff --git a/lib/routes/csu/namespace.ts b/lib/routes/csu/namespace.ts index 7e5fee5d31eddb..f50da37e280210 100644 --- a/lib/routes/csu/namespace.ts +++ b/lib/routes/csu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中南大学', url: 'career.csu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ctbu/namespace.ts b/lib/routes/ctbu/namespace.ts new file mode 100644 index 00000000000000..f0e5557592afc3 --- /dev/null +++ b/lib/routes/ctbu/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '重庆工商大学', + url: 'www.ctbu.edu.cn/', + lang: 'zh-CN', +}; diff --git a/lib/routes/ctbu/xxgg.ts b/lib/routes/ctbu/xxgg.ts new file mode 100644 index 00000000000000..c651f29eed37f1 --- /dev/null +++ b/lib/routes/ctbu/xxgg.ts @@ -0,0 +1,50 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +const baseURL = 'https://www.ctbu.edu.cn/index/xxgg.htm'; + +export const route: Route = { + path: '/xxgg', + categories: ['university'], + example: '/ctbu/xxgg', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.ctbu.edu.cn/', 'www.ctbu.edu.cn/index/xxgg.htm'], + }, + ], + name: '学校公告', + maintainers: ['Skylwn'], + handler, + url: 'www.ctbu.edu.cn/', +}; + +async function handler() { + const response = await got(baseURL); + const $ = load(response.data); + const items = $('li.clearfix') + .toArray() + .map((item) => { + item = $(item); + return { + title: item.find('a').attr('title'), + description: item.find('p').text(), + pubDate: parseDate(item.find('h6').text() + '-' + item.find('em').text(), 'YYYY-MM-DD'), + link: item.find('a').attr('href'), + }; + }); + return { + title: $('title').text(), + link: baseURL, + item: items, + }; +} diff --git a/lib/routes/cts/namespace.ts b/lib/routes/cts/namespace.ts index a71fd7cd376304..6b390eb4793053 100644 --- a/lib/routes/cts/namespace.ts +++ b/lib/routes/cts/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '華視', url: 'news.cts.com.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/cts/news.ts b/lib/routes/cts/news.ts index 55101873cb330c..c06f49020335c7 100644 --- a/lib/routes/cts/news.ts +++ b/lib/routes/cts/news.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['miles170'], handler, description: `| 即時 | 氣象 | 政治 | 國際 | 社會 | 運動 | 生活 | 財經 | 台語 | 地方 | 產業 | 綜合 | 藝文 | 娛樂 | - | ---- | ------- | -------- | ------------- | ------- | ------ | ---- | ----- | --------- | ----- | ---- | ------- | ---- | --------- | - | real | weather | politics | international | society | sports | life | money | taiwanese | local | pr | general | arts | entertain |`, +| ---- | ------- | -------- | ------------- | ------- | ------ | ---- | ----- | --------- | ----- | ---- | ------- | ---- | --------- | +| real | weather | politics | international | society | sports | life | money | taiwanese | local | pr | general | arts | entertain |`, }; async function handler(ctx) { diff --git a/lib/routes/cuc/namespace.ts b/lib/routes/cuc/namespace.ts index 114659b0e3d34c..e552a97aee8685 100644 --- a/lib/routes/cuc/namespace.ts +++ b/lib/routes/cuc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国传媒大学', url: 'yz.cuc.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/cuilingmag/index.ts b/lib/routes/cuilingmag/index.ts new file mode 100644 index 00000000000000..8434b235852cf0 --- /dev/null +++ b/lib/routes/cuilingmag/index.ts @@ -0,0 +1,211 @@ +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'; + +export const handler = async (ctx) => { + const { category } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 12; + + const rootUrl = 'https://www.cuilingmag.com'; + const currentUrl = new URL(category ? `category/${category}` : '', rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('div.new-list-div, div.item') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const title = item.find('h3.new-list-h3, h3.title-font').first().text().trim(); + + const src = item.find('img').first().prop('src'); + const image = src ? new URL(src, rootUrl).href : undefined; + + const description = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + }); + + return { + title, + description, + link: new URL(item.find('a').first().prop('href'), rootUrl).href, + author: item.find('a.new-list-p, div.author').text().trim(), + image, + banner: image, + language, + enclosure_url: image, + enclosure_type: image ? `image/${image.split(/\./).pop()}` : undefined, + enclosure_title: title, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const title = `${$$('p.title-font').text().trim()} ${$$('p.subtitle-font').text().trim()}`; + + const src = $$('div.banner img').first().prop('src'); + const banner = src ? new URL(src, rootUrl).href : undefined; + + const description = + item.description + + art(path.join(__dirname, 'templates/description.art'), { + images: banner + ? [ + { + src: banner, + alt: title, + }, + ] + : undefined, + description: $$('div.article-content').html(), + }); + + item.title = title; + item.description = description; + item.pubDate = parseDate($$('p.time').first().text()); + item.category = [ + ...new Set([ + ...$$('p.sort a') + .toArray() + .map((c) => $$(c).text().trim()), + ...$$('span.type') + .toArray() + .map((c) => $$(c).text().trim()), + ]), + ].filter(Boolean); + item.author = $$('p.author a') + .toArray() + .map((a) => $$(a).contents().first().text().trim()) + .join('/'); + item.content = { + html: description, + text: $$('div.article-content').text(), + }; + item.banner = banner; + item.language = language; + item.enclosure_url = banner ?? item.enclosure_url; + item.enclosure_type = banner ? `image/${banner.split(/\./).pop()}` : item.enclosure_type; + item.enclosure_title = title; + + return item; + }) + ) + ); + + const title = $('title').text().trim(); + const image = new URL($('div.nav-logo a img').prop('src'), rootUrl).href; + + return { + title, + description: $('meta[property="og:description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: title.split(/-/).pop(), + language, + }; +}; + +export const route: Route = { + path: '/:category?', + name: '分类', + url: 'cuilingmag.com', + categories: ['new-media', 'popular'], + maintainers: ['nczitzk'], + handler, + example: '/cuilingmag', + parameters: { category: '分类,默认为空,即全部,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [#哲学·文明](https://www.cuilingmag.com/category/philosophy_civilization),网址为 \`https://www.cuilingmag.com/category/philosophy_civilization\`。截取 \`https://www.cuilingmag.com/category\` 到末尾的部分 \`philosophy_civilization\` 作为参数填入,此时路由为 [\`/cuilingmag/philosophy_civilization\`](https://rsshub.app/cuilingmag/philosophy_civilization)。 +::: + +| 分类 | ID | +| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| [哲学 · 文明](https://www.cuilingmag.com/category/philosophy_civilization) | [philosophy_civilization](https://rsshub.app/cuilingmag/philosophy_civilization) | +| [艺术 · 科技](https://www.cuilingmag.com/category/art_science) | [art_science](https://rsshub.app/cuilingmag/art_science) | +| [未来 · 生命](https://www.cuilingmag.com/category/future_life) | [future_life](https://rsshub.app/cuilingmag/future_life) | +| [行星智慧](https://www.cuilingmag.com/category/planetary_wisdom) | [planetary_wisdom](https://rsshub.app/cuilingmag/planetary_wisdom) | +| [数字治理](https://www.cuilingmag.com/category/digital_governance) | [digital_governance](https://rsshub.app/cuilingmag/digital_governance) | +| [Noema精选](https://www.cuilingmag.com/category/selected_noema) | [selected_noema](https://rsshub.app/cuilingmag/selected_noema) | + `, + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['cuilingmag.com/category/:category'], + target: (params) => { + const category = params.category; + + return `/cuilingmag${category ? `/${category}` : ''}`; + }, + }, + { + title: '全部', + source: ['cuilingmag.com'], + target: '/', + }, + { + title: '哲学 · 文明', + source: ['cuilingmag.com/category/philosophy_civilization'], + target: '/philosophy_civilization', + }, + { + title: '艺术 · 科技', + source: ['cuilingmag.com/category/art_science'], + target: '/art_science', + }, + { + title: '未来 · 生命', + source: ['cuilingmag.com/category/future_life'], + target: '/future_life', + }, + { + title: '行星智慧', + source: ['cuilingmag.com/category/planetary_wisdom'], + target: '/planetary_wisdom', + }, + { + title: '数字治理', + source: ['cuilingmag.com/category/digital_governance'], + target: '/digital_governance', + }, + { + title: 'Noema精选', + source: ['cuilingmag.com/category/selected_noema'], + target: '/selected_noema', + }, + ], +}; diff --git a/lib/routes/cuilingmag/namespace.ts b/lib/routes/cuilingmag/namespace.ts new file mode 100644 index 00000000000000..30740a1b5108e3 --- /dev/null +++ b/lib/routes/cuilingmag/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '萃嶺网', + url: 'cuilingmag.com', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/cuilingmag/templates/description.art b/lib/routes/cuilingmag/templates/description.art new file mode 100644 index 00000000000000..dfab19230c1108 --- /dev/null +++ b/lib/routes/cuilingmag/templates/description.art @@ -0,0 +1,17 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
    + {{ image.alt }} +
    + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/cupl/jwc.ts b/lib/routes/cupl/jwc.ts new file mode 100644 index 00000000000000..45dc33cb9799aa --- /dev/null +++ b/lib/routes/cupl/jwc.ts @@ -0,0 +1,69 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; +// import cache from '@/utils/cache'; + +export const route: Route = { + path: '/jwc', + url: 'jwc.cupl.edu.cn/index/tzgg.htm', + categories: ['university'], + example: '/cupl/jwc', + description: '中国政法大学教务处通知公告', + name: '教务处通知公告', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['jwc.cupl.edu.cn/index/tzgg.htm', 'jwc.cupl.edu.cn/'], + target: '/jwc', + }, + ], + maintainers: ['Fgju'], + handler: async (/* ctx*/) => { + const host = 'https://jwc.cupl.edu.cn/'; + const response = await ofetch(host + 'index/tzgg.htm'); + const $ = load(response); + + const list = $('li[id^=line_u8_]') + .toArray() + .map((elem) => { + const elem_ = $(elem); + const a = elem_.find('a'); + return { + link: a[1].attribs.href, + title: $(a[1]).text(), + pubDate: parseDate(elem_.find('span').text(), 'YYYY-MM-DD'), + category: $(a[0]).text().slice(0, -1), + description: '', + }; + }); + /* + const items = await Promise.all( + list.map((item) => { + cache.tryGet(item.link, async () => { + const response = await ofetch(host + item.link.slice(3)); + const $ = load(response); + const content = $('.form[name=_newscontent_fromname]').html(); + item.description = content ?? ''; + return item; + } + ) + }) + ); + */ + return { + title: '通知公告', + link: 'https://jwc.cupl.edu.cn/index/tzgg.htm', + description: '中国政法大学教务处通知公告', + language: 'zh-CN', + item: list, + }; + }, +}; diff --git a/lib/routes/cupl/namespace.ts b/lib/routes/cupl/namespace.ts new file mode 100644 index 00000000000000..be4aa27fbe6d21 --- /dev/null +++ b/lib/routes/cupl/namespace.ts @@ -0,0 +1,20 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'CUPL', + url: 'jwc.cupl.edu.cn/index/tzgg.htm', + description: 'China University of Political Science and Law Academic Affairs Office Notices', + + zh: { + name: '中国政法大学', + description: '中国政法大学教务处通知公告', + }, + 'zh-TW': { + name: '中國政法大學', + description: '中國政法大學教務處通知公告', + }, + ja: { + name: '中国政法大学', + description: '中国政法大学教務処通知公告', + }, +}; diff --git a/lib/routes/curiouscat/namespace.ts b/lib/routes/curiouscat/namespace.ts index a5c187bff84cfd..7eba48fa7fdc53 100644 --- a/lib/routes/curiouscat/namespace.ts +++ b/lib/routes/curiouscat/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'CuriousCat', url: 'curiouscat.live', + lang: 'en', }; diff --git a/lib/routes/curius/namespace.ts b/lib/routes/curius/namespace.ts index 3c782f89394967..c5bb7fb3115445 100644 --- a/lib/routes/curius/namespace.ts +++ b/lib/routes/curius/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Curius', url: 'curius.app', + lang: 'en', }; diff --git a/lib/routes/cursor/changelog.ts b/lib/routes/cursor/changelog.ts new file mode 100644 index 00000000000000..fb6dc40f5f5d62 --- /dev/null +++ b/lib/routes/cursor/changelog.ts @@ -0,0 +1,106 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +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'; + +export const handler = async (ctx: Context): Promise => { + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '100', 10); + + const baseUrl: string = 'https://www.cursor.com'; + const targetUrl: string = new URL('changelog', baseUrl).href; + + const response = await ofetch(targetUrl, { + headers: { + cookie: 'NEXT_LOCALE=en', + }, + }); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'en'; + + const items: DataItem[] = $('article.relative') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio = $(el); + + const version: string = $el.find('div.items-center p').first().text(); + + const title: string = `[${version}] ${$el + .find(String.raw`h2 a.hover\:underline`) + .contents() + .first() + .text()}`; + const pubDateStr: string | undefined = $el.find('div.inline-flex p').first().text().trim(); + const linkUrl: string | undefined = $el.find(String.raw`h2 a.hover\:underline`).attr('href'); + const guid: string = `cursor-changelog-${version}`; + const upDatedStr: string | undefined = pubDateStr; + + const $h2El = $el.find('h2').first(); + + if ($h2El.length) { + $h2El.prevAll().remove(); + $h2El.remove(); + } + + const description: string = $el.html() || ''; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + guid, + id: guid, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }) + .filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[property="og:description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('meta[property="og:image"]').attr('content'), + language, + }; +}; + +export const route: Route = { + path: '/changelog', + name: 'Changelog', + url: 'www.cursor.com', + maintainers: ['p3psi-boo', 'nczitzk'], + handler, + example: '/cursor/changelog', + parameters: undefined, + description: undefined, + categories: ['program-update'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.cursor.com/changelog'], + target: '/changelog', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/cursor/namespace.ts b/lib/routes/cursor/namespace.ts new file mode 100644 index 00000000000000..a8b7b6abdcda4c --- /dev/null +++ b/lib/routes/cursor/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Cursor', + url: 'www.cursor.com', + description: '', + lang: 'en', +}; diff --git a/lib/routes/cw/master.ts b/lib/routes/cw/master.ts index 3fcc3e36035e37..aa34fb7b45d774 100644 --- a/lib/routes/cw/master.ts +++ b/lib/routes/cw/master.ts @@ -19,20 +19,20 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| 主頻道名稱 | 主頻道 ID | - | ---------- | --------- | - | 財經 | 8 | - | 產業 | 7 | - | 國際 | 9 | - | 管理 | 10 | - | 環境 | 12 | - | 教育 | 13 | - | 人物 | 14 | - | 政治社會 | 77 | - | 調查排行 | 15 | - | 健康關係 | 79 | - | 時尚品味 | 11 | - | 運動生活 | 103 | - | 重磅外媒 | 16 |`, +| ---------- | --------- | +| 財經 | 8 | +| 產業 | 7 | +| 國際 | 9 | +| 管理 | 10 | +| 環境 | 12 | +| 教育 | 13 | +| 人物 | 14 | +| 政治社會 | 77 | +| 調查排行 | 15 | +| 健康關係 | 79 | +| 時尚品味 | 11 | +| 運動生活 | 103 | +| 重磅外媒 | 16 |`, }; async function handler(ctx) { diff --git a/lib/routes/cw/namespace.ts b/lib/routes/cw/namespace.ts index 5ebefe35d267b1..3ddd127118ea26 100644 --- a/lib/routes/cw/namespace.ts +++ b/lib/routes/cw/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '天下雜誌', url: 'cw.com.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/cw/utils.ts b/lib/routes/cw/utils.ts index 687ac4bbb98f4b..04b42d898f83b9 100644 --- a/lib/routes/cw/utils.ts +++ b/lib/routes/cw/utils.ts @@ -61,6 +61,7 @@ const parsePage = async (path, browser, ctx) => { waitUntil: 'domcontentloaded', }); + await page.waitForSelector('.caption'); const response = await page.evaluate(() => document.documentElement.innerHTML); await page.close(); const $ = load(response); @@ -98,6 +99,7 @@ const parseItems = (list, browser, tryGet) => await page.goto(item.link, { waitUntil: 'domcontentloaded', }); + await page.waitForSelector('.article__head .container'); const response = await page.evaluate(() => document.documentElement.innerHTML); await page.close(); diff --git a/lib/routes/cybersecurityventures/namespace.ts b/lib/routes/cybersecurityventures/namespace.ts new file mode 100644 index 00000000000000..f960a613b2b7c6 --- /dev/null +++ b/lib/routes/cybersecurityventures/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Cybercrime Magazine', + url: 'cybersecurityventures.com', + lang: 'en', +}; diff --git a/lib/routes/cybersecurityventures/news.ts b/lib/routes/cybersecurityventures/news.ts new file mode 100644 index 00000000000000..5eeaaed9bb3155 --- /dev/null +++ b/lib/routes/cybersecurityventures/news.ts @@ -0,0 +1,121 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; +import ofetch from '@/utils/ofetch'; +import type { Context } from 'hono'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import type { RawRecord } from './types'; + +const categories: Record< + string, + { + label: string; + scene: number; + view: number; + } +> = { + today: { + label: "Today's News", + scene: 12, + view: 14, + }, + 'intrusion-daily-cyber-threat-alert': { + label: 'Cyberattacks', + scene: 13, + view: 15, + }, + 'ransomware-minute': { + label: 'Ransomware', + scene: 16, + view: 18, + }, + cryptocrime: { + label: 'Cryptocrime', + scene: 18, + view: 20, + }, + 'hack-blotter': { + label: 'Hack Blotter', + scene: 19, + view: 21, + }, + 'cybersecurity-venture-capital-vc-deals': { + label: 'VC Deal Flow', + scene: 3, + view: 3, + }, + 'mergers-and-acquisitions-report': { + label: 'M&A Tracker', + scene: 11, + view: 13, + }, +}; + +export const route: Route = { + name: 'News', + categories: ['programming'], + path: '/news/:category?', + example: '/cybersecurityventures/news', + radar: Object.keys(categories).map((key) => ({ + source: [`cybersecurityventures.com/${key}`], + target: `/news/${key}`, + title: categories[key].label, + })), + parameters: { + category: { + description: 'news category', + default: 'today', + options: Object.keys(categories).map((key) => ({ + value: key, + label: categories[key].label, + })), + }, + }, + handler, + maintainers: ['KarasuShin'], + features: { + supportRadar: true, + }, + view: ViewType.Articles, +}; + +async function handler(ctx: Context): Promise { + const rootUrl = 'https://cybersecurityventures.com/'; + const apiUrl = 'https://us-east-1-renderer-read.knack.com/v1'; + const category = ctx.req.param('category') ?? 'today'; + const limit = ctx.req.query('limit') ?? 20; + + if (!(category in categories)) { + throw new InvalidParameterError('Invalid category'); + } + + const { scene, view, label } = categories[category]; + + const data = await ofetch<{ + records: RawRecord[]; + }>(`${apiUrl}/scenes/scene_${scene}/views/view_${view}/records?format=raw&page=1&rows_per_page=${limit}&sort_field=field_2&sort_order=desc`, { + headers: { + 'X-Knack-Application-Id': '6013171b60be8f001cb27363', + 'X-Knack-Rest-Api-Key': 'renderer', + }, + }); + + return { + title: `${label} - Cybercrime Magazine`, + link: `${rootUrl}/${category}`, + item: data.records.map((item) => { + const $ = load(item.field_3, null, false); + const link = $('a').attr('href'); + const source = item.field_4; + const description = `

    ${source}


    ${$.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.featuredImage.alt}` + 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 = '

    '; + if (data.contents && Array.isArray(data.contents)) { + html += data.contents.map((data) => extractArticleContent(data)).join(''); + } + html += '

    '; + return html; +} + +function handleText(data) { + let content = data.text?.content || ''; + if (data.text?.bold || data.text?.highlight) { + content = `${content}`; + } + return content; +} + +function handleImage(data) { + return data.image?.src ? `${data.image.alt || ''}` : ''; +} + +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').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 = /
  • .*<\/li>/g; + const item = response.match(reg).map((line) => { + const $ = load(line); + const title = $('a'); + return { + title: `宁波市国资委-${noticeCate}:${title.text()}`, + link: `http://gzw.ningbo.gov.cn${title.attr('href')}`, + pubDate: parseDate($('p').text().replaceAll(/\[|]/g, '')), + author: '宁波市国资委', + description: title.text(), + }; + }); + return { + title: '宁波市国资委', + link: url, + item, + }; + }, +}; diff --git a/lib/routes/gov/zj/ningborsjnotice.ts b/lib/routes/gov/zj/ningborsjnotice.ts new file mode 100644 index 00000000000000..5f3d9202aa0a55 --- /dev/null +++ b/lib/routes/gov/zj/ningborsjnotice.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/ningborsjnotice/:colId?', + categories: ['government'], + example: '/gov/zj/ningborsjnotice/1229676740', + parameters: { + colId: '公告分类id、详细信息点击源网站http://rsj.ningbo.gov.cn/请求中寻找', + }, + radar: [ + { + source: ['rsj.ningbo.gov.cn/col/col1229676740/index.html'], + target: '/zj/ningborsjnotice/:colId?', + }, + ], + name: '宁波市人力资源和社会保障局-公告', + url: 'rsj.ningbo.gov.cn', + maintainers: ['HaoyuLee'], + description: ` +| 公告类别 | colId | +| ------------ | -- | +| 事业单位进人公告 | 1229676740 | + `, + async handler(ctx) { + const { colId = '1229676740' } = ctx.req.param(); + const url = `http://rsj.ningbo.gov.cn/col/col${colId}/index.html`; + const { data: response } = await got(url); + const noticeCate = load(response)('.titel.bgcolor01').text(); + const reg = /
  • .*<\/li>/g; + const item = response.match(reg).map((line) => { + const $ = load(line); + const title = $('.news_titel'); + return { + title: `宁波人社公告-${noticeCate}:${title.text()}`, + link: `http://rsj.ningbo.gov.cn${title.attr('href')}`, + pubDate: parseDate($('.news_date').text().replaceAll(/\[|]/g, '')), + author: '宁波市人力资源和社会保障局', + description: title.text(), + }; + }); + return { + title: '宁波市人力资源和社会保障局-公告', + link: url, + item, + }; + }, +}; diff --git a/lib/routes/gov/zj/search.ts b/lib/routes/gov/zj/search.ts index e3f2942fababfb..e8c0bd9efb085c 100644 --- a/lib/routes/gov/zj/search.ts +++ b/lib/routes/gov/zj/search.ts @@ -1,9 +1,8 @@ -import { Route } from '@/types'; +import { Route, DataItem } from '@/types'; import { parseDate } from '@/utils/parse-date'; import got from '@/utils/got'; import { load } from 'cheerio'; import dayjs from 'dayjs'; - export const route: Route = { path: '/zj/search/:websiteid?/:word/:cateid?', categories: ['government'], @@ -24,18 +23,18 @@ export const route: Route = { url: 'search.zj.gov.cn/jsearchfront/search.do', maintainers: ['HaoyuLee'], description: ` - | 行政区域 | websiteid | - | ------------ | -- | - | 宁波市本级 | 330201000000000 | +| 行政区域 | websiteid | +| ------------ | -- | +| 宁波市本级 | 330201000000000 | - | 搜索关键词 | word | +| 搜索关键词 | word | - | 信息分类 | cateid | +| 信息分类 | cateid | - | 排序类型 | sortType | - | ------------ | -- | - | 按相关度 | 1 | - | 按时间 | 2 | +| 排序类型 | sortType | +| ------------ | -- | +| 按相关度 | 1 | +| 按时间 | 2 | `, async handler(ctx) { const { websiteid = '330201000000000', word = '人才', cateid = 658, sortType = 2 } = ctx.req.param(); @@ -71,10 +70,16 @@ export const route: Route = { description: $('.newsDescribe>a').text() || '', }; }) || []; + const res = {}; + for (const current of items) { + if (!res[current.link]) { + res[current.link] = current; + } + } return { title: '浙江省人民政府-全省政府网站统一搜索', link: 'https://search.zj.gov.cn/jsearchfront/search.do', - item: items, + item: Object.entries(res).map(([, value]) => value) as DataItem[], }; }, }; diff --git a/lib/routes/gq/namespace.ts b/lib/routes/gq/namespace.ts index 9814d3c32554bd..2836487cb42380 100644 --- a/lib/routes/gq/namespace.ts +++ b/lib/routes/gq/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'GQ', url: 'gq.com', + lang: 'en', }; diff --git a/lib/routes/gq/news.ts b/lib/routes/gq/news.ts index b012ff9c329b90..c2b4a04cdc5270 100644 --- a/lib/routes/gq/news.ts +++ b/lib/routes/gq/news.ts @@ -1,12 +1,13 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import parser from '@/utils/rss-parser'; import { load } from 'cheerio'; -import { ofetch } from 'ofetch'; +import ofetch from '@/utils/ofetch'; const host = 'https://www.gq.com'; export const route: Route = { path: '/news', - categories: ['traditional-media'], + categories: ['traditional-media', 'popular'], + view: ViewType.Articles, example: '/gq/news', parameters: {}, features: { diff --git a/lib/routes/grainoil/category.ts b/lib/routes/grainoil/category.ts new file mode 100644 index 00000000000000..69419e4d5578a5 --- /dev/null +++ b/lib/routes/grainoil/category.ts @@ -0,0 +1,207 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +import { type CheerioAPI, type Cheerio, type Element, load } from 'cheerio'; +import { type Context } from 'hono'; + +export const handler = async (ctx: Context): Promise => { + const { category, id } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'http://load.grainoil.com.cn'; + const targetUrl: string = new URL(`${category}/${id}.jspx`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + items = $('div.m_listpagebox ol li a') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio = $(el); + + const title: string = $el.find('b').text(); + const pubDateStr: string | undefined = $el.find('span').text().trim(); + const linkUrl: string | undefined = $el.attr('href'); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : undefined, + link: linkUrl, + updated: upDatedStr ? timezone(parseDate(upDatedStr), +8) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.m_tit h2').text(); + const description: string = $$('div.TRS_Editor').html() ?? ''; + const authors: DataItem['author'] = $$('div.m_tit h2 a').first().text(); + + const processedItem: DataItem = { + title, + description, + author: authors, + content: { + html: description, + text: description, + }, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[http-equiv="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('div.top_logo img').attr('src') ? new URL($('div.top_logo img').attr('src') as string, baseUrl).href : undefined, + author: $('meta[http-equiv="keywords"]').attr('content'), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/:category/:id', + name: '分类', + url: 'load.grainoil.com.cn', + maintainers: ['nczitzk'], + handler, + example: '/grainoil/newsListHome/3', + parameters: { + category: { + description: '分类,默认为 `newsListHome`,可在对应分类页 URL 中找到', + options: [ + { + label: 'newsListHome', + value: 'newsListHome', + }, + { + label: 'newsListChannel', + value: 'newsListChannel', + }, + ], + }, + id: { + description: '分类 ID,可在对应分类页 URL 中找到', + }, + }, + description: `:::tip +若订阅 [政务信息](http://load.grainoil.com.cn/newsListHome/1430.jspx),网址为 \`http://load.grainoil.com.cn/newsListHome/1430.jspx\`,请截取 \`https://load.grainoil.com.cn/\` 到末尾 \`.jspx\` 的部分 \`newsListHome/1430\` 作为 \`category\` 和 \`id\`参数填入,此时目标路由为 [\`/grainoil/newsListHome/1430\`](https://rsshub.app/grainoil/newsListHome/1430)。 + +::: + +
    + 更多分类 + +| 分类 | ID | +| -------- | ------------------ | +| 政务信息 | newsListHome/1430 | +| 要闻动态 | newsListHome/3 | +| 产业经济 | newsListHome/1469 | +| 产业信息 | newsListHome/1471 | +| 爱粮节粮 | newsListHome/1470 | +| 政策法规 | newsListChannel/18 | +| 生产气象 | newsListChannel/19 | +| 统计资料 | newsListChannel/20 | +| 综合信息 | newsListChannel/21 | + +
    +`, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['load.grainoil.com.cn/:category/:id'], + target: (params) => { + const category: string = params.category; + const id: string = params.id; + + return `/grainoil/${category}/${id}`; + }, + }, + { + title: '政务信息', + source: ['load.grainoil.com.cn/newsListHome/1430.jspx'], + target: '/newsListHome/1430', + }, + { + title: '要闻动态', + source: ['load.grainoil.com.cn/newsListHome/3.jspx'], + target: '/newsListHome/3', + }, + { + title: '产业经济', + source: ['load.grainoil.com.cn/newsListHome/1469.jspx'], + target: '/newsListHome/1469', + }, + { + title: '产业信息', + source: ['load.grainoil.com.cn/newsListHome/1471.jspx'], + target: '/newsListHome/1471', + }, + { + title: '爱粮节粮', + source: ['load.grainoil.com.cn/newsListHome/1470.jspx'], + target: '/newsListHome/1470', + }, + { + title: '政策法规', + source: ['load.grainoil.com.cn/newsListChannel/18.jspx'], + target: '/newsListChannel/18', + }, + { + title: '生产气象', + source: ['load.grainoil.com.cn/newsListChannel/19.jspx'], + target: '/newsListChannel/19', + }, + { + title: '统计资料', + source: ['load.grainoil.com.cn/newsListChannel/20.jspx'], + target: '/newsListChannel/20', + }, + { + title: '综合信息', + source: ['load.grainoil.com.cn/newsListChannel/21.jspx'], + target: '/newsListChannel/21', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/grainoil/namespace.ts b/lib/routes/grainoil/namespace.ts new file mode 100644 index 00000000000000..438059f3b13307 --- /dev/null +++ b/lib/routes/grainoil/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '国家粮油信息中心', + url: 'load.grainoil.com.cn', + categories: ['new-media'], + description: '中国粮食信息网', + lang: 'zh-CN', +}; diff --git a/lib/routes/greasyfork/namespace.ts b/lib/routes/greasyfork/namespace.ts index 10f49bdfb117c6..8ed9c58a09b899 100644 --- a/lib/routes/greasyfork/namespace.ts +++ b/lib/routes/greasyfork/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Greasy Fork', url: 'greasyfork.org', + lang: 'en', }; diff --git a/lib/routes/grist/featured.ts b/lib/routes/grist/featured.ts index dcc6f306e7bc95..826dd07a4bef31 100644 --- a/lib/routes/grist/featured.ts +++ b/lib/routes/grist/featured.ts @@ -6,7 +6,7 @@ import { load } from 'cheerio'; export const route: Route = { path: '/featured', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/grist/featured', parameters: {}, features: { diff --git a/lib/routes/grist/index.ts b/lib/routes/grist/index.ts index f7e92bc65b21a1..d207b6402e214c 100644 --- a/lib/routes/grist/index.ts +++ b/lib/routes/grist/index.ts @@ -8,8 +8,11 @@ export const route: Route = { source: ['grist.org/articles/'], }, ], - name: 'Unknown', + name: 'Latest Articles', maintainers: ['Rjnishant530'], + categories: ['new-media', 'popular'], + example: '/grist', + parameters: {}, handler, url: 'grist.org/articles/', }; diff --git a/lib/routes/grist/namespace.ts b/lib/routes/grist/namespace.ts index e5c02250251935..9f5213d84df285 100644 --- a/lib/routes/grist/namespace.ts +++ b/lib/routes/grist/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Grist', url: 'grist.org', + lang: 'en', }; diff --git a/lib/routes/grist/series.ts b/lib/routes/grist/series.ts index 0425502e06671b..0241b127a3ec9a 100644 --- a/lib/routes/grist/series.ts +++ b/lib/routes/grist/series.ts @@ -3,7 +3,7 @@ import { getData, getList } from './utils'; export const route: Route = { path: '/series/:series', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/grist/series/best-of-grist', parameters: { series: 'Find in the URL which has /series/' }, features: { diff --git a/lib/routes/grist/topic.ts b/lib/routes/grist/topic.ts index bc073aeb0ef0ac..e4702162732bfc 100644 --- a/lib/routes/grist/topic.ts +++ b/lib/routes/grist/topic.ts @@ -3,7 +3,7 @@ import { getData, getList } from './utils'; export const route: Route = { path: '/topic/:topic', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/grist/topic/extreme-heat', parameters: { topic: 'Any Topic from Table below' }, features: { @@ -25,49 +25,49 @@ export const route: Route = { url: 'grist.org/articles/', description: `Topics - | Topic Name | Topic Link | - | ------------------------ | ------------------ | - | Accountability | accountability | - | Agriculture | agriculture | - | Ask Umbra | ask-umbra-series | - | Buildings | buildings | - | Cities | cities | - | Climate & Energy | climate-energy | - | Climate Fiction | climate-fiction | - | Climate of Courage | climate-of-courage | - | COP26 | cop26 | - | COP27 | cop27 | - | Culture | culture | - | Economics | economics | - | Energy | energy | - | Equity | equity | - | Extreme Weather | extreme-weather | - | Fix | fix | - | Food | food | - | Grist | grist | - | Grist News | grist-news | - | Health | health | - | Housing | housing | - | Indigenous Affairs | indigenous | - | International | international | - | Labor | labor | - | Language | language | - | Migration | migration | - | Opinion | opinion | - | Politics | politics | - | Protest | protest | - | Race | race | - | Regulation | regulation | - | Science | science | - | Shift Happens Newsletter | shift-happens | - | Solutions | solutions | - | Spanish | spanish | - | Sponsored | sponsored | - | Technology | technology | - | Temperature Check | temperature-check | - | Uncategorized | article | - | Updates | updates | - | Video | video |`, +| Topic Name | Topic Link | +| ------------------------ | ------------------ | +| Accountability | accountability | +| Agriculture | agriculture | +| Ask Umbra | ask-umbra-series | +| Buildings | buildings | +| Cities | cities | +| Climate & Energy | climate-energy | +| Climate Fiction | climate-fiction | +| Climate of Courage | climate-of-courage | +| COP26 | cop26 | +| COP27 | cop27 | +| Culture | culture | +| Economics | economics | +| Energy | energy | +| Equity | equity | +| Extreme Weather | extreme-weather | +| Fix | fix | +| Food | food | +| Grist | grist | +| Grist News | grist-news | +| Health | health | +| Housing | housing | +| Indigenous Affairs | indigenous | +| International | international | +| Labor | labor | +| Language | language | +| Migration | migration | +| Opinion | opinion | +| Politics | politics | +| Protest | protest | +| Race | race | +| Regulation | regulation | +| Science | science | +| Shift Happens Newsletter | shift-happens | +| Solutions | solutions | +| Spanish | spanish | +| Sponsored | sponsored | +| Technology | technology | +| Temperature Check | temperature-check | +| Uncategorized | article | +| Updates | updates | +| Video | video |`, }; async function handler(ctx) { diff --git a/lib/routes/grist/utils.ts b/lib/routes/grist/utils.ts index 4b4a5f59cec80b..c93f183280bd3c 100644 --- a/lib/routes/grist/utils.ts +++ b/lib/routes/grist/utils.ts @@ -1,8 +1,8 @@ -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; -const getData = (url) => got.get(url).json(); +const getData = (url) => ofetch(url); const getList = (data) => data.map((value) => { diff --git a/lib/routes/grubstreet/namespace.ts b/lib/routes/grubstreet/namespace.ts index c48bd6b8101f25..b6e76d76eaccbf 100644 --- a/lib/routes/grubstreet/namespace.ts +++ b/lib/routes/grubstreet/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Grub Street', url: 'grubstreet.com', + lang: 'en', }; diff --git a/lib/routes/gs/developer/blog.ts b/lib/routes/gs/developer/blog.ts new file mode 100644 index 00000000000000..36fc4ec299d6fe --- /dev/null +++ b/lib/routes/gs/developer/blog.ts @@ -0,0 +1,58 @@ +import { Route, Data } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/developer/blog', + categories: ['blog'], + example: '/gs/developer/blog', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['developer.gs.com/blog/posts'], + target: '/developer/blog', + }, + ], + name: 'Goldman Sachs Developer Blog', + zh: { + name: '高盛开发者博客', + }, + maintainers: ['chesha1'], + handler: handlerRoute, +}; + +async function handlerRoute(): Promise { + const response = await ofetch('https://developer.gs.com/blog/posts'); + const $ = load(response); + + const items = $('div[data-cy="blog-card-grid"] a') + .toArray() + .map((item) => { + const link = 'https://developer.gs.com' + $(item).attr('href'); + const title = $(item).find('span').eq(1).text(); + const author = $(item).find('span').eq(2).text(); + const description = $(item).find('span').eq(3).text(); + const pubDate = $(item).find('span').eq(0).text(); + return { + title, + link, + author, + description, + pubDate, + }; + }); + + return { + title: 'Goldman Sachs Developer Blog', + link: 'https://developer.gs.com/blog/posts', + item: items, + }; +} diff --git a/lib/routes/gs/namespace.ts b/lib/routes/gs/namespace.ts new file mode 100644 index 00000000000000..9874fd86cd341b --- /dev/null +++ b/lib/routes/gs/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Goldman Sachs', + url: 'goldmansachs.com', + zh: { + name: '高盛', + }, + lang: 'en', +}; diff --git a/lib/routes/guancha/index.ts b/lib/routes/guancha/index.ts index af7b271e15c3f7..33616cbf591566 100644 --- a/lib/routes/guancha/index.ts +++ b/lib/routes/guancha/index.ts @@ -58,16 +58,16 @@ export const route: Route = { handler, url: 'guancha.cn/', description: `| 全部 | 评论 & 研究 | 要闻 | 风闻 | 热点新闻 | 滚动新闻 | - | ---- | ----------- | ----- | ------- | -------- | -------- | - | all | review | story | fengwen | redian | gundong | +| ---- | ----------- | ----- | ------- | -------- | -------- | +| all | review | story | fengwen | redian | gundong | home = 评论 & 研究 + 要闻 + 风闻 others = 热点新闻 + 滚动新闻 - :::tip +::: tip 观察者网首页左中右的三个 column 分别对应 **评论 & 研究**、**要闻**、**风闻** 三个部分。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/guancha/member.ts b/lib/routes/guancha/member.ts index 1f8802152d2b4c..dac3eff1a6ac7b 100644 --- a/lib/routes/guancha/member.ts +++ b/lib/routes/guancha/member.ts @@ -34,8 +34,8 @@ export const route: Route = { handler, url: 'guancha.cn/', description: `| 精选 | 观书堂 | 在线课 | 观学院 | - | --------- | ------ | ------- | -------- | - | recommend | books | courses | huodongs |`, +| --------- | ------ | ------- | -------- | +| recommend | books | courses | huodongs |`, }; async function handler(ctx) { @@ -68,7 +68,7 @@ async function handler(ctx) { for (const i of item.items) { const newPubDate = new Date(i.publish_time); - pubDate = pubDate > newPubDate ? pubDate : newPubDate; + pubDate = Math.max(pubDate, newPubDate); description += `
    ${i.title}
    `; } 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 Promise[] ); return { title: 'Hex-Rays Blog', link, item: items, + image: 'https://hex-rays.com/hubfs/Ico-logo.png', }; } diff --git a/lib/routes/hex-rays/namespace.ts b/lib/routes/hex-rays/namespace.ts index 7c8e572e5ab663..575096f73d7052 100644 --- a/lib/routes/hex-rays/namespace.ts +++ b/lib/routes/hex-rays/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Hex-Rays', url: 'hex-rays.com', + lang: 'en', }; diff --git a/lib/routes/hexun/index.ts b/lib/routes/hexun/index.ts new file mode 100644 index 00000000000000..b4860c48073bbd --- /dev/null +++ b/lib/routes/hexun/index.ts @@ -0,0 +1,74 @@ +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 decoder = new TextDecoder('gbk'); + +export const route: Route = { + path: '/pe/news', + categories: ['finance'], + example: '/hexun/pe/news', + url: 'pe.hexun.com/news/', + name: '创投行业新闻', + maintainers: ['p3psi-boo'], + handler, +}; + +async function handler() { + const baseUrl = 'https://pe.hexun.com/news/'; + + const response = await got({ + method: 'get', + url: baseUrl, + responseType: 'arrayBuffer', + }); + + const $ = load(decoder.decode(response.data)); + + const list = $('.listNews li') + .toArray() + .map((item) => { + const element = $(item); + const a = element.find('a'); + + const link = a.attr('href')?.replace('http://', 'https://') || ''; + const title = a.text() || ''; + + const timeSpan = element.find('span'); + const dateText = timeSpan.text().slice(1, timeSpan.text().length - 1); + const pubDate = parseDate(dateText, 'MM/DD HH:mm'); + + return { + title, + link, + pubDate, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await got({ + method: 'get', + url: item.link, + responseType: 'arrayBuffer', + }); + + const $ = load(decoder.decode(response.data)); + + item.description = $('.art_contextBox').html() || ''; + + return item; + }) + ) + ); + + return { + title: '和讯创投 - 创投行业新闻', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/hexun/namespace.ts b/lib/routes/hexun/namespace.ts new file mode 100644 index 00000000000000..1870d0e18984a8 --- /dev/null +++ b/lib/routes/hexun/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '和讯网', + url: 'hexun.com', + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/hfut/hf/notice.ts b/lib/routes/hfut/hf/notice.ts new file mode 100644 index 00000000000000..d39b935bd28e72 --- /dev/null +++ b/lib/routes/hfut/hf/notice.ts @@ -0,0 +1,43 @@ +import { Route } from '@/types'; +import parseList from './utils'; + +export const route: Route = { + path: '/hf/notice/:type?', + categories: ['university'], + example: '/hfut/hf/notice/tzgg', + parameters: { type: '分类,见下表(默认为 `tzgg`)' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportRadar: true, + supportScihub: false, + }, + radar: [ + { + source: ['news.hfut.edu.cn'], + }, + ], + name: '合肥校区通知', + maintainers: ['batemax'], + handler, + description: `| 通知公告(https://news.hfut.edu.cn/tzgg2.htm) | 教学科研(https://news.hfut.edu.cn/tzgg2/jxky.htm) | 其他通知(https://news.hfut.edu.cn/tzgg2/qttz.htm) | +| ------------ | -------------- | ------------------ | +| tzgg | jxky | qttz |`, +}; + +async function handler(ctx) { + // set default router type + const type = ctx.req.param('type') ?? 'tzgg'; + + const { link, title, resultList } = await parseList(ctx, type); + + return { + title, + link, + description: '合肥工业大学 - 通知公告', + item: resultList, + }; +} diff --git a/lib/routes/hfut/hf/utils.ts b/lib/routes/hfut/hf/utils.ts new file mode 100644 index 00000000000000..874121db6bdd39 --- /dev/null +++ b/lib/routes/hfut/hf/utils.ts @@ -0,0 +1,80 @@ +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +const typeMap = { + tzgg: { name: 'tzgg', url: 'https://news.hfut.edu.cn/tzgg2.htm', root: 'https://news.hfut.edu.cn', title: '合肥工业大学 - 通知公告' }, + jxky: { name: 'jxky', url: 'https://news.hfut.edu.cn/tzgg2.htm', root: 'https://news.hfut.edu.cn', title: '合肥工业大学 - 通知公告 - 教学科研' }, + qttz: { name: 'qttz', url: 'https://news.hfut.edu.cn/tzgg2.htm', root: 'https://news.hfut.edu.cn', title: '合肥工业大学 - 通知公告 - 其它通知' }, +}; + +const commLink = 'https://news.hfut.edu.cn/'; + +const parseList = async (ctx, type) => { + const link = typeMap[type].url; + const title = typeMap[type].title; + + const response = await got(link); + const $ = load(response.data); + + const resultList = await parseArticle(typeMap[type].name, $); + + return { + title, + link, + resultList, + }; +}; + +async function parseArticle(type, $) { + let data = $('#tzz').find('li').toArray(); + + if (type === 'jxky') { + data = $('#c01').find('li').toArray(); + } else if (type === 'qttz') { + data = $('#c02').find('li').toArray(); + } + + const items = data.map((item) => { + item = $(item); + const oriLink = item.find('a').attr('href'); + let linkRes = oriLink; + if (!oriLink.startsWith('http')) { + linkRes = commLink + item.find('a').attr('href'); + } + const pubDate = parseDate(item.find('i').text(), 'YYYY-MM-DD'); + + return { + title: item.find('p').text(), + pubDate, + link: linkRes, + }; + }); + + const resultItems = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + let description; + try { + const response = await got(item.link); + const $ = load(response.data); + description = $('.wp_articlecontent').html() ?? $('.v_news_content').html() ?? item.link; + } catch { + description = item.link; + } + + return { + title: item.title, + link: item.link, + description, + pubDate: item.pubDate, + }; + }) + ) + ); + + return resultItems; +} + +export default parseList; diff --git a/lib/routes/hfut/namespace.ts b/lib/routes/hfut/namespace.ts new file mode 100644 index 00000000000000..854e7c4280c959 --- /dev/null +++ b/lib/routes/hfut/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '合肥工业大学', + url: 'hfut.edu.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/hfut/xc/notice.ts b/lib/routes/hfut/xc/notice.ts new file mode 100644 index 00000000000000..538867464d440b --- /dev/null +++ b/lib/routes/hfut/xc/notice.ts @@ -0,0 +1,43 @@ +import { Route } from '@/types'; +import parseList from './utils'; + +export const route: Route = { + path: '/xc/notice/:type?', + categories: ['university'], + example: '/hfut/xc/notice/tzgg', + parameters: { type: '分类,见下表(默认为 `tzgg`)' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportRadar: true, + supportScihub: false, + }, + radar: [ + { + source: ['xc.hfut.edu.cn'], + }, + ], + name: '宣城校区通知', + maintainers: ['batemax'], + handler, + description: `| 通知公告(https://xc.hfut.edu.cn/1955/list.htm) | 院系动态-工作通知(https://xc.hfut.edu.cn/gztz/list.htm) | +| ------------ | -------------- | +| tzgg | gztz |`, +}; + +async function handler(ctx) { + // set default router type + const type = ctx.req.param('type') ?? 'tzgg'; + + const { link, title, resultList } = await parseList(ctx, type); + + return { + title, + link, + description: '合肥工业大学宣城校区 - 通知公告', + item: resultList, + }; +} diff --git a/lib/routes/hfut/xc/utils.ts b/lib/routes/hfut/xc/utils.ts new file mode 100644 index 00000000000000..3e2c998fed6813 --- /dev/null +++ b/lib/routes/hfut/xc/utils.ts @@ -0,0 +1,73 @@ +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +const typeMap = { + tzgg: { name: 'tzgg', url: 'https://xc.hfut.edu.cn/1955/list.htm', root: 'https://xc.hfut.edu.cn', title: '合肥工业大学宣城校区 - 通知公告' }, + gztz: { name: 'gztz', url: 'https://xc.hfut.edu.cn/gztz/list.htm', root: 'https://xc.hfut.edu.cn', title: '合肥工业大学宣城校区 - 院系动态 - 工作通知' }, +}; + +const commLink = 'https://xc.hfut.edu.cn/'; + +const parseList = async (ctx, type) => { + const link = typeMap[type].url; + const title = typeMap[type].title; + + const response = await got(link); + const $ = load(response.data); + + const resultList = await parseArticle($); + + return { + title, + link, + resultList, + }; +}; + +async function parseArticle($) { + const data = $('#wp_news_w6').find('li').toArray(); + + const items = data.map((item) => { + item = $(item); + const oriLink = item.find('a').attr('href'); + let linkRes = oriLink; + if (!oriLink.startsWith('http')) { + linkRes = commLink + item.find('a').attr('href'); + } + const pubDate = parseDate(item.find('.news_meta').text(), 'YYYY-MM-DD'); + + return { + title: item.find('a').attr('title'), + pubDate, + link: linkRes, + }; + }); + + const resultItems = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + let description; + try { + const response = await got(item.link); + const $ = load(response.data); + description = $('.wp_articlecontent').html() ?? $('.v_news_content').html() ?? item.link; + } catch { + description = item.link; + } + + return { + title: item.title, + link: item.link, + description, + pubDate: item.pubDate, + }; + }) + ) + ); + + return resultItems; +} + +export default parseList; diff --git a/lib/routes/hicairo/namespace.ts b/lib/routes/hicairo/namespace.ts index 412eb637b8f512..4fb98e14ebaf4c 100644 --- a/lib/routes/hicairo/namespace.ts +++ b/lib/routes/hicairo/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: "HiFeng'Blog", url: 'hicairo.com', + lang: 'zh-CN', }; diff --git a/lib/routes/hinatazaka46/blog.ts b/lib/routes/hinatazaka46/blog.ts index 8e40c16f14cefb..8439743d5e7bcc 100644 --- a/lib/routes/hinatazaka46/blog.ts +++ b/lib/routes/hinatazaka46/blog.ts @@ -23,41 +23,41 @@ export const route: Route = { handler, description: `Member ID - | Member ID | Name | - | --------- | ------------ | - | 2000 | 四期生リレー | - | 36 | 渡辺 莉奈 | - | 35 | 山下 葉留花 | - | 34 | 宮地 すみれ | - | 33 | 藤嶌 果歩 | - | 32 | 平岡 海月 | - | 31 | 平尾 帆夏 | - | 30 | 竹内 希来里 | - | 29 | 正源司 陽子 | - | 28 | 清水 理央 | - | 27 | 小西 夏菜実 | - | 26 | 岸 帆夏 | - | 25 | 石塚 瑶季 | - | 24 | 山口 陽世 | - | 23 | 森本 茉莉 | - | 22 | 髙橋 未来虹 | - | 21 | 上村 ひなの | - | 18 | 松田 好花 | - | 17 | 濱岸 ひより | - | 16 | 丹生 明里 | - | 15 | 富田 鈴花 | - | 14 | 小坂 菜緒 | - | 13 | 河田 陽菜 | - | 12 | 金村 美玖 | - | 11 | 東村 芽依 | - | 10 | 高本 彩花 | - | 9 | 高瀬 愛奈 | - | 8 | 佐々木 美玲 | - | 7 | 佐々木 久美 | - | 6 | 齊藤 京子 | - | 5 | 加藤 史帆 | - | 4 | 影山 優佳 | - | 2 | 潮 紗理菜 |`, +| Member ID | Name | +| --------- | ------------ | +| 2000 | 四期生リレー | +| 36 | 渡辺 莉奈 | +| 35 | 山下 葉留花 | +| 34 | 宮地 すみれ | +| 33 | 藤嶌 果歩 | +| 32 | 平岡 海月 | +| 31 | 平尾 帆夏 | +| 30 | 竹内 希来里 | +| 29 | 正源司 陽子 | +| 28 | 清水 理央 | +| 27 | 小西 夏菜実 | +| 26 | 岸 帆夏 | +| 25 | 石塚 瑶季 | +| 24 | 山口 陽世 | +| 23 | 森本 茉莉 | +| 22 | 髙橋 未来虹 | +| 21 | 上村 ひなの | +| 18 | 松田 好花 | +| 17 | 濱岸 ひより | +| 16 | 丹生 明里 | +| 15 | 富田 鈴花 | +| 14 | 小坂 菜緒 | +| 13 | 河田 陽菜 | +| 12 | 金村 美玖 | +| 11 | 東村 芽依 | +| 10 | 高本 彩花 | +| 9 | 高瀬 愛奈 | +| 8 | 佐々木 美玲 | +| 7 | 佐々木 久美 | +| 6 | 齊藤 京子 | +| 5 | 加藤 史帆 | +| 4 | 影山 優佳 | +| 2 | 潮 紗理菜 |`, }; async function handler(ctx) { diff --git a/lib/routes/hinatazaka46/namespace.ts b/lib/routes/hinatazaka46/namespace.ts index 6cb3f555363365..dfe7f28061efb4 100644 --- a/lib/routes/hinatazaka46/namespace.ts +++ b/lib/routes/hinatazaka46/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Sakamichi Series 坂道系列官网资讯', url: 'hinatazaka46.com', + lang: 'zh-CN', }; diff --git a/lib/routes/hiring.cafe/jobs.ts b/lib/routes/hiring.cafe/jobs.ts new file mode 100644 index 00000000000000..30d492755f6e03 --- /dev/null +++ b/lib/routes/hiring.cafe/jobs.ts @@ -0,0 +1,152 @@ +import ofetch from '@/utils/ofetch'; +import path from 'node:path'; +import { art } from '@/utils/render'; +import { Context } from 'hono'; +import { getCurrentPath } from '@/utils/helpers'; +import { Route } from '@/types'; + +const __dirname = getCurrentPath(import.meta.url); + +const CONFIG = { + DEFAULT_PAGE_SIZE: 20, + MAX_PAGE_SIZE: 100, +} as const; + +const API = { + BASE_URL: 'https://hiring.cafe/api/search-jobs', + HEADERS: { + 'Content-Type': 'application/json', + }, +} as const; + +interface GeoLocation { + readonly lat: number; + readonly lon: number; +} + +interface JobInformation { + readonly title: string; + readonly description: string; +} + +interface ProcessedJobData { + readonly company_name: string; + readonly is_compensation_transparent: boolean; + readonly yearly_min_compensation?: number; + readonly yearly_max_compensation?: number; + readonly workplace_type?: string; + readonly requirements_summary?: string; + readonly job_category: string; + readonly role_activities: readonly string[]; + readonly formatted_workplace_location?: string; + readonly estimated_publish_date_millis: string; +} + +interface JobResult { + readonly id: string; + readonly apply_url: string; + readonly job_information: JobInformation; + readonly v5_processed_job_data: ProcessedJobData; + readonly _geoloc: readonly GeoLocation[]; +} + +interface ApiResponse { + readonly results: readonly JobResult[]; + readonly total: number; +} + +interface SearchParams { + readonly keywords: string; + readonly page?: number; + readonly size?: number; + readonly sortBy?: 'date' | 'default' | 'compensation_desc' | 'experience_asc'; +} + +const validateSearchParams = ({ keywords, page = 0, size = CONFIG.DEFAULT_PAGE_SIZE }: SearchParams): SearchParams => ({ + keywords: keywords.trim(), + page: Math.max(0, Math.floor(Number(page))), + size: Math.min(Math.max(1, Math.floor(Number(size))), CONFIG.MAX_PAGE_SIZE), +}); + +const fetchJobs = async (searchParams: SearchParams): Promise => { + const payload = { + size: searchParams.size || 20, + page: searchParams.page || 0, + searchState: { + searchQuery: searchParams.keywords, + sortBy: searchParams.sortBy || 'date', + }, + }; + + return await ofetch(API.BASE_URL, { + method: 'POST', + body: payload, + headers: API.HEADERS, + }); +}; + +const renderJobDescription = (jobInfo: JobInformation, processedData: ProcessedJobData): string => + art(path.join(__dirname, 'templates/jobs.art'), { + company_name: processedData.company_name, + location: processedData.formatted_workplace_location ?? 'Remote/Unspecified', + is_compensation_transparent: Boolean(processedData.is_compensation_transparent && processedData.yearly_min_compensation && processedData.yearly_max_compensation), + yearly_min_compensation_formatted: processedData.yearly_min_compensation?.toLocaleString() ?? '', + yearly_max_compensation_formatted: processedData.yearly_max_compensation?.toLocaleString() ?? '', + workplace_type: processedData.workplace_type ?? 'Not specified', + requirements_summary: processedData.requirements_summary ?? 'No requirements specified', + job_description: jobInfo.description ?? '', + }); + +const transformJobItem = (item: JobResult) => { + const { job_information: jobInfo, v5_processed_job_data: processedData, apply_url, id } = item; + + return { + title: `${jobInfo.title} - ${processedData.company_name}`, + description: renderJobDescription(jobInfo, processedData), + link: apply_url, + pubDate: new Date(processedData.estimated_publish_date_millis).toUTCString(), + category: [processedData.job_category, ...processedData.role_activities, processedData.workplace_type].filter((x): x is string => !!x), + author: processedData.company_name, + guid: id, + }; +}; + +async function handler(ctx: Context) { + const searchParams = validateSearchParams({ + keywords: ctx.req.param('keywords'), + }); + + const response = await fetchJobs(searchParams); + const items = response.results.map((item) => transformJobItem(item)); + + return { + title: `HiringCafe Jobs: ${searchParams.keywords}`, + description: `Job search results for "${searchParams.keywords}" on HiringCafe`, + link: `https://hiring.cafe/jobs?q=${encodeURIComponent(searchParams.keywords)}`, + item: items, + total: response.total, + }; +} + +export const route: Route = { + path: '/jobs/:keywords', + categories: ['other'], + example: '/hiring.cafe/jobs/sustainability', + parameters: { keywords: 'Keywords to search for' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['hiring.cafe'], + }, + ], + name: 'Jobs', + maintainers: ['mintyfrankie'], + handler, +}; diff --git a/lib/routes/hiring.cafe/namespace.ts b/lib/routes/hiring.cafe/namespace.ts new file mode 100644 index 00000000000000..3985f5f803a665 --- /dev/null +++ b/lib/routes/hiring.cafe/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'HiringCafe', + url: 'hiring.cafe', + description: 'HiringCafe is a platform for job seekers to find job opportunities and for employers to post job listings.', + zh: { + name: 'HiringCafe', + }, +}; diff --git a/lib/routes/hiring.cafe/templates/jobs.art b/lib/routes/hiring.cafe/templates/jobs.art new file mode 100644 index 00000000000000..bdc770463b394e --- /dev/null +++ b/lib/routes/hiring.cafe/templates/jobs.art @@ -0,0 +1,18 @@ +

    Company: {{ company_name }}

    +

    Location: {{ location }}

    + +{{if is_compensation_transparent}} +

    Compensation: ${{ yearly_min_compensation_formatted }} - ${{ yearly_max_compensation_formatted }} per year

    +{{/if}} + +

    Workplace Type: {{ workplace_type }}

    +

    Requirements: {{ requirements_summary }}

    + +
    + {{@ job_description }} +
    + +{{if has_company_info}} +

    About {{ 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 社会 | -
    `, +| sraw020 | sraw020-1 | sraw020-2 | sraw020-3 | sraw020-4 | +| -------- | ------------ | --------- | --------- | --------- | +| ESG 主页 | ESG 趋势政策 | ESG 投资 | ESG 企业 | ESG 社会 | +
    `, }; async function handler(ctx) { const { category = 'sran001' } = ctx.req.param(); const baseUrl = urlMap[category.substring(0, 4)].baseUrl; - const { data: response } = await got(`${baseUrl}/${category}`); + const response = await ofetch(`${baseUrl}/${category}`); - const $ = load(response); + const $ = cheerio.load(response); - const list = $('div.listing-title > a') + const list = $('.main-listing-container div.listing-title > a') .toArray() .map((item) => { item = $(item); + const url = item.parent().parent().find('.share-button').data('url'); return { title: item.text().trim(), - link: item.attr('href').startsWith('http') - ? // remove tracking parameters - baseUrl + item.attr('href').split('?')[0].substring(0, item.attr('href').lastIndexOf('/')) - : item.attr('href').split('?')[0].substring(0, item.attr('href').lastIndexOf('/')), + link: url.startsWith('http') ? url : baseUrl + url, }; - }); + }) as DataItem[]; const items = await Promise.all( list.map((item) => - cache.tryGet(item.link, async () => { - if (item.link.startsWith('https://invest.hket.com/') || item.link.startsWith('https://ps.hket.com/')) { - let data; - - data = await (item.link.startsWith('https://invest.hket.com/') - ? got.post('https://invest.hket.com/content-api-middleware/content', { + cache.tryGet(item.link!, async () => { + if (item.link!.startsWith('https://invest.hket.com/') || item.link!.startsWith('https://ps.hket.com/')) { + const data = await (item.link!.startsWith('https://invest.hket.com/') + ? ofetch('https://invest.hket.com/content-api-middleware/content', { headers: { - referer: item.link, + referer: item.link!, }, - json: { - id: item.link.split('/').pop(), + method: 'POST', + body: { + id: item.link!.split('/').pop(), channel: 'invest', }, }) - : got('https://data02.hket.com/content', { + : ofetch('https://data02.hket.com/content', { headers: { - referer: item.link, + referer: item.link!, }, - searchParams: { - id: item.link.split('/').pop(), + query: { + id: item.link!.split('/').pop(), channel: 'epc', }, })); - data = data.data; item.pubDate = timezone(parseDate(data.displayDate), +8); item.updated = timezone(parseDate(data.lastModifiedDate), +8); @@ -166,8 +178,8 @@ async function handler(ctx) { return item; } - const { data: response } = await got(item.link); - const $ = load(response); + const response = await ofetch(item.link!); + const $ = cheerio.load(response); item.category = $('.contentTags-container > .hotkey-container-wrapper > .hotkey-container > a') .toArray() @@ -194,28 +206,32 @@ async function handler(ctx) { e = $(e); e.replaceWith( art(path.join(__dirname, 'templates/image.art'), { - alt: e.attr('data-alt'), - src: e.attr('data-src') ?? e.attr('src'), + alt: e.data('alt'), + src: e.data('src') ?? e.attr('src'), }) ); }); - item.description = $('div.article-detail-body-container').html(); - item.pubDate = timezone(parseDate($('.article-details-info-container_date, .publish-date-time').text().trim()), +8); + const ldJson = JSON.parse( + $('script[type="application/ld+json"]') + .toArray() + .find((e) => $(e).text().includes('NewsArticle'))?.children[0].data + ); + + item.description = $('div.article-detail-body-container').html()!; + item.pubDate = parseDate(ldJson.datePublished); + item.updated = parseDate(ldJson.dateModified); return item; }) ) ); - const ret = { - title: $('head meta[name=title]').attr('content').trim(), + return { + title: $('head meta[name=title]').attr('content')?.trim(), link: baseUrl + '/' + category, - description: $('head meta[name=description]').attr('content').trim(), + description: $('head meta[name=description]').attr('content')?.trim(), item: items, language: 'zh-hk', }; - - ctx.set('json', ret); - return ret; } diff --git a/lib/routes/hket/namespace.ts b/lib/routes/hket/namespace.ts index a388d521bb3f81..31113b5650b0b7 100644 --- a/lib/routes/hket/namespace.ts +++ b/lib/routes/hket/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '香港经济日报', url: 'china.hket.com', + lang: 'zh-HK', }; diff --git a/lib/routes/hkjunkcall/namespace.ts b/lib/routes/hkjunkcall/namespace.ts index bd8bad804015f4..850af22113c18b 100644 --- a/lib/routes/hkjunkcall/namespace.ts +++ b/lib/routes/hkjunkcall/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'HKJunkCall 資訊中心', url: 'hkjunkcall.com', + lang: 'zh-HK', }; diff --git a/lib/routes/hko/earthquake.ts b/lib/routes/hko/earthquake.ts new file mode 100644 index 00000000000000..9bccb0c314b630 --- /dev/null +++ b/lib/routes/hko/earthquake.ts @@ -0,0 +1,56 @@ +import { Route } from '@/types'; +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: '/earthquake', + name: '全球地震資訊網', + maintainers: ['after9'], + handler, + example: '/hko/earthquake', + categories: ['forecast'], + description: '来自香港天文台的全球5级以上地震记录', +}; + +async function handler() { + const title = '来自香港天文台的全球5级以上地震记录'; + const link = 'https://www.hko.gov.hk/tc/gts/equake/quake-info.htm'; + const description = '提供經天文台分析的全球5.0級或以上及本地有感的地震資訊。'; + + // 发送 HTTP GET 请求到 API 并解构返回的数据对象 + const response = await ofetch('https://www.hko.gov.hk/gts/QEM/eq_app-30d_uc.xml', { + headers: { + accept: 'application/xml', + }, + }); + + const $ = load(response); + + const items = $('Earthquake > EventGroup > Event') + // 使用“toArray()”方法将选择的所有 DOM 元素以数组的形式返回。 + .toArray() + // 使用“map()”方法遍历数组,并从每个元素中解析需要的数据。 + .map((item) => { + item = $(item); + const degree = item.find('Mag').text(); + const city = item.find('City').text(); + const citystring = item.find('citystring').text(); + const hktDate = item.find('HKTDate').text(); + const hktTime = item.find('HKTTime').text(); + const latAndLon = '經緯:[' + item.find('Lat').text() + ',' + item.find('Lon').text() + ']'; + return { + title: `[震級:${degree}] [地點:${city}]`, + description: `${citystring}, ${latAndLon}`, + pubDate: timezone(parseDate(hktDate + hktTime, 'YYYYMMDDHHmm'), +8), + }; + }); + + return { + title, + link, + description, + item: items, + }; +} diff --git a/lib/routes/hko/namespace.ts b/lib/routes/hko/namespace.ts new file mode 100644 index 00000000000000..7605322bb30fd0 --- /dev/null +++ b/lib/routes/hko/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Hong Kong Observatory', + url: 'www.hko.gov.hk', + categories: ['forecast'], + description: '来自香港天文台的全球地震记录', + lang: 'zh-HK', +}; diff --git a/lib/routes/hko/weather.ts b/lib/routes/hko/weather.ts new file mode 100644 index 00000000000000..10145ffd4b8355 --- /dev/null +++ b/lib/routes/hko/weather.ts @@ -0,0 +1,56 @@ +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +const handler = async () => { + const url = 'http://rss.weather.gov.hk/rss/CurrentWeather.xml'; + const pageUrl = 'https://www.weather.gov.hk/en/wxinfo/currwx/current.htm'; + + const data = await ofetch(url); + const $ = cheerio.load(data, { + xmlMode: true, + }); + + const description = $('item').first().find('description'); + + const $$ = cheerio.load(description.text()); + + const items = $$('table tr') + .toArray() + .map((item) => { + const $item = $(item); + const area = $item.find('td').first().text(); + const degree = $item.find('td').last().text(); + + return { + title: area, + description: degree, + pubDate: parseDate($('pubDate').text()), + link: pageUrl, + guid: `${$('guid').text()}#${area}`, + }; + }); + + return { + title: 'Current Weather Report', + description: `provided by the Hong Kong Observatory: ${$('pubDate').text()}`, + link: pageUrl, + item: items, + }; +}; + +export const route: Route = { + path: '/weather', + radar: [ + { + source: ['www.weather.gov.hk/en/wxinfo/currwx/current.htm'], + }, + ], + name: 'Current Weather Report', + example: '/hko/weather', + maintainers: ['calpa'], + categories: ['forecast'], + handler, + url: 'www.weather.gov.hk/en/wxinfo/currwx/current.htm', +}; diff --git a/lib/routes/hkushop/namespace.ts b/lib/routes/hkushop/namespace.ts new file mode 100644 index 00000000000000..8af35abcd99850 --- /dev/null +++ b/lib/routes/hkushop/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '環球唱片(香港)官方網上商店', + url: 'hkushop.com', + lang: 'zh-HK', + categories: ['shopping'], + description: '環球唱片(香港)官方網上商店', +}; diff --git a/lib/routes/hkushop/vinyl-or-picture-lp.ts b/lib/routes/hkushop/vinyl-or-picture-lp.ts new file mode 100644 index 00000000000000..c69296541282a3 --- /dev/null +++ b/lib/routes/hkushop/vinyl-or-picture-lp.ts @@ -0,0 +1,87 @@ +import { Route } from '@/types'; +import puppeteer from '@/utils/puppeteer'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/vinyl/:cat?', + categories: ['shopping'], + example: '/hkushop/vinyl', + parameters: { + cat: '分类,见下表,默认不分类', + }, + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + supportRadar: true, + }, + radar: [ + { + source: ['hkushop.com/vinyl-or-picture-lp.html', 'hkushop.com/'], + target: '/vinyl', + }, + ], + name: 'HKU Shop 黑胶专区', + maintainers: ['gideonsenku'], + handler, + description: `常见分类: +| 華語音樂 | 經典復刻 | 古典跨界 | 爵士音樂 | 國際音樂 | 電影原聲帶 | 黑膠日本音樂 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| 37 | 38 | 40 | 41 | 39 | 170 | 224 |`, + url: 'hkushop.com/vinyl-or-picture-lp.html', +}; + +async function handler(ctx) { + const baseUrl = 'https://hkushop.com'; + const cat = ctx.req.param('cat') ?? ''; + const url = cat ? `${baseUrl}/vinyl-or-picture-lp.html?cat=${cat}` : `${baseUrl}/vinyl-or-picture-lp.html`; + + const browser = await puppeteer(); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (request) => { + request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + }); + await page.goto(url, { + waitUntil: 'domcontentloaded', + }); + + const response = await page.content(); + await page.close(); + await browser.close(); + + const $ = load(response); + + const list = $('.products.list.items.product-items .product-item') + .toArray() + .map((item) => { + const $item = $(item); + const $link = $item.find('.product-item-link'); + const $price = $item.find('.price'); + const $image = $item.find('.product-image-photo'); + const $artist = $item.find('.artist a').text().trim(); + + return { + title: $link.text().trim(), + link: $link.attr('href'), + description: ` + +

    作者: ${$artist}

    +

    价格: ${$price.text().trim()}

    + `, + guid: $link.attr('href'), + }; + }); + + const title = cat ? `黑胶\\彩胶系列 - ${$('.page-title').text().trim()}` : String.raw`黑胶\\彩胶系列 - HKU Shop 环球唱片网店`; + + return { + title, + link: url, + description: 'HKU Shop 黑胶唱片最新商品信息', + item: list, + }; +} diff --git a/lib/routes/hljucm/namespace.ts b/lib/routes/hljucm/namespace.ts index 5c36e5a893a90c..565a3cafcd02a4 100644 --- a/lib/routes/hljucm/namespace.ts +++ b/lib/routes/hljucm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '黑龙江中医药大学', url: 'yjsy.hljucm.net', + lang: 'zh-CN', }; diff --git a/lib/routes/hljucm/yjsy.ts b/lib/routes/hljucm/yjsy.ts index 5294105426747e..6873a21810511e 100644 --- a/lib/routes/hljucm/yjsy.ts +++ b/lib/routes/hljucm/yjsy.ts @@ -22,8 +22,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 新闻动态 | 通知公告 | - | -------- | -------- | - | xwdt | tzgg |`, +| -------- | -------- | +| xwdt | tzgg |`, }; async function handler(ctx) { diff --git a/lib/routes/hnrb/index.ts b/lib/routes/hnrb/index.ts index 1b4b5b34a9de09..821969d0e7d82e 100644 --- a/lib/routes/hnrb/index.ts +++ b/lib/routes/hnrb/index.ts @@ -29,17 +29,17 @@ export const route: Route = { handler, url: 'voc.com.cn/', description: `| 版 | 编号 | - | -------------------- | ---- | - | 全部 | | - | 第 01 版:头版 | 1 | - | 第 02 版:要闻 | 2 | - | 第 03 版:要闻 | 3 | - | 第 04 版:深度 | 4 | - | 第 05 版:市州 | 5 | - | 第 06 版:理论・学习 | 6 | - | 第 07 版:观察 | 7 | - | 第 08 版:时事 | 8 | - | 第 09 版:中缝 | 9 |`, +| -------------------- | ---- | +| 全部 | | +| 第 01 版:头版 | 1 | +| 第 02 版:要闻 | 2 | +| 第 03 版:要闻 | 3 | +| 第 04 版:深度 | 4 | +| 第 05 版:市州 | 5 | +| 第 06 版:理论・学习 | 6 | +| 第 07 版:观察 | 7 | +| 第 08 版:时事 | 8 | +| 第 09 版:中缝 | 9 |`, }; async function handler(ctx) { diff --git a/lib/routes/hnrb/namespace.ts b/lib/routes/hnrb/namespace.ts index 8d0254c6d3dd72..2cf4e0b2687302 100644 --- a/lib/routes/hnrb/namespace.ts +++ b/lib/routes/hnrb/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '湖南日报', url: 'voc.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/hnu/namespace.ts b/lib/routes/hnu/namespace.ts index 58f399dcab01cc..5220e812ec0a23 100644 --- a/lib/routes/hnu/namespace.ts +++ b/lib/routes/hnu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '湖南大学', url: 'scc.hnu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/home-assistant/hacs.ts b/lib/routes/home-assistant/hacs.ts new file mode 100644 index 00000000000000..14580fbbbee251 --- /dev/null +++ b/lib/routes/home-assistant/hacs.ts @@ -0,0 +1,50 @@ +import { Route } from '@/types'; + +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/hacs/repositories', + name: 'HACS Repositories', + maintainers: ['DIYgod'], + categories: ['program-update'], + example: '/home-assistant/hacs/repositories', + handler, +}; + +async function handler() { + const sections = ['appdaemon', 'critical', 'integration', 'theme', 'python_script', 'plugin']; + const dataList = ( + await Promise.all( + sections.map(async (section) => { + const url = `https://data-v2.hacs.xyz/${section}/data.json`; + const response = await ofetch(url); + return Object.values(response); + }) + ) + ).flat() as { + manifest: { + name: string; + }; + manifest_name: string; + description: string; + full_name: string; + domain: string; + stargazers_count: number; + topics?: string[]; + last_updated: string; + last_fetched: number; + }[]; + + return { + title: 'HACS Repositories', + link: `https://www.hacs.xyz/`, + item: dataList.map((item) => ({ + title: item.manifest_name || item.manifest?.name || item.full_name, + description: `${item.domain ? `` : ''}
    ${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}, blogUrl: string, lang: 'zh', link: string}>} */ + const papers = $('div[data-target="BlogThumbnail"]') + .toArray() + .map((item) => { + const props = $(item).data('props') as BlogData; + const link = $(item).find('a').attr('href'); + return { + ...props, + link, + }; + }); + + const items: DataItem[] = papers.map((item) => ({ + title: item.blog.title, + link: `https://huggingface.co${item.link}`, + pubDate: parseDate(item.blog.publishedAt), + author: item.blog.authors.map((author) => ({ + name: author.user, + })), + upvotes: item.blog.upvotes, + image: new URL(item.blog.thumbnail, 'https://huggingface.co').toString(), + })); + + return { + title: 'Huggingface 英文博客', + link: 'https://huggingface.co/blog', + item: items, + }; +} diff --git a/lib/routes/huggingface/namespace.ts b/lib/routes/huggingface/namespace.ts index 10db48d63852af..902e04d0309f11 100644 --- a/lib/routes/huggingface/namespace.ts +++ b/lib/routes/huggingface/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Huggingface', url: 'huggingface.co', + lang: 'en', }; diff --git a/lib/routes/hunanpea/namespace.ts b/lib/routes/hunanpea/namespace.ts index cb5beeecb4879b..2be52b905e7172 100644 --- a/lib/routes/hunanpea/namespace.ts +++ b/lib/routes/hunanpea/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '湖南人事考试网', url: 'rsks.hunanpea.com', + lang: 'zh-CN', }; diff --git a/lib/routes/hunau/gfxy/index.ts b/lib/routes/hunau/gfxy/index.ts index 93477785ee2b4a..035bae0f99f85c 100644 --- a/lib/routes/hunau/gfxy/index.ts +++ b/lib/routes/hunau/gfxy/index.ts @@ -16,7 +16,7 @@ export const route: Route = { }, radar: [ { - source: ['xky.hunau.edu.cn/', 'xky.hunau.edu.cntzgg_8472', 'xky.hunau.edu.cn/:category'], + source: ['xky.hunau.edu.cn/', 'xky.hunau.edu.cn/tzgg_8472', 'xky.hunau.edu.cn/:category'], target: '/:category', }, ], @@ -25,8 +25,8 @@ export const route: Route = { handler, url: 'xky.hunau.edu.cn/', description: `| 分类 | 通知公告 | 学院新闻 | 其他分类通知... | - | ---- | -------- | -------- | --------------- | - | 参数 | tzgg | xyxw | 对应 URL |`, +| ---- | -------- | -------- | --------------- | +| 参数 | tzgg | xyxw | 对应 URL |`, }; async function handler(ctx) { diff --git a/lib/routes/hunau/ied.ts b/lib/routes/hunau/ied.ts index 676e7779aeeeb0..684f033cb8b4c1 100644 --- a/lib/routes/hunau/ied.ts +++ b/lib/routes/hunau/ied.ts @@ -16,7 +16,7 @@ export const route: Route = { }, radar: [ { - source: ['xky.hunau.edu.cn/', 'xky.hunau.edu.cntzgg_8472', 'xky.hunau.edu.cn/:category'], + source: ['xky.hunau.edu.cn/', 'xky.hunau.edu.cn/tzgg_8472', 'xky.hunau.edu.cn/:category'], target: '/:category', }, ], @@ -25,9 +25,9 @@ export const route: Route = { handler, url: 'xky.hunau.edu.cn/', description: `| 分类 | 公告通知 | 新闻快讯 | 其他分类... | - | -------- | -------- | -------- | ----------- | - | type | xwzx | xwzx | 对应 URL | - | category | tzgg | xwkx | 对应 URL |`, +| -------- | -------- | -------- | ----------- | +| type | xwzx | xwzx | 对应 URL | +| category | tzgg | xwkx | 对应 URL |`, }; async function handler(ctx) { diff --git a/lib/routes/hunau/jwc.ts b/lib/routes/hunau/jwc.ts index 939577a7d8ee8d..b79a462086e248 100644 --- a/lib/routes/hunau/jwc.ts +++ b/lib/routes/hunau/jwc.ts @@ -16,7 +16,7 @@ export const route: Route = { }, radar: [ { - source: ['xky.hunau.edu.cn/', 'xky.hunau.edu.cntzgg_8472', 'xky.hunau.edu.cn/:category'], + source: ['xky.hunau.edu.cn/', 'xky.hunau.edu.cn/tzgg_8472', 'xky.hunau.edu.cn/:category'], target: '/:category', }, ], @@ -25,8 +25,8 @@ export const route: Route = { handler, url: 'xky.hunau.edu.cn/', description: `| 分类 | 通知公告 | 教务动态 | 其他教务通知... | - | ---- | -------- | -------- | --------------- | - | 参数 | tzgg | jwds | 对应 URL |`, +| ---- | -------- | -------- | --------------- | +| 参数 | tzgg | jwds | 对应 URL |`, }; async function handler(ctx) { diff --git a/lib/routes/hunau/namespace.ts b/lib/routes/hunau/namespace.ts index 1dae9f578308a1..adf88a400b2752 100644 --- a/lib/routes/hunau/namespace.ts +++ b/lib/routes/hunau/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '湖南农业大学', url: 'gfxy.hunau.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/hunau/xky/index.ts b/lib/routes/hunau/xky/index.ts index c3b91012843b12..65024f3c24c9a5 100644 --- a/lib/routes/hunau/xky/index.ts +++ b/lib/routes/hunau/xky/index.ts @@ -16,7 +16,7 @@ export const route: Route = { }, radar: [ { - source: ['xky.hunau.edu.cn/', 'xky.hunau.edu.cntzgg_8472', 'xky.hunau.edu.cn/:category'], + source: ['xky.hunau.edu.cn/', 'xky.hunau.edu.cn/tzgg_8472', 'xky.hunau.edu.cn/:category'], target: '/:category', }, ], @@ -25,8 +25,8 @@ export const route: Route = { handler, url: 'xky.hunau.edu.cn/', description: `| 分类 | 通知公告 | 学院新闻 | 其他分类通知... | - | ---- | ---------- | -------- | --------------- | - | 参数 | tzgg\_8472 | xyxw | 对应 URL |`, +| ---- | ---------- | -------- | --------------- | +| 参数 | tzgg\_8472 | xyxw | 对应 URL |`, }; async function handler(ctx) { diff --git a/lib/routes/huoxian/namespace.ts b/lib/routes/huoxian/namespace.ts index 351daf6dcd290b..103b6fb1e5d773 100644 --- a/lib/routes/huoxian/namespace.ts +++ b/lib/routes/huoxian/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '火线', url: 'zone.huoxian.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/hupu/all.ts b/lib/routes/hupu/all.ts index 1f343b85179a46..08d74519555013 100644 --- a/lib/routes/hupu/all.ts +++ b/lib/routes/hupu/all.ts @@ -32,9 +32,9 @@ export const route: Route = { name: '热帖', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 更多热帖版面参见 [论坛](https://bbs.hupu.com) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/hupu/bbs.ts b/lib/routes/hupu/bbs.ts index c2ff2e453b76c5..e3cd3aee525b5f 100644 --- a/lib/routes/hupu/bbs.ts +++ b/lib/routes/hupu/bbs.ts @@ -32,9 +32,9 @@ export const route: Route = { name: '社区', maintainers: ['LogicJake', 'nczitzk'], handler, - description: `:::tip + description: `::: tip 更多社区参见 [社区](https://bbs.hupu.com) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/hupu/index.ts b/lib/routes/hupu/index.ts index e30b5b0dfd2bdc..99fa3d88d4203b 100644 --- a/lib/routes/hupu/index.ts +++ b/lib/routes/hupu/index.ts @@ -32,12 +32,12 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| NBA | CBA | 足球 | - | --- | --- | ------ | - | nba | cba | soccer | +| --- | --- | ------ | +| nba | cba | soccer | - :::tip +::: tip 电竞分类参见 [游戏热帖](https://bbs.hupu.com/all-gg) 的对应路由 [\`/hupu/all/all-gg\`](https://rsshub.app/hupu/all/all-gg)。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/hupu/namespace.ts b/lib/routes/hupu/namespace.ts index 766d4e2ff0b300..8455500a9e0b58 100644 --- a/lib/routes/hupu/namespace.ts +++ b/lib/routes/hupu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '虎扑', url: '.hupu.com', + lang: 'zh-CN', }; diff --git a/lib/routes/hust/aia/notice.ts b/lib/routes/hust/aia/notice.ts index cfa165de876ec8..9ae3fc32d57aa1 100755 --- a/lib/routes/hust/aia/notice.ts +++ b/lib/routes/hust/aia/notice.ts @@ -20,8 +20,8 @@ export const route: Route = { maintainers: ['budui'], handler, description: `| 最新 | 党政 | 科研 | 本科生 | 研究生 | 学工思政 | 离退休 | - | ---- | ---- | ---- | ------ | ------ | -------- | ------ | - | | dz | ky | bk | yjs | xgsz | litui |`, +| ---- | ---- | ---- | ------ | ------ | -------- | ------ | +| | dz | ky | bk | yjs | xgsz | litui |`, }; async function handler(ctx) { diff --git a/lib/routes/hust/gs.ts b/lib/routes/hust/gs.ts new file mode 100644 index 00000000000000..6058fb8901a7f8 --- /dev/null +++ b/lib/routes/hust/gs.ts @@ -0,0 +1,282 @@ +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 = 'xwdt' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 16; + + const rootUrl = 'https://gs.hust.edu.cn'; + const currentUrl = new URL(`${category}.htm`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + let items = $('div.btlist ul li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const a = item.find('a'); + const link = a.prop('href'); + + return { + title: a.text(), + pubDate: parseDate(item.find('span.time').text()), + link: link.startsWith('http') ? link : new URL(link, rootUrl).href, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + try { + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const description = $$('div.v_news_content').html(); + + item.title = $$('div.article_title h1, h5').text(); + item.description = description; + item.content = { + html: description, + text: $$('div.v_news_content').text(), + }; + } catch { + // no-empty + } + + return item; + }) + ) + ); + + const title = $('meta[name="keywords"]').prop('content')?.replace(/,/g, ' - ') ?? $('title').text(); + const image = new URL($('div.logo img').prop('src'), rootUrl).href; + + return { + title, + description: title.split(/-/).pop()?.trim(), + link: currentUrl, + item: items, + allowEmpty: true, + image, + }; +}; + +export const route: Route = { + path: '/gs/:category{.+}?', + name: '研究生院', + url: 'gs.hust.edu.cn', + maintainers: ['nczitzk'], + handler, + example: '/hust/gs/xwdt', + parameters: { category: '分类,默认为新闻动态,即 `xwdt`,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [新闻动态](https://gs.hust.edu.cn/xwdt.htm),网址为 \`https://gs.hust.edu.cn/xwdt.htm\`。截取 \`https://gs.hust.edu.cn/\` 到末尾 \`.htm\` 的部分 \`xwdt\` 作为参数填入,此时路由为 [\`/hust/gs/xwdt\`](https://rsshub.app/hust/gs/xwdt)。 +::: + +| [新闻动态](https://gs.hust.edu.cn/xwdt.htm) | [研究生服务专区](https://gs.hust.edu.cn/yjsfwzq.htm) | [综合管理](https://gs.hust.edu.cn/gzzd/zhgl.htm) | +| ------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------- | +| [xwdt](https://rsshub.app/hust/gs/xwdt) | [yjsfwzq](https://rsshub.app/hust/gs/yjsfwzq) | [gzzd/zhgl](https://rsshub.app/hust/gs/gzzd/zhgl) | + +#### [通知公告](https://gs.hust.edu.cn/tzgg/kcjksap.htm) + +| [课程及考试安排](https://gs.hust.edu.cn/tzgg/kcjksap.htm) | [国际交流](https://gs.hust.edu.cn/tzgg/gjjl.htm) | [学位工作](https://gs.hust.edu.cn/tzgg/xwgz.htm) | [同济医学院](https://gs.hust.edu.cn/tzgg/tjyxy.htm) | [其他](https://gs.hust.edu.cn/tzgg/qt.htm) | +| --------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------- | --------------------------------------------- | +| [tzgg/kcjksap](https://rsshub.app/hust/gs/tzgg/kcjksap) | [tzgg/gjjl](https://rsshub.app/hust/gs/tzgg/gjjl) | [tzgg/xwgz](https://rsshub.app/hust/gs/tzgg/xwgz) | [tzgg/tjyxy](https://rsshub.app/hust/gs/tzgg/tjyxy) | [tzgg/qt](https://rsshub.app/hust/gs/tzgg/qt) | + +#### [学籍管理](https://gs.hust.edu.cn/pygz/zbjs1/xjyd.htm) + +| [学籍异动](https://gs.hust.edu.cn/pygz/zbjs1/xjyd.htm) | [毕业管理](https://gs.hust.edu.cn/pygz/zbjs1/bygl.htm) | +| ------------------------------------------------------------- | ------------------------------------------------------------- | +| [pygz/zbjs1/xjyd](https://rsshub.app/hust/gs/pygz/zbjs1/xjyd) | [pygz/zbjs1/bygl](https://rsshub.app/hust/gs/pygz/zbjs1/bygl) | + +#### [教学管理](https://gs.hust.edu.cn/pygz/zbjs13/jxyj.htm) + +| [教学研究](https://gs.hust.edu.cn/pygz/zbjs13/jxyj.htm) | [课程教材](https://gs.hust.edu.cn/pygz/zbjs13/kcjc.htm) | [教学安排](https://gs.hust.edu.cn/pygz/zbjs13/jxap.htm) | [课表查询](https://gs.hust.edu.cn/pygz/zbjs13/kbcx.htm) | +| --------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | +| [pygz/zbjs13/jxyj](https://rsshub.app/hust/gs/pygz/zbjs13/jxyj) | [pygz/zbjs13/kcjc](https://rsshub.app/hust/gs/pygz/zbjs13/kcjc) | [pygz/zbjs13/jxap](https://rsshub.app/hust/gs/pygz/zbjs13/jxap) | [pygz/zbjs13/kbcx](https://rsshub.app/hust/gs/pygz/zbjs13/kbcx) | + +#### [培养过程](https://gs.hust.edu.cn/pygz/pygc.htm) + +| [培养方案](https://gs.hust.edu.cn/pygz/pygc/pyfa.htm) | [硕博连读](https://gs.hust.edu.cn/pygz/pygc/sbld.htm) | +| ----------------------------------------------------------- | ----------------------------------------------------------- | +| [pygz/pygc/pyfa](https://rsshub.app/hust/gs/pygz/pygc/pyfa) | [pygz/pygc/sbld](https://rsshub.app/hust/gs/pygz/pygc/sbld) | + +#### [国际交流](https://gs.hust.edu.cn/pygz/zbjs11/gjgpxm.htm) + +| [国家公派项目](https://gs.hust.edu.cn/pygz/zbjs11/gjgpxm.htm) | [国际学术会议](https://gs.hust.edu.cn/pygz/zbjs11/gjxshy.htm) | [校际合作项目](https://gs.hust.edu.cn/pygz/zbjs11/xjhzxm.htm) | [国际交流与合作办事流程](https://gs.hust.edu.cn/pygz/zbjs11/gjjlyhzbslc.htm) | +| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| [pygz/zbjs11/gjgpxm](https://rsshub.app/hust/gs/pygz/zbjs11/gjgpxm) | [pygz/zbjs11/gjxshy](https://rsshub.app/hust/gs/pygz/zbjs11/gjxshy) | [pygz/zbjs11/xjhzxm](https://rsshub.app/hust/gs/pygz/zbjs11/xjhzxm) | [pygz/zbjs11/gjjlyhzbslc](https://rsshub.app/hust/gs/pygz/zbjs11/gjjlyhzbslc) | + +#### [专业学位](https://gs.hust.edu.cn/pygz/zbjs111/xwsqdml.htm) + +| [学位授权点目录](https://gs.hust.edu.cn/pygz/zbjs111/xwsqdml.htm) | [专业学位建设](https://gs.hust.edu.cn/pygz/zbjs111/zyxwjs.htm) | [特色培养](https://gs.hust.edu.cn/pygz/zbjs111/tspy.htm) | +| ----------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------- | +| [pygz/zbjs111/xwsqdml](https://rsshub.app/hust/gs/pygz/zbjs111/xwsqdml) | [pygz/zbjs111/zyxwjs](https://rsshub.app/hust/gs/pygz/zbjs111/zyxwjs) | [pygz/zbjs111/tspy](https://rsshub.app/hust/gs/pygz/zbjs111/tspy) | + +#### [学位工作](https://gs.hust.edu.cn/xwgz/xwdjs.htm) + +| [学位点建设](https://gs.hust.edu.cn/xwgz/xwdjs.htm) | [学位授予](https://gs.hust.edu.cn/xwgz/xwsy.htm) | [导师队伍](https://gs.hust.edu.cn/xwgz/dsdw.htm) | +| --------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- | +| [xwgz/xwdjs](https://rsshub.app/hust/gs/xwgz/xwdjs) | [xwgz/xwsy](https://rsshub.app/hust/gs/xwgz/xwsy) | [xwgz/dsdw](https://rsshub.app/hust/gs/xwgz/dsdw) | + `, + categories: ['university'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['gs.hust.edu.cn/:category'], + target: (params) => { + const category = params.category; + + return `/hust/gs${category ? `/${category}` : ''}`; + }, + }, + { + title: '新闻动态', + source: ['gs.hust.edu.cn/xwdt.htm'], + target: '/gs/xwdt', + }, + { + title: '研究生服务专区', + source: ['gs.hust.edu.cn/yjsfwzq.htm'], + target: '/gs/yjsfwzq', + }, + { + title: '综合管理', + source: ['gs.hust.edu.cn/gzzd/zhgl.htm'], + target: '/gs/gzzd/zhgl', + }, + { + title: '通知公告 - 课程及考试安排', + source: ['gs.hust.edu.cn/tzgg/kcjksap.htm'], + target: '/gs/tzgg/kcjksap', + }, + { + title: '通知公告 - 国际交流', + source: ['gs.hust.edu.cn/tzgg/gjjl.htm'], + target: '/gs/tzgg/gjjl', + }, + { + title: '通知公告 - 学位工作', + source: ['gs.hust.edu.cn/tzgg/xwgz.htm'], + target: '/gs/tzgg/xwgz', + }, + { + title: '通知公告 - 同济医学院', + source: ['gs.hust.edu.cn/tzgg/tjyxy.htm'], + target: '/gs/tzgg/tjyxy', + }, + { + title: '通知公告 - 其他', + source: ['gs.hust.edu.cn/tzgg/qt.htm'], + target: '/gs/tzgg/qt', + }, + { + title: '学籍管理 - 学籍异动', + source: ['gs.hust.edu.cn/pygz/zbjs1/xjyd.htm'], + target: '/gs/pygz/zbjs1/xjyd', + }, + { + title: '学籍管理 - 毕业管理', + source: ['gs.hust.edu.cn/pygz/zbjs1/bygl.htm'], + target: '/gs/pygz/zbjs1/bygl', + }, + { + title: '教学管理 - 教学研究', + source: ['gs.hust.edu.cn/pygz/zbjs13/jxyj.htm'], + target: '/gs/pygz/zbjs13/jxyj', + }, + { + title: '教学管理 - 课程教材', + source: ['gs.hust.edu.cn/pygz/zbjs13/kcjc.htm'], + target: '/gs/pygz/zbjs13/kcjc', + }, + { + title: '教学管理 - 教学安排', + source: ['gs.hust.edu.cn/pygz/zbjs13/jxap.htm'], + target: '/gs/pygz/zbjs13/jxap', + }, + { + title: '教学管理 - 课表查询', + source: ['gs.hust.edu.cn/pygz/zbjs13/kbcx.htm'], + target: '/gs/pygz/zbjs13/kbcx', + }, + { + title: '培养过程 - 培养方案', + source: ['gs.hust.edu.cn/pygz/pygc/pyfa.htm'], + target: '/gs/pygz/pygc/pyfa', + }, + { + title: '培养过程 - 硕博连读', + source: ['gs.hust.edu.cn/pygz/pygc/sbld.htm'], + target: '/gs/pygz/pygc/sbld', + }, + { + title: '国际交流 - 国家公派项目', + source: ['gs.hust.edu.cn/pygz/zbjs11/gjgpxm.htm'], + target: '/gs/pygz/zbjs11/gjgpxm', + }, + { + title: '国际交流 - 国际学术会议', + source: ['gs.hust.edu.cn/pygz/zbjs11/gjxshy.htm'], + target: '/gs/pygz/zbjs11/gjxshy', + }, + { + title: '国际交流 - 校际合作项目', + source: ['gs.hust.edu.cn/pygz/zbjs11/xjhzxm.htm'], + target: '/gs/pygz/zbjs11/xjhzxm', + }, + { + title: '国际交流 - 国际交流与合作办事流程', + source: ['gs.hust.edu.cn/pygz/zbjs11/gjjlyhzbslc.htm'], + target: '/gs/pygz/zbjs11/gjjlyhzbslc', + }, + { + title: '专业学位 - 学位授权点目录', + source: ['gs.hust.edu.cn/pygz/zbjs111/xwsqdml.htm'], + target: '/gs/pygz/zbjs111/xwsqdml', + }, + { + title: '专业学位 - 专业学位建设', + source: ['gs.hust.edu.cn/pygz/zbjs111/zyxwjs.htm'], + target: '/gs/pygz/zbjs111/zyxwjs', + }, + { + title: '专业学位 - 特色培养', + source: ['gs.hust.edu.cn/pygz/zbjs111/tspy.htm'], + target: '/gs/pygz/zbjs111/tspy', + }, + { + title: '学位工作 - 学位点建设', + source: ['gs.hust.edu.cn/xwgz/xwdjs.htm'], + target: '/gs/xwgz/xwdjs', + }, + { + title: '学位工作 - 学位授予', + source: ['gs.hust.edu.cn/xwgz/xwsy.htm'], + target: '/gs/xwgz/xwsy', + }, + { + title: '学位工作 - 导师队伍', + source: ['gs.hust.edu.cn/xwgz/dsdw.htm'], + target: '/gs/xwgz/dsdw', + }, + ], +}; diff --git a/lib/routes/hust/mse.ts b/lib/routes/hust/mse.ts new file mode 100644 index 00000000000000..df8741c6a340e3 --- /dev/null +++ b/lib/routes/hust/mse.ts @@ -0,0 +1,575 @@ +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 = 'sylm/xyxw' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 11; + + const domain = 'mse.hust.edu.cn'; + const rootUrl = `https://${domain}`; + const currentUrl = new URL(`${category}.htm`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('div.list ul li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const a = item.find('a'); + + return { + title: a.text(), + pubDate: parseDate(item.find('span.time').text()), + link: new URL(a.prop('href'), currentUrl).href, + language, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + if (!item.link.includes(domain)) { + return item; + } + + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const title = $$('h1.article_title').text() || $$('div.article_title').text() || item.title; + const description = $$('div.v_news_content').html(); + + for (const d of $$('div.article_data').contents().toArray()) { + const data = $$(d).text(); + + if (!item.pubDate && data.startsWith('发布')) { + const pubDate = data.split(/:/)?.pop(); + item.pubDate = pubDate ? parseDate(pubDate) : item.pubDate; + } else if (!item.author && data.startsWith('作者')) { + item.author = data.split(/:/)?.pop() ?? undefined; + } else if (!item.author && data.startsWith('编辑')) { + item.author = data.split(/:/)?.pop() ?? undefined; + } + } + + item.title = title; + item.description = description; + item.content = { + html: description, + text: $$('div.v_news_content').text(), + }; + item.language = language; + + return item; + }) + ) + ); + + const title = $('meta[name="keywords"]').prop('content')?.replace(/,/g, ' - ') ?? $('title').text(); + const image = new URL($('div.logo img').prop('src'), rootUrl).href; + + return { + title, + description: title.split(/-/).pop()?.trim(), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: title.split(/-/)[0]?.trim(), + language, + }; +}; + +export const route: Route = { + path: '/mse/:category{.+}?', + name: '机械科学与工程学院', + url: 'mse.hust.edu.cn', + maintainers: ['nczitzk'], + handler, + example: '/hust/mse/sylm/xyxw', + parameters: { category: '分类,默认为 `sylm/xyxw`,即学院新闻,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [通知公告](https://mse.hust.edu.cn/sylm/tzgg.htm),网址为 \`https://mse.hust.edu.cn/sylm/tzgg.htm\`。截取 \`https://mse.hust.edu.cn/\` 到末尾 \`.html\` 的部分 \`sylm/tzgg\` 作为参数填入,此时路由为 [\`/hust/mse/sylm/tzgg\`](https://rsshub.app/hust/mse/sylm/tzgg)。 +::: + +#### [首页栏目](https://mse.hust.edu.cn/xyxw.htm) + +| [学院新闻](https://mse.hust.edu.cn/xyxw.htm) | [通知公告](https://mse.hust.edu.cn/tzgg.htm) | [招生招聘](https://mse.hust.edu.cn/zszp.htm) | [媒体聚焦](https://mse.hust.edu.cn/mtjj.htm) | +| -------------------------------------------- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------- | +| [xyxw](https://rsshub.app/hust/mse/xyxw) | [tzgg](https://rsshub.app/hust/mse/tzgg) | [zszp](https://rsshub.app/hust/mse/zszp) | [mtjj](https://rsshub.app/hust/mse/mtjj) | + +| [期刊动态](https://mse.hust.edu.cn/qkdt.htm) | [学术活动](https://mse.hust.edu.cn/xshd.htm) | [师生天地](https://mse.hust.edu.cn/sstd.htm) | [STAR风采](https://mse.hust.edu.cn/STARfc.htm) | +| -------------------------------------------- | -------------------------------------------- | -------------------------------------------- | ---------------------------------------------- | +| [qkdt](https://rsshub.app/hust/mse/qkdt) | [xshd](https://rsshub.app/hust/mse/xshd) | [sstd](https://rsshub.app/hust/mse/sstd) | [STARfc](https://rsshub.app/hust/mse/STARfc) | + +
    +更多分类 + +#### [理论学习](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) | + +
    + `, + categories: ['university'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['mse.hust.edu.cn/:category?'], + target: (params) => { + const category = params.category; + + return `/hust/mse${category ? `/${category}` : ''}`; + }, + }, + { + title: '首页栏目 - 学院新闻', + source: ['mse.hust.edu.cn/xyxw.htm'], + target: '/mse/xyxw', + }, + { + title: '首页栏目 - 通知公告', + source: ['mse.hust.edu.cn/tzgg.htm'], + target: '/mse/tzgg', + }, + { + title: '首页栏目 - 招生招聘', + source: ['mse.hust.edu.cn/zszp.htm'], + target: '/mse/zszp', + }, + { + title: '首页栏目 - 媒体聚焦', + source: ['mse.hust.edu.cn/mtjj.htm'], + target: '/mse/mtjj', + }, + { + title: '首页栏目 - 期刊动态', + source: ['mse.hust.edu.cn/qkdt.htm'], + target: '/mse/qkdt', + }, + { + title: '首页栏目 - 学术活动', + source: ['mse.hust.edu.cn/xshd.htm'], + target: '/mse/xshd', + }, + { + title: '首页栏目 - 师生天地', + source: ['mse.hust.edu.cn/sstd.htm'], + target: '/mse/sstd', + }, + { + title: '首页栏目 - STAR风采', + source: ['mse.hust.edu.cn/STARfc.htm'], + target: '/mse/STARfc', + }, + { + title: '理论学习 - 党务动态', + source: ['mse.hust.edu.cn/llxx1/dwdt/djxw.htm'], + target: '/mse/llxx1/dwdt/djxw', + }, + { + title: '理论学习 - 共青团', + source: ['mse.hust.edu.cn/llxx1/gqt/xwdt.htm'], + target: '/mse/llxx1/gqt/xwdt', + }, + { + title: '理论学习 - 工会组织', + source: ['mse.hust.edu.cn/llxx1/ghzz/xwgg.htm'], + target: '/mse/llxx1/ghzz/xwgg', + }, + { + title: '理论学习 - 学习参考', + source: ['mse.hust.edu.cn/llxx1/xxck.htm'], + target: '/mse/llxx1/xxck', + }, + { + title: '理论学习 - 资料汇编', + source: ['mse.hust.edu.cn/llxx1/zlhb.htm'], + target: '/mse/llxx1/zlhb', + }, + { + title: '理论学习 - 其他群团', + source: ['mse.hust.edu.cn/llxx1/ghzz1/lmmc.htm'], + target: '/mse/llxx1/ghzz1/lmmc', + }, + { + title: '师资队伍 - 人才招聘', + source: ['mse.hust.edu.cn/szdw/rczp.htm'], + target: '/mse/szdw/rczp', + }, + { + title: '师资队伍 - 常用下载', + source: ['mse.hust.edu.cn/szdw/cyxz.htm'], + target: '/mse/szdw/cyxz', + }, + { + title: '人才培养 - 本科生教育', + source: ['mse.hust.edu.cn/rcpy/bksjy.htm'], + target: '/mse/rcpy/bksjy', + }, + { + title: '人才培养 - 研究生教育', + source: ['mse.hust.edu.cn/rcpy/yjsjy.htm'], + target: '/mse/rcpy/yjsjy', + }, + { + title: '人才培养 - 学生工作', + source: ['mse.hust.edu.cn/rcpy/xsg_z.htm'], + target: '/mse/rcpy/xsg_z', + }, + { + title: '人才培养 - 机械创新基地', + source: ['mse.hust.edu.cn/rcpy/jxcxjd.htm'], + target: '/mse/rcpy/jxcxjd', + }, + { + title: '人才培养 - 常用下载', + source: ['mse.hust.edu.cn/rcpy/cyxz.htm'], + target: '/mse/rcpy/cyxz', + }, + { + title: '科学研究 - 科研动态', + source: ['mse.hust.edu.cn/kxyj/kydt.htm'], + target: '/mse/kxyj/kydt', + }, + { + title: '科学研究 - 安全管理', + source: ['mse.hust.edu.cn/kxyj/aqgl.htm'], + target: '/mse/kxyj/aqgl', + }, + { + title: '科学研究 - 设备开放', + source: ['mse.hust.edu.cn/kxyj/sbkf.htm'], + target: '/mse/kxyj/sbkf', + }, + { + title: '科学研究 - 科研成果', + source: ['mse.hust.edu.cn/kxyj/kycg.htm'], + target: '/mse/kxyj/kycg', + }, + { + title: '科学研究 - 常用下载', + source: ['mse.hust.edu.cn/kxyj/cyxz.htm'], + target: '/mse/kxyj/cyxz', + }, + { + title: '社会服务 - 驻外研究院', + source: ['mse.hust.edu.cn/shfw/zwyjy.htm'], + target: '/mse/shfw/zwyjy', + }, + { + title: '社会服务 - 产业公司', + source: ['mse.hust.edu.cn/shfw/cygs.htm'], + target: '/mse/shfw/cygs', + }, + { + title: '合作交流 - 专家来访', + source: ['mse.hust.edu.cn/hzjl/zjlf.htm'], + target: '/mse/hzjl/zjlf', + }, + { + title: '合作交流 - 师生出访', + source: ['mse.hust.edu.cn/hzjl/sscf.htm'], + target: '/mse/hzjl/sscf', + }, + { + title: '合作交流 - 项目合作', + source: ['mse.hust.edu.cn/hzjl/xmhz.htm'], + target: '/mse/hzjl/xmhz', + }, + { + title: '合作交流 - 国际会议', + source: ['mse.hust.edu.cn/hzjl/gjhy.htm'], + target: '/mse/hzjl/gjhy', + }, + { + title: '合作交流 - 常用下载', + source: ['mse.hust.edu.cn/hzjl/cyxz.htm'], + target: '/mse/hzjl/cyxz', + }, + { + title: '校友专栏 - 校友动态', + source: ['mse.hust.edu.cn/xyzl/xydt.htm'], + target: '/mse/xyzl/xydt', + }, + { + title: '校友专栏 - 杰出校友', + source: ['mse.hust.edu.cn/xyzl/jcxy.htm'], + target: '/mse/xyzl/jcxy', + }, + { + title: '校友专栏 - 校友名录', + source: ['mse.hust.edu.cn/xyzl/xyml.htm'], + target: '/mse/xyzl/xyml', + }, + { + title: '校友专栏 - 校友照片', + source: ['mse.hust.edu.cn/xyzl/xyzp.htm'], + target: '/mse/xyzl/xyzp', + }, + { + title: '校友专栏 - 服务校友', + source: ['mse.hust.edu.cn/xyzl/fwxy.htm'], + target: '/mse/xyzl/fwxy', + }, + { + title: '校友专栏 - 常用下载', + source: ['mse.hust.edu.cn/xyzl/cyxz.htm'], + target: '/mse/xyzl/cyxz', + }, + { + title: '理论学习 - 党务动态', + source: ['mse.hust.edu.cn/llxx1/dwdt/djxw.htm'], + target: '/mse/llxx1/dwdt/djxw', + }, + { + title: '理论学习 - 共青团', + source: ['mse.hust.edu.cn/llxx1/gqt/xwdt.htm'], + target: '/mse/llxx1/gqt/xwdt', + }, + { + title: '理论学习 - 工会组织', + source: ['mse.hust.edu.cn/llxx1/ghzz/xwgg.htm'], + target: '/mse/llxx1/ghzz/xwgg', + }, + { + title: '理论学习 - 学习参考', + source: ['mse.hust.edu.cn/llxx1/xxck.htm'], + target: '/mse/llxx1/xxck', + }, + { + title: '理论学习 - 资料汇编', + source: ['mse.hust.edu.cn/llxx1/zlhb.htm'], + target: '/mse/llxx1/zlhb', + }, + { + title: '理论学习 - 其他群团', + source: ['mse.hust.edu.cn/llxx1/ghzz1/lmmc.htm'], + target: '/mse/llxx1/ghzz1/lmmc', + }, + { + title: '师资队伍 - 人才招聘', + source: ['mse.hust.edu.cn/szdw/rczp.htm'], + target: '/mse/szdw/rczp', + }, + { + title: '师资队伍 - 常用下载', + source: ['mse.hust.edu.cn/szdw/cyxz.htm'], + target: '/mse/szdw/cyxz', + }, + { + title: '人才培养 - 本科生教育', + source: ['mse.hust.edu.cn/rcpy/bksjy.htm'], + target: '/mse/rcpy/bksjy', + }, + { + title: '人才培养 - 研究生教育', + source: ['mse.hust.edu.cn/rcpy/yjsjy.htm'], + target: '/mse/rcpy/yjsjy', + }, + { + title: '人才培养 - 学生工作', + source: ['mse.hust.edu.cn/rcpy/xsg_z.htm'], + target: '/mse/rcpy/xsg_z', + }, + { + title: '人才培养 - 机械创新基地', + source: ['mse.hust.edu.cn/rcpy/jxcxjd.htm'], + target: '/mse/rcpy/jxcxjd', + }, + { + title: '人才培养 - 常用下载', + source: ['mse.hust.edu.cn/rcpy/cyxz.htm'], + target: '/mse/rcpy/cyxz', + }, + { + title: '科学研究 - 科研动态', + source: ['mse.hust.edu.cn/kxyj/kydt.htm'], + target: '/mse/kxyj/kydt', + }, + { + title: '科学研究 - 安全管理', + source: ['mse.hust.edu.cn/kxyj/aqgl.htm'], + target: '/mse/kxyj/aqgl', + }, + { + title: '科学研究 - 设备开放', + source: ['mse.hust.edu.cn/kxyj/sbkf.htm'], + target: '/mse/kxyj/sbkf', + }, + { + title: '科学研究 - 科研成果', + source: ['mse.hust.edu.cn/kxyj/kycg.htm'], + target: '/mse/kxyj/kycg', + }, + { + title: '科学研究 - 常用下载', + source: ['mse.hust.edu.cn/kxyj/cyxz.htm'], + target: '/mse/kxyj/cyxz', + }, + { + title: '社会服务 - 驻外研究院', + source: ['mse.hust.edu.cn/shfw/zwyjy.htm'], + target: '/mse/shfw/zwyjy', + }, + { + title: '社会服务 - 产业公司', + source: ['mse.hust.edu.cn/shfw/cygs.htm'], + target: '/mse/shfw/cygs', + }, + { + title: '合作交流 - 专家来访', + source: ['mse.hust.edu.cn/hzjl/zjlf.htm'], + target: '/mse/hzjl/zjlf', + }, + { + title: '合作交流 - 师生出访', + source: ['mse.hust.edu.cn/hzjl/sscf.htm'], + target: '/mse/hzjl/sscf', + }, + { + title: '合作交流 - 项目合作', + source: ['mse.hust.edu.cn/hzjl/xmhz.htm'], + target: '/mse/hzjl/xmhz', + }, + { + title: '合作交流 - 国际会议', + source: ['mse.hust.edu.cn/hzjl/gjhy.htm'], + target: '/mse/hzjl/gjhy', + }, + { + title: '合作交流 - 常用下载', + source: ['mse.hust.edu.cn/hzjl/cyxz.htm'], + target: '/mse/hzjl/cyxz', + }, + { + title: '校友专栏 - 校友动态', + source: ['mse.hust.edu.cn/xyzl/xydt.htm'], + target: '/mse/xyzl/xydt', + }, + { + title: '校友专栏 - 杰出校友', + source: ['mse.hust.edu.cn/xyzl/jcxy.htm'], + target: '/mse/xyzl/jcxy', + }, + { + title: '校友专栏 - 校友名录', + source: ['mse.hust.edu.cn/xyzl/xyml.htm'], + target: '/mse/xyzl/xyml', + }, + { + title: '校友专栏 - 校友照片', + source: ['mse.hust.edu.cn/xyzl/xyzp.htm'], + target: '/mse/xyzl/xyzp', + }, + { + title: '校友专栏 - 服务校友', + source: ['mse.hust.edu.cn/xyzl/fwxy.htm'], + target: '/mse/xyzl/fwxy', + }, + { + title: '校友专栏 - 常用下载', + source: ['mse.hust.edu.cn/xyzl/cyxz.htm'], + target: '/mse/xyzl/cyxz', + }, + ], +}; diff --git a/lib/routes/hust/namespace.ts b/lib/routes/hust/namespace.ts index c4f337abdae647..2d9ffbbaffffc3 100644 --- a/lib/routes/hust/namespace.ts +++ b/lib/routes/hust/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '华中科技大学', - url: 'aia.hust.edu.cn', + url: 'hust.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/huxiu/brief-column.ts b/lib/routes/huxiu/brief-column.ts index ffbca49210c473..5648db7a0df7e5 100644 --- a/lib/routes/huxiu/brief-column.ts +++ b/lib/routes/huxiu/brief-column.ts @@ -6,7 +6,7 @@ import { apiBriefRootUrl, processItems, fetchBriefColumnData } from './util'; export const route: Route = { path: '/briefcolumn/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/huxiu/briefcolumn/1', parameters: { id: '简报 id,可在对应简报页 URL 中找到' }, features: { diff --git a/lib/routes/huxiu/channel.ts b/lib/routes/huxiu/channel.ts index aed26052443299..0bea4b479f4696 100644 --- a/lib/routes/huxiu/channel.ts +++ b/lib/routes/huxiu/channel.ts @@ -6,7 +6,7 @@ import { rootUrl, apiArticleRootUrl, processItems, fetchData } from './util'; export const route: Route = { path: ['/article', '/channel/:id?'], - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/huxiu/article', parameters: {}, features: { @@ -26,16 +26,16 @@ export const route: Route = { maintainers: ['HenryQW', 'nczitzk'], handler, description: `| 视频 | 车与出行 | 年轻一代 | 十亿消费者 | 前沿科技 | - | ---- | -------- | -------- | ---------- | -------- | - | 10 | 21 | 106 | 103 | 105 | +| ---- | -------- | -------- | ---------- | -------- | +| 10 | 21 | 106 | 103 | 105 | - | 财经 | 娱乐淘金 | 医疗健康 | 文化教育 | 出海 | - | ---- | -------- | -------- | -------- | ---- | - | 115 | 22 | 111 | 113 | 114 | +| 财经 | 娱乐淘金 | 医疗健康 | 文化教育 | 出海 | +| ---- | -------- | -------- | -------- | ---- | +| 115 | 22 | 111 | 113 | 114 | - | 金融地产 | 企业服务 | 创业维艰 | 社交通讯 | 全球热点 | 生活腔调 | - | -------- | -------- | -------- | -------- | -------- | -------- | - | 102 | 110 | 2 | 112 | 107 | 4 |`, +| 金融地产 | 企业服务 | 创业维艰 | 社交通讯 | 全球热点 | 生活腔调 | +| -------- | -------- | -------- | -------- | -------- | -------- | +| 102 | 110 | 2 | 112 | 107 | 4 |`, url: 'huxiu.com/article', }; diff --git a/lib/routes/huxiu/club.ts b/lib/routes/huxiu/club.ts index 3714685fa8e53f..fad41f0f44b1b0 100644 --- a/lib/routes/huxiu/club.ts +++ b/lib/routes/huxiu/club.ts @@ -6,7 +6,10 @@ import { apiBriefRootUrl, processItems, fetchClubData } from './util'; export const route: Route = { path: '/club/:id', - name: 'Unknown', + name: '源流', + categories: ['new-media', 'popular'], + example: '/huxiu/club/2029', + parameters: { id: '俱乐部 id,可在对应俱乐部页 URL 中找到' }, maintainers: ['nczitzk'], handler, }; diff --git a/lib/routes/huxiu/collection.ts b/lib/routes/huxiu/collection.ts index 0112c44a59f9a0..2af03e2efd2e04 100644 --- a/lib/routes/huxiu/collection.ts +++ b/lib/routes/huxiu/collection.ts @@ -6,7 +6,7 @@ import { rootUrl, apiArticleRootUrl, processItems, fetchData } from './util'; export const route: Route = { path: '/collection/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/huxiu/collection/212', parameters: { id: '文集 id,可在对应文集页 URL 中找到' }, features: { diff --git a/lib/routes/huxiu/member.ts b/lib/routes/huxiu/member.ts index 1fca5316e42847..244084b4a9b78a 100644 --- a/lib/routes/huxiu/member.ts +++ b/lib/routes/huxiu/member.ts @@ -6,12 +6,15 @@ import { rootUrl, apiMemberRootUrl, processItems, fetchData } from './util'; export const route: Route = { path: ['/author/:id/:type?', '/member/:id/:type?'], - name: 'Unknown', - maintainers: [], + name: '用户', + example: '/huxiu/member/2313050', + categories: ['new-media', 'popular'], + parameters: { id: '用户 id,可在对应用户页 URL 中找到' }, + maintainers: ['nczitzk'], handler, description: `| TA 的文章 | TA 的 24 小时 | - | --------- | ------------- | - | article | moment |`, +| --------- | ------------- | +| article | moment |`, }; async function handler(ctx) { diff --git a/lib/routes/huxiu/moment.ts b/lib/routes/huxiu/moment.ts index 01336d063b2b79..2a883a709d12a4 100644 --- a/lib/routes/huxiu/moment.ts +++ b/lib/routes/huxiu/moment.ts @@ -6,7 +6,7 @@ import { rootUrl, apiMomentRootUrl, processItems, fetchData } from './util'; export const route: Route = { path: '/moment', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/huxiu/moment', parameters: {}, features: { diff --git a/lib/routes/huxiu/namespace.ts b/lib/routes/huxiu/namespace.ts index c9c55006bf8b81..27bdf507aa7c07 100644 --- a/lib/routes/huxiu/namespace.ts +++ b/lib/routes/huxiu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '虎嗅', url: 'huxiu.com', + lang: 'zh-CN', }; diff --git a/lib/routes/huxiu/search.ts b/lib/routes/huxiu/search.ts index dc7be05140cfa1..84a97f492f5e67 100644 --- a/lib/routes/huxiu/search.ts +++ b/lib/routes/huxiu/search.ts @@ -6,7 +6,7 @@ import { rootUrl, apiSearchRootUrl, generateSignature, processItems, fetchData } export const route: Route = { path: '/search/:keyword', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/huxiu/search/生活', parameters: { keyword: '关键字' }, features: { diff --git a/lib/routes/huxiu/tag.ts b/lib/routes/huxiu/tag.ts index 16e32a306ae996..8da32d5b57ffb8 100644 --- a/lib/routes/huxiu/tag.ts +++ b/lib/routes/huxiu/tag.ts @@ -6,7 +6,7 @@ import { rootUrl, processItems, fetchData } from './util'; export const route: Route = { path: '/tag/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/huxiu/tag/291', parameters: { id: '标签 id,可在对应标签页 URL 中找到' }, features: { diff --git a/lib/routes/huxiu/util.ts b/lib/routes/huxiu/util.ts index 216865a44d1eda..b69659dfda586a 100644 --- a/lib/routes/huxiu/util.ts +++ b/lib/routes/huxiu/util.ts @@ -30,15 +30,17 @@ const cleanUpHTML = (data) => { $('em.vote__bar, div.vote__btn, div.vote__time').remove(); $('p img').each((_, e) => { e = $(e); - e.parent().replaceWith( - art(path.join(__dirname, 'templates/description.art'), { - image: { - src: (e.prop('src') ?? e.prop('_src')).split(/\?/)[0], - width: e.prop('data-w'), - height: e.prop('data-h'), - }, - }) - ); + if ((e.prop('src') ?? e.prop('_src')) !== undefined) { + e.parent().replaceWith( + art(path.join(__dirname, 'templates/description.art'), { + image: { + src: (e.prop('src') ?? e.prop('_src')).split(/\?/)[0], + width: e.prop('data-w'), + height: e.prop('data-h'), + }, + }) + ); + } }); $('p, span').each((_, e) => { e = $(e); @@ -188,7 +190,7 @@ const fetchItem = async (item) => { const { data: detailResponse } = await got(item.link); const state = parseInitialState(detailResponse); - const data = state.briefStoreModule?.brief_detail.brief ?? state.articleDetail?.articleDetail ?? undefined; + const data = state?.briefStoreModule?.brief_detail.brief ?? state?.articleDetail?.articleDetail ?? undefined; if (!data) { return item; @@ -376,7 +378,7 @@ const processItems = async (items, limit, tryGet) => { }), author: item.user_info?.username ?? item.brief_column?.name ?? item.author_info?.username ?? item.author, guid, - pubDate: item.publish_time ?? item.dateline ? parseDate(item.publish_time ?? item.dateline, 'X') : undefined, + pubDate: (item.publish_time ?? item.dateline) ? parseDate(item.publish_time ?? item.dateline, 'X') : undefined, upvotes: Number.parseInt(upvotes, 10), downvotes: Number.parseInt(downvotes, 10), comments: Number.parseInt(comments, 10), diff --git a/lib/routes/arknights/announce.ts b/lib/routes/hypergryph/arknights/announce.ts similarity index 59% rename from lib/routes/arknights/announce.ts rename to lib/routes/hypergryph/arknights/announce.ts index 5056013070bf8e..c45360cbb2b484 100644 --- a/lib/routes/arknights/announce.ts +++ b/lib/routes/hypergryph/arknights/announce.ts @@ -1,59 +1,62 @@ 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'; +import { config } from '@/config'; + +type AnnounceItem = { + announceId: string; + title: string; + isWebUrl: boolean; + webUrl: string; + day: number; + month: number; + group: string; +}; export const route: Route = { - path: '/announce/:platform?/:group?', + path: '/arknights/announce/:platform?/:group?', categories: ['game'], - example: '/arknights/announce', + example: '/hypergryph/arknights/announce', parameters: { platform: '平台,默认为 Android', group: '分组,默认为 ALL' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - name: '游戏内公告', + name: '明日方舟 - 游戏内公告', maintainers: ['swwind'], handler, description: `平台 - | 安卓服 | iOS 服 | B 服 | - | :-----: | :----: | :------: | - | Android | IOS | Bilibili | +| 安卓服 | iOS 服 | B 服 | +| :-----: | :----: | :------: | +| Android | IOS | Bilibili | 分组 - | 全部 | 系统公告 | 活动公告 | - | :--: | :------: | :------: | - | ALL | SYSTEM | ACTIVITY |`, +| 全部 | 系统公告 | 活动公告 | +| :--: | :------: | :------: | +| ALL | SYSTEM | ACTIVITY |`, }; async function handler(ctx) { const { platform = 'Android', group = 'ALL' } = ctx.req.param(); - let { - data: { announceList }, - } = await got({ - method: 'get', - url: `https://ak-conf.hypergryph.com/config/prod/announce_meta/${platform}/announcement.meta.json`, - }); + let announceList = (await cache.tryGet( + `hypergryph:arknights:announce_meta:${platform}`, + async () => { + const { announceList } = await ofetch(`https://ak-conf.hypergryph.com/config/prod/announce_meta/${platform}/announcement.meta.json`); + return announceList; + }, + config.cache.routeExpire, + false + )) as AnnounceItem[]; if (group !== 'ALL') { announceList = announceList.filter((item) => item.group === group); } - announceList = await Promise.all( + const items = await Promise.all( announceList.map((item) => cache.tryGet(item.webUrl, async () => { - const { data } = await got({ - method: 'get', - url: item.webUrl, - }); + const data = await ofetch(item.webUrl); const $ = load(data); let description = @@ -79,8 +82,8 @@ async function handler(ctx) { ); return { - title: `《明日方舟》${group === 'SYSTEM' ? '系统' : group === 'ACTIVITY' ? '活动' : '全部'}公告`, + title: `《明日方舟》${group === 'SYSTEM' ? '系统' : (group === 'ACTIVITY' ? '活动' : '全部')}公告`, link: 'https://ak.hypergryph.com/', - item: announceList, + item: items, }; } diff --git a/lib/routes/hypergryph/arknights/arktca.ts b/lib/routes/hypergryph/arknights/arktca.ts new file mode 100644 index 00000000000000..712ed338bcafd5 --- /dev/null +++ b/lib/routes/hypergryph/arknights/arktca.ts @@ -0,0 +1,124 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +const rssDescription = '期刊《回归线》 | 泰拉创作者联合会'; +const url = 'aneot.arktca.com'; +const author = 'Bendancom'; + +export const route: Route = { + path: '/arknights/arktca', + categories: ['game'], + example: '/hypergryph/arknights/arktca', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '期刊', + url, + maintainers: [author], + radar: [ + { + source: [url], + }, + ], + description: rssDescription, + handler, +}; + +async function handler() { + const baseUrl = `https://${url}`; + const { data: allResponse } = await got(`${baseUrl}/posts`); + const $ = load(allResponse); + + const allUrlList = $('div.theme-hope-content > table') + .find('a') + .toArray() + .map((item) => baseUrl + $(item).prop('href')); + + const journalList = await Promise.all( + allUrlList.map(async (item) => { + const { data: response } = await got(item); + const $$ = load(response); + const regVol = /(?<=Vol. )(\w+)/; + const match = regVol.exec($$('div.vp-page-title').find('h1').text()); + const volume = match ? match[0] : ''; + const links = $$('div.theme-hope-content > ul a') + .toArray() + .map((e) => baseUrl + $(e).prop('href')); + return { + volume, + links, + }; + }) + ); + + const journals = await Promise.all( + journalList.map( + async (item) => + await Promise.all( + item.links.map((link) => + cache.tryGet(link, async () => { + const { data: response } = await got(link); + const $$ = load(response); + + $$('div.ads-container').remove(); + const language = $$('html').prop('lang'); + + const pageTitle = $$('div.vp-page-title'); + + const title = `Vol.${item.volume} ` + pageTitle.children('h1').text(); + const pageInfo = pageTitle.children('div.page-info'); + + const pageAuthorInfo = pageInfo.children('span.page-author-info'); + const author = pageAuthorInfo.find('span.page-author-item').text(); + + const pageDateInfo = pageInfo.children('span.page-date-info'); + const date = pageDateInfo.children('meta').prop('content'); + const pubDate = parseDate(date); + + const pageCategoryInfo = pageInfo.find('span.page-category-info'); + const category = pageCategoryInfo.children('meta').prop('content'); + + const article = $$('div.theme-hope-content'); + const description = article.html(); + + const comments = Number.parseInt($$('span.wl-num').text()); + return { + title, + language, + author, + pubDate, + category, + description, + comments, + guid: link, + link, + }; + }) + ) + ) + ) + ); + + const logoUrl = `${baseUrl}/logo.svg`; + + return { + title: '回归线', + link: baseUrl, + description: rssDescription, + icon: logoUrl, + logo: logoUrl, + image: logoUrl, + author, + language: 'zh-CN', + item: journals.flat(Infinity), + }; +} diff --git a/lib/routes/hypergryph/arknights/japan.ts b/lib/routes/hypergryph/arknights/japan.ts new file mode 100644 index 00000000000000..db96cae33720aa --- /dev/null +++ b/lib/routes/hypergryph/arknights/japan.ts @@ -0,0 +1,78 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import type { Context } from 'hono'; + +type ContentItem = { + key: string; + value: string; + bannerUrl: string; + bannerUrlMobile: string; + digest: string; + imgCount: number; + imgInfo: any[]; + ext_0: string; + ext_1: string; + ext_2: string; + ext_3: string; + ext_4: string; + ext_5: string; + ext_6: string; + ext_7: string; + ext_8: string; + ext_9: string; +}; + +type NewsDetail = { + id: string; + title: string; + category: number; + content: ContentItem[]; + lang: string; + highlight: number; + publishedAt: string; + ext1: null | string; + ext2: null | string; +}; + +export const route: Route = { + path: '/arknights/japan', + categories: ['game'], + example: '/hypergryph/arknights/japan', + radar: [ + { + source: ['ak.arknights.jp/news', 'ak.arknights.jp/'], + }, + ], + name: 'アークナイツ (日服新闻)', + maintainers: ['ofyark'], + handler, + url: 'ak.arknights.jp/news', +}; + +async function handler(ctx: Context) { + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 9; + + const response = await ofetch('https://www.arknights.jp:10014/news', { + query: { + lang: 'ja', + limit, + page: 1, + }, + }); + + const newsList = (response.data.items as NewsDetail[]).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/hypergryph/arknights/news.ts b/lib/routes/hypergryph/arknights/news.ts new file mode 100644 index 00000000000000..fc5f65fb479117 --- /dev/null +++ b/lib/routes/hypergryph/arknights/news.ts @@ -0,0 +1,118 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { config } from '@/config'; + +type NewsItem = { + cid: string; + tab: string; + sticky: boolean; + title: string; + author: string; + displayTime: number; + cover: string; + extraCover: string; + brief: string; +}; + +type InitialData = { + LATEST: { + list: NewsItem[]; + total: number; + end: boolean; + map: Record; + }; + ANNOUNCEMENT: { + list: NewsItem[]; + total: number; + end: boolean; + map: Record; + }; + ACTIVITY: { + list: NewsItem[]; + total: number; + end: boolean; + map: Record; + }; + NEWS: { + list: NewsItem[]; + total: number; + end: boolean; + map: Record; + }; +}; + +const parseList = (list: NewsItem[]) => + list.map((item) => ({ + title: item.title, + author: item.author, + description: item.brief.trim().replaceAll('\n', '
    '), + 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 = await cache.tryGet( + 'hypergryph:arknights:news', + async () => { + const response = await ofetch('https://ak.hypergryph.com/news'); + const $ = cheerio.load(response); + const renderData = JSON.parse( + $('script:contains("initialData")') + .first() + .text() + .match(/self\.__next_f\.push\((.+)\)/)?.[1] ?? '' + ); + return JSON.parse(renderData[1].slice(2))[3].initialData as InitialData; + }, + config.cache.routeExpire, + false + ); + + const list = group === 'ALL' ? Object.values(initialData).flatMap(({ list }) => parseList(list)) : parseList(initialData[group].list); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = cheerio.load(response); + + const description = $('div > div > div > div > div > div > div:nth-child(4)'); + item.description = description.length ? description.html() : item.description; + + return item; + }) + ) + ); + + return { + title: '《明日方舟》游戏公告与新闻', + link: 'https://ak.hypergryph.com/news', + item: items, + language: 'zh-cn', + }; +} diff --git a/lib/routes/hypergryph/namespace.ts b/lib/routes/hypergryph/namespace.ts new file mode 100644 index 00000000000000..dd1c75c26605fa --- /dev/null +++ b/lib/routes/hypergryph/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '鹰角网络', + url: 'www.hypergryph.com', + categories: ['game'], + lang: 'zh-CN', +}; diff --git a/lib/routes/i-cable/namespace.ts b/lib/routes/i-cable/namespace.ts new file mode 100644 index 00000000000000..5971a6092a1a64 --- /dev/null +++ b/lib/routes/i-cable/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '有線新聞', + url: 'i-cable.com', + lang: 'zh-HK', +}; diff --git a/lib/routes/i-cable/news.ts b/lib/routes/i-cable/news.ts new file mode 100644 index 00000000000000..067dacd8670aaf --- /dev/null +++ b/lib/routes/i-cable/news.ts @@ -0,0 +1,77 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { getCurrentPath } from '@/utils/helpers'; +import path from 'node:path'; +import { art } from '@/utils/render'; +import { config } from '@/config'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +const __dirname = getCurrentPath(import.meta.url); + +export const route: Route = { + path: '/news/:category?', + categories: ['traditional-media'], + example: '/i-cable/news', + parameters: { category: '分類,默認為新聞資訊' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.i-cable.com'], + target: '/news', + }, + { + source: ['www.i-cable.com/category/:category'], + target: '/news/:category', + }, + ], + name: '新聞', + maintainers: ['quiniapiezoelectricity'], + handler, + url: 'www.i-cable.com/', + description: ` +::: tip +分類只可用分類名稱,如:新聞資訊/港聞 +:::`, +}; + +async function handler(ctx) { + const category = ctx.req.param('category') ?? '新聞資訊'; + const limit = ctx.req.query('limit') ?? 20; + const root = 'https://www.i-cable.com/wp-json/wp/v2'; + + const response = await cache.tryGet(`${root}/categories?slug=${category}`, async () => await got(`${root}/categories?slug=${category}`), config.cache.routeExpire, false); + if (response.data.length < 1) { + throw new InvalidParameterError(`Invalid Category: ${category}`); + } + const metadata = response.data[0]; + + const list = await got(`${root}/posts?_embed=1&categories=${metadata.id}&per_page=${limit}`); + const items = list.data.map((item) => { + const description = art(path.join(__dirname, 'templates/description.art'), { + media: item._embedded['wp:featuredmedia'] ?? [], + content: item.content.rendered, + }); + return { + title: item.title.rendered, + link: item.link, + pubDate: item.date_gmt, + description, + category: item._embedded['wp:term'][0].map((term) => term.name) ?? [], + }; + }); + + return { + title: `有線新聞 - ${metadata.name}`, + description: metadata.description, + link: metadata.link, + item: items, + }; +} diff --git a/lib/routes/i-cable/templates/description.art b/lib/routes/i-cable/templates/description.art new file mode 100644 index 00000000000000..1748ab4221bd89 --- /dev/null +++ b/lib/routes/i-cable/templates/description.art @@ -0,0 +1,8 @@ +{{ if media.length > 0 }} + {{ each media }} +
    + {{ /each }} +{{ /if }} +{{ if content }} + {{@ content }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/ianspriggs/index.ts b/lib/routes/ianspriggs/index.ts index d973da7f1bf274..f91b883c3c07b6 100644 --- a/lib/routes/ianspriggs/index.ts +++ b/lib/routes/ianspriggs/index.ts @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 3D PORTRAITS | CHARACTERS | - | ------------ | ---------- | - | portraits | characters |`, +| ------------ | ---------- | +| portraits | characters |`, }; async function handler(ctx) { diff --git a/lib/routes/ianspriggs/namespace.ts b/lib/routes/ianspriggs/namespace.ts index 7be23c643f1977..53f3f0d2443d9d 100644 --- a/lib/routes/ianspriggs/namespace.ts +++ b/lib/routes/ianspriggs/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Ian Spriggss', url: 'ianspriggs.com', + lang: 'en', }; diff --git a/lib/routes/icac/namespace.ts b/lib/routes/icac/namespace.ts index 7111af69e70a96..4b7fe23eef4cdf 100644 --- a/lib/routes/icac/namespace.ts +++ b/lib/routes/icac/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Hong Kong Independent Commission Against Corruption 香港廉政公署', url: 'icac.org.hk', + lang: 'zh-HK', }; diff --git a/lib/routes/icbc/namespace.ts b/lib/routes/icbc/namespace.ts index e77c46120c31cd..60a731f53bf85f 100644 --- a/lib/routes/icbc/namespace.ts +++ b/lib/routes/icbc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国工商银行', url: 'icbc.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/icbc/whpj.ts b/lib/routes/icbc/whpj.ts index ae3f0ef8d6865c..b44761535a64e6 100644 --- a/lib/routes/icbc/whpj.ts +++ b/lib/routes/icbc/whpj.ts @@ -25,8 +25,8 @@ export const route: Route = { handler, url: 'icbc.com.cn/column/1438058341489590354.html', description: `| 短格式 | 参考价 | 现汇买卖 | 现钞买卖 | 现汇买入 | 现汇卖出 | 现钞买入 | 现钞卖出 | - | ------ | ------ | -------- | -------- | -------- | -------- | -------- | -------- | - | short | zs | xh | xc | xhmr | xhmc | xcmr | xcmc |`, +| ------ | ------ | -------- | -------- | -------- | -------- | -------- | -------- | +| short | zs | xh | xc | xhmr | xhmc | xcmr | xcmc |`, }; async function handler(ctx) { diff --git a/lib/routes/idaily/index.ts b/lib/routes/idaily/index.ts index f3a0d59c61fa80..6845f898c4f25d 100644 --- a/lib/routes/idaily/index.ts +++ b/lib/routes/idaily/index.ts @@ -20,8 +20,8 @@ export const route: Route = { ], handler, description: `| 简体中文 | 繁体中文 | - | -------- | -------- | - | zh-hans | zh-hant |`, +| -------- | -------- | +| zh-hans | zh-hant |`, }; async function handler(ctx) { @@ -55,7 +55,7 @@ async function handler(ctx) { intro: item.content, }), author: item.location, - category: item.tags.map((c) => c.name), + category: item.tags?.map((c) => c.name), guid: `idaily-${item.guid}`, pubDate: parseDate(item.pubdate_timestamp, 'X'), updated: parseDate(item.lastupdate_timestamp, 'X'), diff --git a/lib/routes/idaily/namespace.ts b/lib/routes/idaily/namespace.ts index 98d52e35604eb0..12b76308cbdf16 100644 --- a/lib/routes/idaily/namespace.ts +++ b/lib/routes/idaily/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'iDaily', url: 'idai.ly', + lang: 'zh-CN', }; diff --git a/lib/routes/idolmaster/namespace.ts b/lib/routes/idolmaster/namespace.ts new file mode 100644 index 00000000000000..a96677ca3e42b1 --- /dev/null +++ b/lib/routes/idolmaster/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'アイドルマスター THE IDOLM@STER', + url: 'idolmaster-official.jp', + lang: 'ja', +}; diff --git a/lib/routes/idolmaster/news.ts b/lib/routes/idolmaster/news.ts new file mode 100644 index 00000000000000..5df59d2929e631 --- /dev/null +++ b/lib/routes/idolmaster/news.ts @@ -0,0 +1,112 @@ +import { Route, Data, DataItem } from '@/types'; +import type { Context } from 'hono'; +import got from '@/utils/got'; +import querystring from 'querystring'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; + +export const route: Route = { + url: 'idolmaster-official.jp/news', + path: '/news/:routeParams?', + categories: ['anime'], + example: '/idolmaster/news/brand=MILLIONLIVE&brand=SHINYCOLORS&category=GAME&category=ANIME', + parameters: { + routeParams: 'The `brand` and `category` params in the path. The available values are as follows.', + }, + description: `**Brand** +| THE IDOLM@STER | シンデレラガールズ | ミリオンライブ! | SideM | シャイニーカラーズ | 学園アイドルマスター | その他 | +| -------------- | --------------- | ------------- | ----- | --------------- | ----------------- | ----- | +| IDOLMASTER | CINDERELLAGIRLS | MILLIONLIVE | SIDEM | SHINYCOLORS | GAKUEN | OTHER | + +**Category** +| ゲーム | ライブ・イベント | アニメ | 配信番組 | ラジオ | グッズ | コラボ・キャンペーン | ミュージック | ブック・コミック | メディア | その他 | +| ----- | ------------- | ----- | ------- | ----- | ----- | ----------------- | --------- | -------------- | ------ | ----- | +| GAME | LIVE-EVENT | ANIME | LIVESTREAM | RADIO | GOODS | COLLABO-CAMP | CD | BOOK | MEDIA | OTHER | + `, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['idolmaster-official.jp/news'], + target: '/news', + }, + ], + name: 'ニュース News', + maintainers: ['keocheung'], + handler, +}; + +const apiUrl = 'https://cmsapi-frontend.idolmaster-official.jp'; + +async function handler(ctx: Context): Promise { + const tokenUrl = `${apiUrl}/sitern/api/cmsbase/Token/get`; + const tokenRsp = await got(tokenUrl); + const token = tokenRsp.data.data.token; + + const options: { + category: string[]; + subcategory?: string | string[]; + brand?: string | string[]; + } = { + category: ['NEWS'], + }; + + const routeParams = ctx.req.param('routeParams'); + if (routeParams) { + const queries = querystring.parse(routeParams); + options.subcategory = toUpperCase(queries.category); + options.brand = toUpperCase(queries.brand); + } + + const limitParam = ctx.req.query('limit'); + let limit = limitParam ? Number.parseInt(limitParam) : 12; + if (limit > 30) { + limit = 30; + } + const listUrl = `${apiUrl}/sitern/api/idolmaster/Article/list?site=jp&ip=idolmaster&token=${token}&sort=desc&data=${JSON.stringify(options)}&limit=${limit}&start=0`; + const listnRsp = await got(listUrl); + const articleList = listnRsp.data.data.article_list; + + let items = articleList.map( + (article): DataItem => ({ + title: article.title, + link: article.url, + pubDate: timezone(parseDate(article.dspdate), +9), + category: article.categories.subcategory.map((cat) => cat.name), + }) + ); + + items = await Promise.all( + items.map((item: DataItem) => + cache.tryGet(item.link, async () => { + const rsp = await got(item.link); + const content = load(rsp.data); + const nextData = JSON.parse(content('script#__NEXT_DATA__').text()); + item.description = `
    ${nextData.props.pageProps.data.content?.replaceAll('`; + 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 @@

    {{ item.authors }}
    - https://doi.org/{{ item.doi }}
    + https://doi.org/{{ item.doi }}
    Volume {{ item.volume }}

    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 = `Article Cover Image
    `; + } + 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 = `Article Cover Image
    `; + } + 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}} +
    • {{$value.percent}} {{$value.desc}}
    • + {{/each}} +
    +
    + +
    +

    Table View

    + {{if tableData}} + + + {{each tableData}} + + + + + + {{/each}} +
    CategoryCountPercent
    {{$value.key}}{{$value.count}}{{$value.percent}}
    + {{/if}} +
    + +
    +

    Top List

    + {{each topList}} +

    {{$value.title}}

    + {{@ $value.content}} + {{/each}} +
    +
    diff --git a/lib/routes/imagemagick/namespace.ts b/lib/routes/imagemagick/namespace.ts index 8bbf90ef92bcd2..b1d12133feed80 100644 --- a/lib/routes/imagemagick/namespace.ts +++ b/lib/routes/imagemagick/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ImageMagick', url: 'imagemagick.org', + lang: 'en', }; diff --git a/lib/routes/imdb/chart.ts b/lib/routes/imdb/chart.ts new file mode 100644 index 00000000000000..4619f9a3ac65ff --- /dev/null +++ b/lib/routes/imdb/chart.ts @@ -0,0 +1,73 @@ +import { Route, ViewType } from '@/types'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import type { Context } from 'hono'; +import { ChartTitleSearchConnection } from './types'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +import { art } from '@/utils/render'; + +const __dirname = getCurrentPath(import.meta.url); +const render = (data) => art(path.join(__dirname, 'templates', 'chart.art'), data); + +export const route: Route = { + path: '/chart/:chart?', + categories: ['multimedia', 'popular'], + view: ViewType.Notifications, + parameters: { + chart: { + description: 'The chart to display, `top` by default', + options: [ + { value: 'top', label: 'Top 250 Movies' }, + { value: 'moviemeter', label: 'Most Popular Movies' }, + { value: 'toptv', label: 'Top 250 TV Shows' }, + { value: 'tvmeter', label: 'Most Popular TV Shows' }, + ], + default: 'top', + }, + }, + example: '/imdb/chart', + radar: [ + { + source: ['www.imdb.com/chart/:chart/'], + }, + ], + name: 'Charts', + maintainers: ['TonyRL'], + handler, + url: 'www.imdb.com/chart/top/', + description: `| Top 250 Movies | Most Popular Movies | Top 250 TV Shows | Most Popular TV Shows | +| -------------- | ------------------- | ---------------- | --------------------- | +| top | moviemeter | toptv | tvmeter |`, +}; + +async function handler(ctx: Context) { + const { chart = 'top' } = ctx.req.param(); + const baseUrl = 'https://www.imdb.com'; + const link = `${baseUrl}/chart/${chart}/`; + + const response = await ofetch(link); + const $ = cheerio.load(response); + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + const chartTitles = nextData.props.pageProps.pageData.chartTitles as ChartTitleSearchConnection; + + const items = chartTitles.edges.map(({ currentRank, node }) => ({ + title: `${currentRank}. ${node.titleText.text} (${node.releaseYear.year}${node.releaseYear.endYear ? `-${node.releaseYear.endYear}` : ''})`, + description: render({ + primaryImage: node.primaryImage, + originalTitleText: node.originalTitleText, + certificate: node.certificate, + ratingsSummary: node.ratingsSummary, + plot: node.plot, + }), + link: `${baseUrl}/title/${node.id}`, + category: node.titleGenres.genres.map((g) => chartTitles.genres.find((genre) => genre.filterId === g.genre.text)?.text), + })); + + return { + title: $('head title').text(), + description: $('head meta[name="description"]').attr('content'), + link, + item: items, + }; +} diff --git a/lib/routes/imdb/namespace.ts b/lib/routes/imdb/namespace.ts new file mode 100644 index 00000000000000..2e6bc557c7c51f --- /dev/null +++ b/lib/routes/imdb/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'IMDb', + url: 'www.imdb.com', + lang: 'en', +}; diff --git a/lib/routes/imdb/templates/chart.art b/lib/routes/imdb/templates/chart.art new file mode 100644 index 00000000000000..57134175a511fa --- /dev/null +++ b/lib/routes/imdb/templates/chart.art @@ -0,0 +1,15 @@ +{{ if primaryImage.url }} +
    + {{ primaryImage.caption.plainText }} +
    {{ primaryImage.caption.plainText }}
    +
    +
    +{{ /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 | -
    `, +| Ludum Dare | Global Game Jam | +| ---------- | --------------- | +| 31 | 13 | +`, }; async function handler(ctx) { diff --git a/lib/routes/indienova/namespace.ts b/lib/routes/indienova/namespace.ts index 583d7848658163..793aa617ed174a 100644 --- a/lib/routes/indienova/namespace.ts +++ b/lib/routes/indienova/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'indienova 独立游戏', url: 'indienova.com', + lang: 'zh-CN', }; diff --git a/lib/routes/inewsweek/index.ts b/lib/routes/inewsweek/index.ts index d2173236a9a10e..f1c2b362e6e613 100644 --- a/lib/routes/inewsweek/index.ts +++ b/lib/routes/inewsweek/index.ts @@ -31,9 +31,9 @@ export const route: Route = { handler, description: `提取文章全文。 - | 封面 | 时政 | 社会 | 经济 | 国际 | 调查 | 人物 | - | ----- | -------- | ------- | ------- | ----- | ------ | ------ | - | cover | politics | society | finance | world | survey | people |`, +| 封面 | 时政 | 社会 | 经济 | 国际 | 调查 | 人物 | +| ----- | -------- | ------- | ------- | ----- | ------ | ------ | +| cover | politics | society | finance | world | survey | people |`, }; async function handler(ctx) { diff --git a/lib/routes/inewsweek/namespace.ts b/lib/routes/inewsweek/namespace.ts index 0ba28707927768..f9d6c76154a6db 100644 --- a/lib/routes/inewsweek/namespace.ts +++ b/lib/routes/inewsweek/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国新闻周刊', url: 'inewsweek.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/infoq/namespace.ts b/lib/routes/infoq/namespace.ts index 0f48f6db3676e2..b8f4cdca848472 100644 --- a/lib/routes/infoq/namespace.ts +++ b/lib/routes/infoq/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'InfoQ 中文', url: 'infoq.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/infoq/presentations.ts b/lib/routes/infoq/presentations.ts new file mode 100644 index 00000000000000..a7b4c91b59f1cb --- /dev/null +++ b/lib/routes/infoq/presentations.ts @@ -0,0 +1,203 @@ +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'; + +export const handler = async (ctx) => { + const { conference } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 12; + + const rootUrl = 'https://www.infoq.com'; + const currentUrl = new URL(`${conference ? `${conference}/` : ''}presentations/`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('ul[data-tax="presentations"] li[data-path]') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const a = item.find('h3.card__title a'); + + const title = a.prop('title') || a.text().trim(); + const image = item.find('img.card__image').prop('src'); + const description = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + intro: item.find('p.card__excerpt').text(), + }); + const link = new URL(a.prop('href'), rootUrl).href; + const guid = `infoq-${item.prop('data-path').replace(/^\//, '')}`; + const length = item.find('div.card__length').text() || undefined; + + return { + title, + description, + pubDate: parseDate(item.find('span.card__date span').text().trim()), + link, + category: item + .find('div.card__topics') + .toArray() + .map((c) => $(c).text().trim()), + author: item + .find('div.card__authors a') + .toArray() + .map((a) => $(a).text().trim()) + .join('/'), + guid, + id: guid, + content: { + html: description, + text: item.find('p.card__excerpt').text(), + }, + image, + banner: image, + language, + enclosure_url: length ? link : undefined, + enclosure_type: length ? 'video/mp4' : undefined, + enclosure_title: title, + itunes_duration: length, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + $$('div.player').prevAll().remove(); + $$('div.event__list-box').remove(); + + const length = $$('div.player__actions span').text() || undefined; + + const script = $$('script[type="text/javascript"]').text(); + const videoSrc = script.match(/P\.s\s=\s'(.*?)';/)?.[1] ?? undefined; + const poster = script.match(/P\.c\(.*?isWideScreen,\s'(.*?)',\s/)?.[1] ?? undefined; + const topicsStr = script.match(/var\stopicsInPage\s=\sJSON\.parse\('(.*?)'\);/)?.[1]?.replace(/\\/g, '') ?? undefined; + + if (videoSrc) { + $$('div.player').replaceWith( + art(path.join(__dirname, 'templates/description.art'), { + videos: [ + { + src: videoSrc, + poster, + type: `video/${videoSrc.split(/\./).pop()}`, + }, + ], + }) + ); + } + + const title = $$('meta[property="og:title"]').prop('content').trim(); + const image = $$('meta[property="twitter:image"]').prop('content') || $$('meta[property="og:image"]').prop('content'); + + item.title = title; + item.pubDate = parseDate($$('p.date').text()); + item.link = $$('meta[property="og:url"]').prop('content'); + item.category = topicsStr ? JSON.parse(topicsStr).map((t) => t.name) : $$('meta[name="keywords"]').prop('content').split(/,/); + item.author = $$('ul.authors a.author__link') + .toArray() + .map((a) => $$(a).text()) + .join('/'); + + $$('div.article__content').nextAll().remove(); + + const description = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + description: $$('article.article').html(), + }); + + item.description = description; + item.content = { + html: description, + text: $$('article.article').text(), + }; + item.image = image; + item.banner = image; + item.language = language; + item.enclosure_url = videoSrc; + item.enclosure_type = item.enclosure_url ? 'video/mp4' : undefined; + item.enclosure_title = title; + item.itunes_duration = length; + + return item; + }) + ) + ); + + const title = $('title').text(); + const image = $('meta[property="og:image"]').prop('content'); + + return { + title, + description: $('meta[name="description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: title.split(/-/).pop(), + language, + }; +}; + +export const route: Route = { + path: '/presentations/:conference?', + name: 'Presentations', + url: 'www.infoq.com', + maintainers: ['nczitzk'], + handler, + example: '/infoq/presentations', + parameters: { conference: 'Conference, all by default, can be found in URL' }, + description: `::: tip + If you subscribe to [InfoQ Live Jan 2024](https://www.infoq.com/infoq-live-jan-2024/presentations/),where the URL is \`https://www.infoq.com/infoq-live-jan-2024/presentations/\`, extract the part \`https://www.infoq.com/\` to the end, which is \`/presentations/\`, and use it as the parameter to fill in. Therefore, the route will be [\`/infoq/presentations/infoq-live-jan-2024\`](https://rsshub.app/infoq/presentations/infoq-live-jan-2024). +::: + `, + categories: ['programming', 'popular'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.infoq.com/presentations', 'www.infoq.com/:conference/presentations'], + target: (params) => { + const conference = params.conference; + + return `/presentations${conference ? `/${conference}` : ''}`; + }, + }, + ], +}; diff --git a/lib/routes/infoq/recommend.ts b/lib/routes/infoq/recommend.ts index b2ec6aa303f193..4ed3acd83ebf7b 100644 --- a/lib/routes/infoq/recommend.ts +++ b/lib/routes/infoq/recommend.ts @@ -5,7 +5,7 @@ import utils from './utils'; export const route: Route = { path: '/recommend', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/infoq/recommend', parameters: {}, features: { diff --git a/lib/routes/infoq/templates/description.art b/lib/routes/infoq/templates/description.art new file mode 100644 index 00000000000000..5b807209cc5d21 --- /dev/null +++ b/lib/routes/infoq/templates/description.art @@ -0,0 +1,41 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
    + {{ image.alt }} +
    + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if videos }} + {{ each videos video }} + {{ if video?.src }} + + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} +
    {{ 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({ + method: 'get', + url: `https://www.infzm.com/hot_contents`, + headers: { + Referer: link, + }, + }); + + const resultItem = await fetchArticles(data.data.hot_contents); + + return { + title: `南方周末-热门文章`, + link, + image: 'https://www.infzm.com/favicon.ico', + item: resultItem as DataItem[], + }; +} diff --git a/lib/routes/infzm/index.ts b/lib/routes/infzm/index.ts index 2b98371ec62fd1..e0beedf5e2b7f2 100644 --- a/lib/routes/infzm/index.ts +++ b/lib/routes/infzm/index.ts @@ -1,10 +1,7 @@ import type { Data, DataItem, Route } from '@/types'; import type { ContentsResponse } from './types'; -import { config } from '@/config'; import got from '@/utils/got'; -import { load } from 'cheerio'; -import timezone from '@/utils/timezone'; -import cache from '@/utils/cache'; +import { fetchArticles } from './utils'; export const route: Route = { path: '/:id', @@ -22,12 +19,12 @@ export const route: Route = { handler, description: `下面给出部分参考: - | 推荐 | 新闻 | 观点 | 文化 | 人物 | 影像 | 专题 | 生活 | 视频 | - | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | - | 1 | 2 | 3 | 4 | 7 | 8 | 6 | 5 | 131 |`, +| 推荐 | 新闻 | 观点 | 文化 | 人物 | 影像 | 专题 | 生活 | 视频 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| 1 | 2 | 3 | 4 | 7 | 8 | 6 | 5 | 131 |`, }; -const baseUrl = 'https://www.infzm.com/contents'; +export const baseUrl = 'https://www.infzm.com/contents'; async function handler(ctx): Promise { const id = ctx.req.param('id'); @@ -40,31 +37,7 @@ async function handler(ctx): Promise { }, }); - const resultItem = await Promise.all( - data.data.contents.map(({ id, subject, author, publish_time }) => { - const link = `${baseUrl}/${id}`; - - return cache.tryGet(link, async () => { - const cookie = config.infzm.cookie; - const response = await got.get({ - method: 'get', - url: link, - headers: { - Referer: link, - Cookie: cookie || `passport_session=${Math.floor(Math.random() * 100)};`, - }, - }); - const $ = load(response.data); - return { - title: subject, - description: $('div.nfzm-content__content').html() ?? '', - pubDate: timezone(publish_time, +8).toUTCString(), - link, - author, - }; - }); - }) - ); + const resultItem = await fetchArticles(data.data.contents); return { title: `南方周末-${data.data.current_term.title}`, diff --git a/lib/routes/infzm/namespace.ts b/lib/routes/infzm/namespace.ts new file mode 100644 index 00000000000000..fa93312d53de0d --- /dev/null +++ b/lib/routes/infzm/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '南方周末', + url: 'www.infzm.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/infzm/utils.ts b/lib/routes/infzm/utils.ts new file mode 100644 index 00000000000000..83ca7538985175 --- /dev/null +++ b/lib/routes/infzm/utils.ts @@ -0,0 +1,34 @@ +import { config } from '@/config'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import timezone from '@/utils/timezone'; +import { load } from 'cheerio'; +import { baseUrl } from '.'; + +export async function fetchArticles(data) { + return await Promise.all( + data.map(({ id, subject, author, publish_time }) => { + const link = `${baseUrl}/${id}`; + + return cache.tryGet(link, async () => { + const cookie = config.infzm.cookie; + const response = await got.get({ + method: 'get', + url: link, + headers: { + Referer: link, + Cookie: cookie || `passport_session=${Math.floor(Math.random() * 100)};`, + }, + }); + const $ = load(response.data); + return { + title: subject, + description: $('div.nfzm-content__content').html() ?? '', + pubDate: timezone(publish_time, +8).toUTCString(), + link, + author, + }; + }); + }) + ); +} diff --git a/lib/routes/inoreader/index.ts b/lib/routes/inoreader/index.ts index 3f5669149d0500..feed3b4308fcf7 100644 --- a/lib/routes/inoreader/index.ts +++ b/lib/routes/inoreader/index.ts @@ -1,12 +1,15 @@ -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: '/html_clip/:user/:tag', - name: 'Unknown', - maintainers: [], + example: '/inoreader/html_clip/1005137674/user-favorites', + categories: ['reading', 'popular'], + view: ViewType.Articles, + name: 'HTML Clip', + maintainers: ['EthanWng97'], handler, }; diff --git a/lib/routes/inoreader/namespace.ts b/lib/routes/inoreader/namespace.ts index 1474f49c4f3ba0..c14e8fd051488e 100644 --- a/lib/routes/inoreader/namespace.ts +++ b/lib/routes/inoreader/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Inoreader', url: 'inoreader.com', + lang: 'en', }; diff --git a/lib/routes/inoreader/rss.ts b/lib/routes/inoreader/rss.ts index 2a5e95ae4135ce..fba2ed46beeb11 100644 --- a/lib/routes/inoreader/rss.ts +++ b/lib/routes/inoreader/rss.ts @@ -1,10 +1,11 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import parser from '@/utils/rss-parser'; import { load } from 'cheerio'; export const route: Route = { path: '/rss/:user/:tag', - categories: ['reading'], + categories: ['reading', 'popular'], + view: ViewType.Articles, example: '/inoreader/rss/1005137674/user-favorites', parameters: { user: 'user id, the interger after user/ in the example URL', tag: 'tag, the string after tag/ in the example URL' }, features: { diff --git a/lib/routes/inspirehep/author.ts b/lib/routes/inspirehep/author.ts new file mode 100644 index 00000000000000..768ebea1d06749 --- /dev/null +++ b/lib/routes/inspirehep/author.ts @@ -0,0 +1,54 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { AuthorResponse, LiteratureResponse } from './types'; +import cache from '@/utils/cache'; + +import { baseUrl, parseLiterature } from './utils'; + +export const route: Route = { + path: '/authors/:id', + example: '/inspirehep/authors/1696909', + parameters: { id: 'Author ID' }, + name: 'Author Search', + maintainers: ['TonyRL'], + radar: [ + { + source: ['inspirehep.net/authors/:id'], + }, + ], + handler, +}; + +export const getAuthorById = (id: string) => + cache.tryGet(`inspirehep:author:${id}`, () => + ofetch(`${baseUrl}/api/authors/${id}`, { + headers: { + accept: 'application/vnd+inspire.record.ui+json', + }, + parseResponse: JSON.parse, + }) + ); + +async function handler(ctx) { + const id = ctx.req.param('id'); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 25; + + const authorInfo = (await getAuthorById(id)) as AuthorResponse; + const response = await ofetch(`${baseUrl}/api/literature`, { + query: { + sort: 'mostrecent', + size: limit, + page: 1, + search_type: 'hep-author-publication', + author: authorInfo.metadata.facet_author_name, + }, + }); + + const items = parseLiterature(response.hits.hits); + + return { + title: `${authorInfo.metadata.name.preferred_name} - INSPIRE`, + link: `${baseUrl}/authors/${id}`, + item: items, + }; +} diff --git a/lib/routes/inspirehep/literature.ts b/lib/routes/inspirehep/literature.ts new file mode 100644 index 00000000000000..64a2f1f545caea --- /dev/null +++ b/lib/routes/inspirehep/literature.ts @@ -0,0 +1,42 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { LiteratureResponse } from './types'; + +import { baseUrl, parseLiterature } from './utils'; + +export const route: Route = { + path: '/literature/:q', + example: '/inspirehep/literature/Physics', + parameters: { q: 'Search keyword' }, + name: 'Literature Search', + maintainers: ['TonyRL'], + radar: [ + { + source: ['inspirehep.net/literature'], + target: (_params, url) => `/inspirehep/literature/${new URL(url).searchParams.get('q')}`, + }, + ], + handler, +}; + +async function handler(ctx) { + const q = ctx.req.param('q'); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 25; + + const response = await ofetch(`${baseUrl}/api/literature`, { + query: { + sort: 'mostrecent', + size: limit, + page: 1, + q, + }, + }); + + const items = parseLiterature(response.hits.hits); + + return { + title: 'Literature Search - INSPIRE', + link: `${baseUrl}/literature?sort=mostrecent&size=${limit}&page=1&q=${q}`, + item: items, + }; +} diff --git a/lib/routes/inspirehep/namespace.ts b/lib/routes/inspirehep/namespace.ts new file mode 100644 index 00000000000000..790901c6803a82 --- /dev/null +++ b/lib/routes/inspirehep/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'INSPIRE', + url: 'inspirehep.net', + categories: ['journal'], + lang: 'en', +}; diff --git a/lib/routes/inspirehep/types.ts b/lib/routes/inspirehep/types.ts new file mode 100644 index 00000000000000..44c44cfb6a9010 --- /dev/null +++ b/lib/routes/inspirehep/types.ts @@ -0,0 +1,210 @@ +interface Links { + self?: string; + next?: string; + bibtex: string; + 'latex-eu': string; + 'latex-us': string; + json: string; + cv: string; + citations: string; +} + +interface Metadata { + number_of_authors: number; + date: string; + publication_info: { + year: number; + artid?: string; + journal_volume?: string; + journal_title?: string; + journal_issue?: string; + }[]; + citation_count: number; + is_collection_hidden: boolean; + authors: { + uuid: string; + record: { + $ref: string; + }; + full_name: string; + first_name: string; + ids: { + schema: string; + value: string; + }[]; + last_name: string; + recid: number; + affiliations: { + value: string; + record: { + $ref: string; + }; + curated_relation?: boolean; + }[]; + raw_affiliations: { + value: string; + }[]; + curated_relation?: boolean; + }[]; + citation_count_without_self_citations: number; + titles: { + title: string; + source: string; + }[]; + texkeys: string[]; + imprints: { + date: string; + publisher?: string; + }[]; + abstracts: { + value: string; + source: string; + }[]; + document_type: string[]; + control_number: number; + inspire_categories: { + term: string; + source?: string; + }[]; + number_of_pages?: number; + keywords?: { + value: string; + source?: string; + schema?: string; + }[]; + thesis_info?: { + institutions: { + name: string; + record: { + $ref: string; + }; + curated_relation?: boolean; + }[]; + degree_type: string; + date: string; + }; + license?: { + url: string; + license: string; + imposing?: string; + }[]; + persistent_identifiers?: { + value: string; + schema: string; + source: string; + }[]; + supervisors?: { + uuid: string; + record: { + $ref: string; + }; + full_name: string; + inspire_roles: string[]; + }[]; + isbns?: { + value: string; + medium?: string; + }[]; + urls?: { + value: string; + description?: string; + }[]; + dois?: { + value: string; + }[]; + number_of_references?: number; + external_system_identifiers?: { + url_name: string; + url_link: string; + }[]; + report_numbers?: { + value: string; + }[]; + accelerator_experiments?: { + name: string; + record: { + $ref: string; + }; + }[]; + documents?: { + source: string; + key: string; + url: string; + fulltext: boolean; + hidden: boolean; + filename: string; + }[]; + citation_pdf_urls?: string[]; + fulltext_links?: { + description: string; + value: string; + }[]; +} + +interface Literature { + created: string; + metadata: Metadata; + links: Links; + updated: string; + id: string; +} + +export interface AuthorResponse { + id: string; + uuid: string; + revision_id: number; + updated: string; + links: { + json: string; + }; + metadata: { + can_edit: boolean; + orcid: string; + bai: string; + facet_author_name: string; + should_display_positions: boolean; + positions: { + institution: string; + current: boolean; + display_date: string; + record: { + $ref: string; + }; + }[]; + project_membership: { + name: string; + record: { + $ref: string; + }; + current: boolean; + curated_relation: boolean; + }[]; + ids: { + value: string; + schema: string; + }[]; + name: { + value: string; + preferred_name: string; + }; + stub: boolean; + status: string; + deleted: boolean; + control_number: number; + legacy_version: string; + legacy_creation_date: string; + }; + created: string; +} + +export interface LiteratureResponse { + hits: { + hits: Literature[]; + total: number; + }; + links: Links; + sort_options: { + value: string; + title: string; + }[]; +} diff --git a/lib/routes/inspirehep/utils.ts b/lib/routes/inspirehep/utils.ts new file mode 100644 index 00000000000000..fe903b5e919e12 --- /dev/null +++ b/lib/routes/inspirehep/utils.ts @@ -0,0 +1,15 @@ +import { LiteratureResponse } from './types'; +import { parseDate } from '@/utils/parse-date'; + +export const baseUrl = 'https://inspirehep.net'; + +export const parseLiterature = (hits: LiteratureResponse['hits']['hits']) => + hits.map((item) => ({ + title: item.metadata.titles.map((t) => t.title).join(' '), + link: `${baseUrl}/literature/${item.id}`, + description: item.metadata.abstracts?.map((a) => `${a.value}`).join('
    '), + 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 + ) + ); + + return { + title: '异次元软件世界', + description: '软件改变生活', + language: 'zh-CN', + link: 'https://www.iplaysoft.com', + item: items, + }; +}; + +export const route: Route = { + path: '/', + name: '首页', + url: 'www.iplaysoft.com', + maintainers: ['williamgateszhao', 'cscnk52', 'LokHsu'], + handler, + example: '/iplaysoft', + parameters: {}, + categories: ['program-update'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.iplaysoft.com'], + target: '/', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/iplaysoft/namespace.ts b/lib/routes/iplaysoft/namespace.ts new file mode 100644 index 00000000000000..7fa3a955e56623 --- /dev/null +++ b/lib/routes/iplaysoft/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '异次元软件世界', + url: 'www.iplaysoft.com', + categories: ['new-media'], + lang: 'zh-CN', +}; diff --git a/lib/routes/iplaysoft/tag.ts b/lib/routes/iplaysoft/tag.ts new file mode 100644 index 00000000000000..16c88947146070 --- /dev/null +++ b/lib/routes/iplaysoft/tag.ts @@ -0,0 +1,49 @@ +import { Data, DataItem, Route, ViewType } from '@/types'; +import { fetchNewsItems, fetchTag } from './utils'; + +export const handler = async (ctx): Promise => { + const slug = ctx.req.param('slug'); + + const { id, name } = await fetchTag(slug); + + const rootUrl = 'https://www.iplaysoft.com/'; + const postApiUrl = `${rootUrl}wp-json/wp/v2/posts?_embed&tags=${id}`; + + const items: DataItem[] = await fetchNewsItems(postApiUrl); + + return { + title: `${name} - 异次元软件世界`, + description: '软件改变生活', + language: 'zh-CN', + link: `${rootUrl}tag/${slug}`, + item: items, + }; +}; + +export const route: Route = { + path: '/tag/:slug', + name: '标签', + url: 'www.iplaysoft.com', + maintainers: ['cscnk52'], + handler, + example: '/iplaysoft/tag/windows', + 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/tag/:slug'], + target: '/tag/:slug', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/iplaysoft/utils.ts b/lib/routes/iplaysoft/utils.ts new file mode 100644 index 00000000000000..0dec7ef6b0addb --- /dev/null +++ b/lib/routes/iplaysoft/utils.ts @@ -0,0 +1,34 @@ +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; + +const rootUrl = 'https://www.iplaysoft.com/'; + +const fetchTaxonomy = async (slug: string, type: 'categories' | 'tags') => { + const taxonomyUrl = `${rootUrl}wp-json/wp/v2/${type}?slug=${slug}`; + const cachedTaxonomy = await cache.tryGet(taxonomyUrl, async () => { + const taxonomyData = await ofetch(taxonomyUrl); + if (!taxonomyData[0] || !taxonomyData[0].id || !taxonomyData[0].name) { + throw new Error(`${type} ${slug} not found`); + } + return { id: taxonomyData[0].id, name: taxonomyData[0].name }; + }); + return cachedTaxonomy; +}; + +const fetchCategory = async (categorySlug: string) => await fetchTaxonomy(categorySlug, 'categories'); +const fetchTag = async (tagSlug: string) => await fetchTaxonomy(tagSlug, 'tags'); + +async function fetchNewsItems(apiUrl: string) { + const data = await ofetch(apiUrl); + + return data.map((item) => ({ + title: item.title.rendered, + description: item.content.rendered, + link: item.link, + pubDate: new Date(item.date_gmt).toUTCString(), + author: item._embedded.author[0].name, + category: item._embedded['wp:term'].flat().map((term) => term.name), + })); +} + +export { fetchCategory, fetchTag, fetchNewsItems }; diff --git a/lib/routes/ippa/namespace.ts b/lib/routes/ippa/namespace.ts index 7c7ed93f74a294..2637efe0d494ab 100644 --- a/lib/routes/ippa/namespace.ts +++ b/lib/routes/ippa/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '子方有料', url: 'ippa.top', + lang: 'zh-CN', }; diff --git a/lib/routes/ipsw.dev/index.ts b/lib/routes/ipsw.dev/index.ts new file mode 100644 index 00000000000000..a5ad5fd1be2bc3 --- /dev/null +++ b/lib/routes/ipsw.dev/index.ts @@ -0,0 +1,64 @@ +import { Data, Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; + +export const route: Route = { + path: '/index/:productID', + categories: ['program-update'], + example: '/ipsw.dev/index/iPhone16,1', + parameters: { + productID: 'Product ID', + }, + name: 'Apple latest beta firmware', + maintainers: ['RieN7'], + handler, +}; + +async function handler(ctx) { + const { productID } = ctx.req.param(); + const __dirname = getCurrentPath(import.meta.url); + const link = `https://ipsw.dev/product/version/${productID}`; + + const resp = await got({ + method: 'get', + url: link, + headers: { + Referer: 'https://ipsw.dev/', + }, + }); + + const $ = load(resp.data); + + const productName = $('#IdentifierModal > div > div > div.modal-body > p:nth-child(1) > em').text(); + + const list: Data[] = $('.firmware') + .map((index, element) => { + const ele = $(element); + const version = ele.find('td:nth-child(1) > div > div > strong').text(); + const build = ele.find('td:nth-child(1) > div > div > div > code').text(); + const date = ele.find('td:nth-child(3)').text(); + const size = ele.find('td:nth-child(4)').text(); + return { + title: `${productName} - ${version}`, + link: `https://ipsw.dev/download/${productID}/${build}`, + pubDate: new Date(date).toLocaleDateString(), + guid: build, + description: art(path.join(__dirname, 'templates/description.art'), { + version, + build, + date, + size, + }), + }; + }) + .get(); + + return { + title: `${productName} Released`, + link, + item: list, + }; +} diff --git a/lib/routes/ipsw.dev/namespace.ts b/lib/routes/ipsw.dev/namespace.ts new file mode 100644 index 00000000000000..3beb11a3b16747 --- /dev/null +++ b/lib/routes/ipsw.dev/namespace.ts @@ -0,0 +1,8 @@ +import { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'IPSW.dev', + url: 'ipsw.dev', + description: 'Download the latest beta firmware for iPhone, iPad, Mac, Apple Vision Pro, and Apple TV. Check the signing status of the beta firmware.', + lang: 'en', +}; diff --git a/lib/routes/ipsw.dev/templates/description.art b/lib/routes/ipsw.dev/templates/description.art new file mode 100644 index 00000000000000..c0faef7bfac60d --- /dev/null +++ b/lib/routes/ipsw.dev/templates/description.art @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + +
    Version{{ version }}
    Build{{ build }}
    Released{{ released }}
    Size{{ size }}
    \ 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 }} +
    + +
    + {{ /if }} + {{ /each }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/iresearch/weekly.ts b/lib/routes/iresearch/weekly.ts index c267f3e6865f17..d1668dde072e49 100644 --- a/lib/routes/iresearch/weekly.ts +++ b/lib/routes/iresearch/weekly.ts @@ -24,7 +24,7 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 家电行业 | 服装行业 | 美妆行业 | 食品饮料行业 | - | -------- | -------- | -------- | ------------ |`, +| -------- | -------- | -------- | ------------ |`, }; async function handler(ctx) { diff --git a/lib/routes/isct/namespace.ts b/lib/routes/isct/namespace.ts new file mode 100644 index 00000000000000..d9db953937732c --- /dev/null +++ b/lib/routes/isct/namespace.ts @@ -0,0 +1,15 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Institute of Science Tokyo', + url: 'isct.ac.jp', + lang: 'ja', + description: `::: tip +支持通过 category 参数筛选新闻类别。详情请查看 [指南](https://docs.rsshub.app/zh/guide/parameters#%E5%86%85%E5%AE%B9%E8%BF%87%E6%BB%A4) 。 + +You can filter news by category through the category parameter. For more information, please refer to the [guide](https://docs.rsshub.app/guide/parameters#filtering). +:::`, + ja: { + name: '東京科学大学', + }, +}; diff --git a/lib/routes/isct/news.ts b/lib/routes/isct/news.ts new file mode 100644 index 00000000000000..9d0e8677357d3a --- /dev/null +++ b/lib/routes/isct/news.ts @@ -0,0 +1,81 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { decode } from 'entities'; + +interface MediaItem { + ID: string; + TITLE: string; + PUBLISH_DATE: string; + META_DESCRIPTION: string; + MEDIA_CD: string; + MEDIA_TYPES: string; +} + +interface TagItem { + TAG_ID: string; + TAG_NAME: string; +} + +export const route: Route = { + path: '/news/:lang', + categories: ['university'], + example: '/isct/news/ja', + parameters: { lang: 'language, could be ja or en' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.isct.ac.jp/:lang/news'], + target: '/news/:lang', + }, + ], + name: 'News', + maintainers: ['catyyy'], + handler: async (ctx) => { + const { lang = 'ja' } = ctx.req.param(); + const mediaResponse = await ofetch(`https://www.isct.ac.jp/expansion/get_media_list_json.php?lang_cd=${lang}`); + const tagResponse = await ofetch(`https://www.isct.ac.jp/expansion/get_tag_list_json.php?lang_cd=${lang}`); + + const mediaData = JSON.parse(decode(mediaResponse)); + const tagData = JSON.parse(decode(tagResponse)); + + const itemsArray: MediaItem[] = Object.values(mediaData); + const tagArray: TagItem[] = Object.values(tagData); + + const tagIdNameMapping: { [key: string]: string } = {}; + + for (const item of Object.values(tagArray)) { + tagIdNameMapping[item.TAG_ID] = item.TAG_NAME; + } + + const items = itemsArray.map((item) => ({ + // 文章标题 + title: item.TITLE, + // 文章链接 + link: 'news/' + item.MEDIA_CD, + // 文章正文 + description: item.META_DESCRIPTION, + // 文章发布日期 + pubDate: parseDate(item.PUBLISH_DATE), + // 如果有的话,文章作者 + // author: item.user.login, + // 如果有的话,文章分类 + category: item.MEDIA_TYPES ? [tagIdNameMapping[Number.parseInt(item.MEDIA_TYPES.replaceAll('"', ''), 10)]] : [], + })); + return { + // 源标题 + title: `ISCT News - ${lang}`, + // 源链接 + link: `https://www.isct.ac.jp/${lang}/news`, + // 源文章 + item: items, + }; + }, +}; diff --git a/lib/routes/issuehunt/namespace.ts b/lib/routes/issuehunt/namespace.ts index f4b08b973914ba..192821f693e23f 100644 --- a/lib/routes/issuehunt/namespace.ts +++ b/lib/routes/issuehunt/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Issue Hunt', url: 'issuehunt.io', + lang: 'en', }; diff --git a/lib/routes/itc/namespace.ts b/lib/routes/itc/namespace.ts index 436bb50fc3dea0..8d5bca57e4939e 100644 --- a/lib/routes/itc/namespace.ts +++ b/lib/routes/itc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Open Github社区', url: 'open.itc.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/itch/namespace.ts b/lib/routes/itch/namespace.ts index 873b70f78b4963..919e2564e29490 100644 --- a/lib/routes/itch/namespace.ts +++ b/lib/routes/itch/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'itch.io', url: 'itch.io', + lang: 'en', }; diff --git a/lib/routes/ithome/index.ts b/lib/routes/ithome/index.ts index 840e9ab1bcd440..16968647adaf1c 100644 --- a/lib/routes/ithome/index.ts +++ b/lib/routes/ithome/index.ts @@ -53,8 +53,8 @@ export const route: Route = { maintainers: ['luyuhuang'], handler, description: `| it | soft | win10 | win11 | iphone | ipad | android | digi | next | - | ------- | -------- | ---------- | ---------- | ----------- | --------- | ------------ | -------- | -------- | - | IT 资讯 | 软件之家 | win10 之家 | win11 之家 | iphone 之家 | ipad 之家 | android 之家 | 数码之家 | 智能时代 |`, +| ------- | -------- | ---------- | ---------- | ----------- | --------- | ------------ | -------- | -------- | +| IT 资讯 | 软件之家 | win10 之家 | win11 之家 | iphone 之家 | ipad 之家 | android 之家 | 数码之家 | 智能时代 |`, }; async function handler(ctx) { diff --git a/lib/routes/ithome/namespace.ts b/lib/routes/ithome/namespace.ts index eb210c384b358b..19fbf1ec8fa4f0 100644 --- a/lib/routes/ithome/namespace.ts +++ b/lib/routes/ithome/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'iThome 台灣', url: 'ithome.com', + lang: 'zh-TW', }; diff --git a/lib/routes/ithome/ranking.ts b/lib/routes/ithome/ranking.ts index b95630cdceea9c..c35b40ebe95760 100644 --- a/lib/routes/ithome/ranking.ts +++ b/lib/routes/ithome/ranking.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['immmortal', 'luyuhuang'], handler, description: `| 24h | 7days | monthly | - | ------------- | -------- | ------- | - | 24 小时阅读榜 | 7 天最热 | 月榜 |`, +| ------------- | -------- | ------- | +| 24 小时阅读榜 | 7 天最热 | 月榜 |`, }; async function handler(ctx) { diff --git a/lib/routes/ithome/tw/feeds.ts b/lib/routes/ithome/tw/feeds.ts index 0d0f381f1283b5..4b9efe7d402662 100644 --- a/lib/routes/ithome/tw/feeds.ts +++ b/lib/routes/ithome/tw/feeds.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['miles170'], handler, description: `| 新聞 | AI | Cloud | DevOps | 資安 | - | ---- | -------- | ----- | ------ | -------- | - | news | big-data | cloud | devops | security |`, +| ---- | -------- | ----- | ------ | -------- | +| news | big-data | cloud | devops | security |`, }; async function handler(ctx) { diff --git a/lib/routes/ithome/zt.ts b/lib/routes/ithome/zt.ts index 4e7a251dd26cb6..7c174365e862c1 100644 --- a/lib/routes/ithome/zt.ts +++ b/lib/routes/ithome/zt.ts @@ -127,9 +127,9 @@ export const route: Route = { handler, example: '/ithome/zt/xijiayi', parameters: { category: '专题 id,默认为 xijiayi,即 [喜加一](https://www.ithome.com/zt/xijiayi),可在对应专题页 URL 中找到' }, - description: `:::tip + description: `::: tip 更多专题请见 [IT之家专题](https://www.ithome.com/zt) - :::`, +:::`, categories: ['new-media'], features: { diff --git a/lib/routes/iwara/namespace.ts b/lib/routes/iwara/namespace.ts index 5339b9d95e8715..01aa2f0c3fd2db 100644 --- a/lib/routes/iwara/namespace.ts +++ b/lib/routes/iwara/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'iwara', url: 'ecchi.iwara.tv', + lang: 'en', }; diff --git a/lib/routes/iwara/subscriptions.ts b/lib/routes/iwara/subscriptions.ts index c7a94b687b0b29..5c637ecf90253a 100644 --- a/lib/routes/iwara/subscriptions.ts +++ b/lib/routes/iwara/subscriptions.ts @@ -45,9 +45,9 @@ export const route: Route = { maintainers: ['FeCCC'], handler, url: 'ecchi.iwara.tv/', - description: `:::warning + description: `::: warning This route requires username and password, therefore it's only available when self-hosting, refer to the [Deploy Guide](https://docs.rsshub.app/deploy/config#route-specific-configurations) for route-specific configurations. - :::`, +:::`, }; async function handler() { @@ -69,7 +69,7 @@ async function handler() { headers: { 'content-type': 'application/json', }, - data: JSON.stringify({ + body: JSON.stringify({ email: username, password, }), diff --git a/lib/routes/ixigua/namespace.ts b/lib/routes/ixigua/namespace.ts index 62800120e66620..5a48e1f726cb1e 100644 --- a/lib/routes/ixigua/namespace.ts +++ b/lib/routes/ixigua/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '西瓜视频', url: 'ixigua.com', + lang: 'zh-CN', }; diff --git a/lib/routes/ixigua/user-video.ts b/lib/routes/ixigua/user-video.ts index c1dd8cc965c399..d9bed9df03e8ac 100644 --- a/lib/routes/ixigua/user-video.ts +++ b/lib/routes/ixigua/user-video.ts @@ -30,7 +30,7 @@ export const route: Route = { }, ], name: '用户视频投稿', - maintainers: [], + maintainers: ['FlashWingShadow', 'Fatpandac', 'pseudoyu'], handler, }; @@ -39,13 +39,24 @@ async function handler(ctx) { const disableEmbed = ctx.req.param('disableEmbed'); const url = `${host}/home/${uid}/?wid_try=1`; - const response = await got(url); - const $ = load(response.data); - const jsData = $('#SSR_HYDRATED_DATA').html().replace('window._SSR_HYDRATED_DATA=', '').replaceAll('undefined', '""'); - const data = JSON.parse(jsData); + const { data } = await got(url); + const $ = load(data); + const jsData = $('#SSR_HYDRATED_DATA').html(); - const videoInfos = data.AuthorVideoList.videoList; - const userInfo = data.AuthorDetailInfo; + if (!jsData) { + throw new Error('Failed to find SSR_HYDRATED_DATA'); + } + + const jsonData = JSON.parse(jsData.match(/var\s+data\s*=\s*({.*?});/s)?.[1].replaceAll('undefined', 'null') || '{}'); + + const { + AuthorVideoList: { videoList: videoInfos }, + AuthorDetailInfo: userInfo, + } = jsonData; + + if (!videoInfos || !userInfo) { + throw new Error('Failed to extract required data from JSON'); + } return { title: `${userInfo.name} 的西瓜视频`, diff --git a/lib/routes/j-test/namespace.ts b/lib/routes/j-test/namespace.ts new file mode 100644 index 00000000000000..3c36d198c9bb78 --- /dev/null +++ b/lib/routes/j-test/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '实用日本语鉴定考试(J.TEST)', + url: 'www.j-test.com', + lang: 'ja', +}; diff --git a/lib/routes/j-test/news.ts b/lib/routes/j-test/news.ts new file mode 100644 index 00000000000000..ab102eac10f49b --- /dev/null +++ b/lib/routes/j-test/news.ts @@ -0,0 +1,64 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +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: '/news', + name: '公告', + url: 'www.j-test.com', + maintainers: ['kuhahku'], + example: '/j-test/news', + parameters: {}, + categories: ['study'], + features: { + supportRadar: true, + }, + radar: [ + { + source: ['www.j-test.com'], + target: '/news', + }, + ], + handler, + description: '', +}; + +async function handler() { + const baseUrl = 'http://www.j-test.com'; + const response = await ofetch(baseUrl); + const $ = load(response); + + const list = $('#content1 > .center > .col_box1 > .col_body1 > ul > li') + .toArray() + .map((item) => { + const [title, date] = $(item).text().trim().replaceAll(']', '').split(' ['); + const link = new URL($(item).children('a').attr('href')!, baseUrl).href; + const pubDate = timezone(parseDate(date), +8); + return { + title, + link, + pubDate, + description: '', + }; + }); + + 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 > table').html() ?? ''; + return item; + }) + ) + ); + + return { + title: '实用日本语鉴定考试(J.TEST)公告', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/jandan/namespace.ts b/lib/routes/jandan/namespace.ts index 6767717903de77..6266018917e2c7 100644 --- a/lib/routes/jandan/namespace.ts +++ b/lib/routes/jandan/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '煎蛋', url: 'jandan.net', + lang: 'zh-CN', }; diff --git a/lib/routes/japanpost/namespace.ts b/lib/routes/japanpost/namespace.ts index 79375e4d6372dd..9b654047f5d956 100644 --- a/lib/routes/japanpost/namespace.ts +++ b/lib/routes/japanpost/namespace.ts @@ -1,6 +1,13 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Japanpost 日本郵便', + name: 'Japanpost', url: 'trackings.post.japanpost.jp', + zh: { + name: '日本邮政', + }, + ja: { + name: '日本郵便', + }, + lang: 'ja', }; diff --git a/lib/routes/japanpost/router.ts b/lib/routes/japanpost/router.ts new file mode 100644 index 00000000000000..616116dcf6433e --- /dev/null +++ b/lib/routes/japanpost/router.ts @@ -0,0 +1,40 @@ +import { Route } from '@/types'; +import { track } from './track'; + +export const route: Route = { + name: 'Track & Trace Service', + path: '/track/:reqCode/:locale?', + example: '/japanpost/track/EJ123456789JP/en', + url: 'trackings.post.japanpost.jp/services/srv/search/', + handler: track, + categories: ['other'], + maintainers: ['tuzi3040'], + parameters: { + reqCode: 'Package Number', + locale: 'Language, default to japanese `ja`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: false, // unsupported due to deprecation of `target` as a function in RSSHub-Radar 2.0.19 + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + description: `| Japanese | English | +| -------- | ------- | +| ja | en |`, + zh: { + name: '邮件追踪查询', + description: `| 日语 | 英语 | +| ---- | ---- | +| ja | en |`, + }, + ja: { + name: '郵便追跡サービス', + description: `| 日本語 | 英語 | +| ---- | ---- | +| ja | en |`, + }, +}; diff --git a/lib/routes/japanpost/track.ts b/lib/routes/japanpost/track.ts index 7b2906f55163a8..9f44377a0fc9bb 100644 --- a/lib/routes/japanpost/track.ts +++ b/lib/routes/japanpost/track.ts @@ -1,4 +1,3 @@ -import { Route } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); @@ -11,28 +10,7 @@ import utils from './utils'; let baseTitle = '日本郵便'; const baseUrl = 'https://trackings.post.japanpost.jp/services/srv/search/direct?'; -export const route: Route = { - path: '/track/:reqCode/:locale?', - categories: ['other'], - example: '/japanpost/track/EJ123456789JP/en', - parameters: { reqCode: 'Package Number', locale: 'Language, default to japanese `ja`' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - name: 'Track & Trace Service 郵便追跡サービス', - maintainers: ['tuzi3040'], - handler, - description: `| Japanese | English | - | -------- | ------- | - | ja | en |`, -}; - -async function handler(ctx) { +export async function track(ctx) { const reqCode = ctx.req.param('reqCode'); const reqReqCode = 'reqCodeNo1=' + reqCode; diff --git a/lib/routes/japanpost/utils.ts b/lib/routes/japanpost/utils.ts index 53d883be4a656e..92cdd855bf586d 100644 --- a/lib/routes/japanpost/utils.ts +++ b/lib/routes/japanpost/utils.ts @@ -57,10 +57,10 @@ const utils = { switch (l) { case 'ja': - customFormat = dayjs(t, formatJaDate, true).isValid() ? formatJaDate : dayjs(t, formatJaDateTime, true).isValid() ? formatJaDateTime : undefined; + customFormat = dayjs(t, formatJaDate, true).isValid() ? formatJaDate : (dayjs(t, formatJaDateTime, true).isValid() ? formatJaDateTime : undefined); break; case 'en': - customFormat = dayjs(t, formatEnDate, true).isValid() ? formatEnDate : dayjs(t, formatEnDateTime, true).isValid() ? formatEnDateTime : undefined; + customFormat = dayjs(t, formatEnDate, true).isValid() ? formatEnDate : (dayjs(t, formatEnDateTime, true).isValid() ? formatEnDateTime : undefined); break; } diff --git a/lib/routes/javbus/index.ts b/lib/routes/javbus/index.ts index 37ed73efa21bd7..f07d9b3ebcfe11 100644 --- a/lib/routes/javbus/index.ts +++ b/lib/routes/javbus/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); @@ -20,17 +20,25 @@ const toSize = (raw) => { const allowDomain = new Set(['javbus.com', 'javbus.org', 'javsee.icu', 'javsee.one']); export const route: Route = { - path: '*', + path: '/:path{.+}?', radar: [ { - source: ['www.seejav.pw/'], - target: '', + source: ['www.javbus.com/:path*'], + target: '/:path', }, ], - name: 'Unknown', - maintainers: [], + name: 'Works', + maintainers: ['MegrezZhu', 'CoderTonyChan', 'nczitzk', 'Felix2yu'], + categories: ['multimedia', 'popular'], + view: ViewType.Videos, handler, - url: 'www.seejav.pw/', + url: 'www.javbus.com', + example: '/javbus/star/rwt', + parameters: { + path: { + description: 'Any path of list page on javbus', + }, + }, }; async function handler(ctx) { diff --git a/lib/routes/javbus/namespace.ts b/lib/routes/javbus/namespace.ts index 1badfdc7fd6776..59eac0a6428c19 100644 --- a/lib/routes/javbus/namespace.ts +++ b/lib/routes/javbus/namespace.ts @@ -3,11 +3,11 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'JavBus', url: 'www.javbus.com', - description: `:::warning + description: `::: warning Requests from non-Asia areas will be redirected to login page. ::: -:::tip Language +::: tip Language You can change the language of each route to the languages listed below. | English | 日本语 | 한국의 | 中文 | @@ -15,9 +15,10 @@ You can change the language of each route to the languages listed below. | en | ja | ko | (leave it empty) | ::: -:::tip +::: tip JavBus has multiple backup domains, these routes use default domain \`https://javbus.com\`. If the domain is unreachable, you can add \`?domain=\` to the end of the route to specify the domain to visit. Let say you want to use the backup domain \`https://javsee.icu\`, you can add \`?domain=javsee.icu\` to the end of the route, then the route will be [\`/javbus/en?domain=javsee.icu\`](https://rsshub.app/javbus?domain=javsee.icu) **Note**: **Western** has different domain than the main site, the backup domains are also different. The default domain is \`https://javbus.org\` and you can add \`?western_domain=\` to the end of the route to specify the domain to visit. Let say you want to use the backup domain \`https://javsee.one\`, you can add \`?western_domain=javsee.one\` to the end of the route, then the route will be [\`/javbus/western/en?western_domain=javsee.one\`](https://rsshub.app/javbus/western?western_domain=javsee.one) :::`, + lang: 'en', }; diff --git a/lib/routes/javdb/actors.ts b/lib/routes/javdb/actors.ts index a4bc97517e13d5..782506a47c0531 100644 --- a/lib/routes/javdb/actors.ts +++ b/lib/routes/javdb/actors.ts @@ -31,17 +31,21 @@ export const route: Route = { handler, url: 'javdb.com/', description: `| 全部 | 可播放 | 單體作品 | 可下載 | 含字幕 | - | ---- | ------ | -------- | ------ | ------ | - | | p | s | d | c | +| ---- | ------ | -------- | ------ | ------ | +| | p | s | d | c | - 所有演员编号参见 [演員庫](https://javdb.com/actors)`, + 所有演员编号参见 [演員庫](https://javdb.com/actors) + + 可用 addon_tags 参数添加额外的过滤 tag,可从网页 url 中获取,例如 \`/javdb/actors/R2Vg?addon_tags=212,18\` 可筛选 \`VR\` 和 \`中出\`。`, }; async function handler(ctx) { const id = ctx.req.param('id'); const filter = ctx.req.param('filter') ?? ''; + const addonTags = ctx.req.query('addon_tags') ?? ''; - const currentUrl = `/actors/${id}${filter ? `?t=${filter}` : ''}`; + const finalTags = addonTags && filter ? `${filter},${addonTags}` : `${filter}${addonTags}`; + const currentUrl = `/actors/${id}${finalTags ? `?t=${finalTags}` : ''}`; const filters = { '': '', diff --git a/lib/routes/javdb/index.ts b/lib/routes/javdb/index.ts index 67d24d9f780af2..0c24e38f0dc9ac 100644 --- a/lib/routes/javdb/index.ts +++ b/lib/routes/javdb/index.ts @@ -2,35 +2,35 @@ import { Route } from '@/types'; import utils from './utils'; export const route: Route = { - path: ['/home/:category?/:sort?/:filter?', '/:category?/:sort?/:filter?'], + path: '/home/:category?/:sort?/:filter?', radar: [ { source: ['javdb.com/'], - target: '', }, ], - name: 'Unknown', + name: '主页', + example: '/javdb/home', + parameters: { category: '分类,见下表,默认为 `有碼`', sort: '排序,见下表,默认为 `磁鏈更新排序`', filter: '过滤,见下表,默认为 `可下载`' }, maintainers: ['nczitzk'], handler, url: 'javdb.com/', description: `分类 - | 有碼 | 無碼 | 歐美 | - | -------- | ---------- | ------- | - | censored | uncensored | western | +| 有碼 | 無碼 | 歐美 | +| -------- | ---------- | ------- | +| censored | uncensored | western | 排序 - | 发布日期排序 | 磁鏈更新排序 | - | ------------ | ------------ | - | 1 | 2 | +| 发布日期排序 | 磁鏈更新排序 | +| ------------ | ------------ | +| 1 | 2 | 过滤 - | 全部 | 可下载 | 含字幕 | 含短評 | - | ---- | ------ | ------ | ------ | - | 0 | 1 | 2 | 3 |`, - url: 'javdb.com/', +| 全部 | 可下载 | 含字幕 | 含短評 | +| ---- | ------ | ------ | ------ | +| 0 | 1 | 2 | 3 |`, }; async function handler(ctx) { diff --git a/lib/routes/javdb/makers.ts b/lib/routes/javdb/makers.ts index ce61dcc5ed57e6..5d4dd2860b4a2d 100644 --- a/lib/routes/javdb/makers.ts +++ b/lib/routes/javdb/makers.ts @@ -31,8 +31,8 @@ export const route: Route = { handler, url: 'javdb.com/', description: `| 全部 | 可播放 | 單體作品 | 可下載 | 字幕 | 預覽圖 | - | ---- | -------- | -------- | -------- | ----- | ------- | - | | playable | single | download | cnsub | preview | +| ---- | -------- | -------- | -------- | ----- | ------- | +| | playable | single | download | cnsub | preview | 所有片商编号参见 [片商庫](https://javdb.com/makers)`, }; diff --git a/lib/routes/javdb/namespace.ts b/lib/routes/javdb/namespace.ts index 8f3d2325a3c8a9..1c79c32898f002 100644 --- a/lib/routes/javdb/namespace.ts +++ b/lib/routes/javdb/namespace.ts @@ -3,7 +3,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'JavDB', url: 'javdb.com', - description: `:::tip + description: `::: tip JavDB 有多个备用域名,本路由默认使用永久域名 \`https://javdb.com\` ,若该域名无法访问,可以通过在路由最后加上 \`?domain=<域名>\` 指定路由访问的域名。如指定备用域名为 \`https://javdb36.com\`,则在所有 JavDB 路由最后加上 \`?domain=javdb36.com\` 即可,此时路由为 [\`/javdb?domain=javdb36.com\`](https://rsshub.app/javdb?domain=javdb36.com) 如果加入了 **分類** 参数,直接在分類参数后加入 \`?domain=<域名>\` 即可。如指定分類 URL 为 \`https://javdb.com/tags?c2=5&c10=1\` 并指定备用域名为 \`https://javdb36.com\`,即在 \`/javdb/tags/c2=5&c10=1\` 最后加上 \`?domain=javdb36.com\`,此时路由为 [\`/javdb/tags/c2=5&c10=1?domain=javdb36.com\`](https://rsshub.app/javdb/tags/c2=5\&c10=1?domain=javdb36.com) @@ -11,9 +11,10 @@ JavDB 有多个备用域名,本路由默认使用永久域名 \`https://javdb. **排行榜**、**搜索**、**演員**、**片商** 参数同适用于 **分類** 参数的上述规则 ::: -:::tip +::: tip 你可以通过指定 \`limit\` 参数来获取特定数量的条目,即可以通过在路由后方加上 \`?limit=25\`,默认为单次获取 20 个条目,即默认 \`?limit=20\` 因为该站有反爬检测,所以不应将此值调整过高 :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/javdb/rankings.ts b/lib/routes/javdb/rankings.ts index 5e767ad44ec770..304c87423d2c53 100644 --- a/lib/routes/javdb/rankings.ts +++ b/lib/routes/javdb/rankings.ts @@ -32,15 +32,15 @@ export const route: Route = { url: 'javdb.com/', description: `分类 - | 有碼 | 無碼 | 歐美 | - | -------- | ---------- | ------- | - | censored | uncensored | western | +| 有碼 | 無碼 | 歐美 | +| -------- | ---------- | ------- | +| censored | uncensored | western | 时间 - | 日榜 | 週榜 | 月榜 | - | ----- | ------ | ------- | - | daily | weekly | monthly |`, +| 日榜 | 週榜 | 月榜 | +| ----- | ------ | ------- | +| daily | weekly | monthly |`, }; async function handler(ctx) { diff --git a/lib/routes/javdb/search.ts b/lib/routes/javdb/search.ts index be869f49dd2087..f4c67cbd1e48a6 100644 --- a/lib/routes/javdb/search.ts +++ b/lib/routes/javdb/search.ts @@ -32,15 +32,15 @@ export const route: Route = { url: 'javdb.com/', description: `过滤 - | 全部 | 占位 | 可播放 | 單體作品 | 演員 | 片商 | 導演 | 系列 | 番號 | 可下載 | 字幕 | 預覽圖 | - | ---- | ---- | -------- | -------- | ----- | ----- | -------- | ------ | ---- | -------- | ----- | ------- | - | | none | playable | single | actor | maker | director | series | code | download | cnsub | preview | +| 全部 | 占位 | 可播放 | 單體作品 | 演員 | 片商 | 導演 | 系列 | 番號 | 可下載 | 字幕 | 預覽圖 | +| ---- | ---- | -------- | -------- | ----- | ----- | -------- | ------ | ---- | -------- | ----- | ------- | +| | none | playable | single | actor | maker | director | series | code | download | cnsub | preview | 排序 - | 按相关度排序 | 按发布时间排序 | - | ------------ | -------------- | - | 0 | 1 |`, +| 按相关度排序 | 按发布时间排序 | +| ------------ | -------------- | +| 0 | 1 |`, }; async function handler(ctx) { diff --git a/lib/routes/javdb/series.ts b/lib/routes/javdb/series.ts index 10172c5c95d9f0..ffcb0f4dd49587 100644 --- a/lib/routes/javdb/series.ts +++ b/lib/routes/javdb/series.ts @@ -31,8 +31,8 @@ export const route: Route = { handler, url: 'javdb.com/', description: `| 全部 | 可播放 | 單體作品 | 可下載 | 字幕 | 預覽圖 | - | ---- | -------- | -------- | -------- | ----- | ------- | - | | playable | single | download | cnsub | preview | +| ---- | -------- | -------- | -------- | ----- | ------- | +| | playable | single | download | cnsub | preview | 所有系列编号参见 [系列庫](https://javdb.com/series)`, }; @@ -52,7 +52,7 @@ async function handler(ctx) { preview: '預覽圖', }; - const title = `JavDB${filters[filter] === '' ? '' : ` - ${filter[filter]}`} `; + const title = `JavDB${filters[filter] === '' ? '' : ` - ${filters[filter]}`} `; return await utils.ProcessItems(ctx, currentUrl, title); } diff --git a/lib/routes/javdb/tags.ts b/lib/routes/javdb/tags.ts index 94712a76110911..f9aa38426dc036 100644 --- a/lib/routes/javdb/tags.ts +++ b/lib/routes/javdb/tags.ts @@ -30,17 +30,17 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'javdb.com/', - description: `:::tip + description: `::: tip 在 [分類](https://javdb.com/tags) 中选定分类后,URL 中 \`tags?\` 后的字段即为筛选参数。 如 \`https://javdb.com/tags?c2=5&c10=1\` 中 \`c2=5&c10=1\` 为筛选参数。 - ::: +::: 分类 - | 有碼 | 無碼 | 歐美 | - | -------- | ---------- | ------- | - | censored | uncensored | western |`, +| 有碼 | 無碼 | 歐美 | +| -------- | ---------- | ------- | +| censored | uncensored | western |`, }; async function handler(ctx) { diff --git a/lib/routes/javdb/utils.ts b/lib/routes/javdb/utils.ts index 545fe9b058a77c..fc4b0b6171badc 100644 --- a/lib/routes/javdb/utils.ts +++ b/lib/routes/javdb/utils.ts @@ -33,6 +33,9 @@ const ProcessItems = async (ctx, currentUrl, title) => { method: 'get', url: url.href, cookieJar, + headers: { + 'User-Agent': config.trueUA, + }, }); const $ = load(response.data); @@ -58,6 +61,9 @@ const ProcessItems = async (ctx, currentUrl, title) => { method: 'get', url: item.link, cookieJar, + headers: { + 'User-Agent': config.trueUA, + }, }); const content = load(detailResponse.data); diff --git a/lib/routes/javdb/videocodes.ts b/lib/routes/javdb/videocodes.ts new file mode 100644 index 00000000000000..3dc9a19847dfad --- /dev/null +++ b/lib/routes/javdb/videocodes.ts @@ -0,0 +1,56 @@ +import { Route } from '@/types'; +import utils from './utils'; + +export const route: Route = { + path: '/video_codes/:code/:filter?', + categories: ['multimedia'], + example: '/javdb/video_codes/SIVR', + parameters: { id: '番号前缀', filter: '过滤,见下表,默认为 `全部`' }, + features: { + requireConfig: [ + { + name: 'JAVDB_SESSION', + description: 'JavDB登陆后的session值,可在控制台的cookie下查找 `_jdb_session` 的值,即可获取', + optional: true, + }, + ], + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['javdb.com/'], + target: '', + }, + ], + name: '番号', + maintainers: ['sgpublic'], + handler, + url: 'javdb.com/', + description: `| 全部 | 可播放 | 單體作品 | 可下載 | 字幕 | 預覽圖 | +| ---- | -------- | -------- | -------- | ----- | ------- | +| | playable | single | download | cnsub | preview |`, +}; + +async function handler(ctx) { + const code = ctx.req.param('code'); + const filter = ctx.req.param('filter') ?? ''; + + const currentUrl = `/video_codes/${code}${filter ? `?f=${filter}` : ''}`; + + const filters = { + '': '', + playable: '可播放', + single: '單體作品', + download: '可下載', + cnsub: '字幕', + preview: '預覽圖', + }; + + const title = `JavDB${filters[filter] === '' ? '' : ` - ${filters[filter]}`} `; + + return await utils.ProcessItems(ctx, currentUrl, title); +} diff --git a/lib/routes/javlibrary/bestrated.ts b/lib/routes/javlibrary/bestrated.ts index 770100251a16fd..9964bea6ebc840 100644 --- a/lib/routes/javlibrary/bestrated.ts +++ b/lib/routes/javlibrary/bestrated.ts @@ -8,8 +8,8 @@ export const route: Route = { maintainers: [], handler, description: `| Last Month | All Time | - | ---------- | -------- | - | 1 | 2 |`, +| ---------- | -------- | +| 1 | 2 |`, }; async function handler(ctx) { diff --git a/lib/routes/javlibrary/bestreviews.ts b/lib/routes/javlibrary/bestreviews.ts index ba3507c84630ae..65cb7894f90ed9 100644 --- a/lib/routes/javlibrary/bestreviews.ts +++ b/lib/routes/javlibrary/bestreviews.ts @@ -19,8 +19,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| Last Month | All Time | - | ---------- | -------- | - | 1 | 2 |`, +| ---------- | -------- | +| 1 | 2 |`, }; async function handler(ctx) { diff --git a/lib/routes/javlibrary/genre.ts b/lib/routes/javlibrary/genre.ts index e4f432f4a9ef37..f289d82aa549e5 100644 --- a/lib/routes/javlibrary/genre.ts +++ b/lib/routes/javlibrary/genre.ts @@ -8,12 +8,12 @@ export const route: Route = { maintainers: [], handler, description: `| videos with comments (by date) | everything (by date) | - | ------------------------------ | -------------------- | - | 1 | 2 | +| ------------------------------ | -------------------- | +| 1 | 2 | - :::tip +::: tip See [Categories](https://www.javlibrary.com/en/genres.php) to view all categories. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/javlibrary/maker.ts b/lib/routes/javlibrary/maker.ts index b42254003cf5ad..f62d80a797a990 100644 --- a/lib/routes/javlibrary/maker.ts +++ b/lib/routes/javlibrary/maker.ts @@ -19,8 +19,8 @@ export const route: Route = { maintainers: [], handler, description: `| videos with comments (by date) | everything (by date) | - | ------------------------------ | -------------------- | - | 1 | 2 |`, +| ------------------------------ | -------------------- | +| 1 | 2 |`, }; async function handler(ctx) { diff --git a/lib/routes/javlibrary/mostwanted.ts b/lib/routes/javlibrary/mostwanted.ts index 70b6ad498a1487..f20cfbeb0838ee 100644 --- a/lib/routes/javlibrary/mostwanted.ts +++ b/lib/routes/javlibrary/mostwanted.ts @@ -8,8 +8,8 @@ export const route: Route = { maintainers: [], handler, description: `| Last Month | All Time | - | ---------- | -------- | - | 1 | 2 |`, +| ---------- | -------- | +| 1 | 2 |`, }; async function handler(ctx) { diff --git a/lib/routes/javlibrary/namespace.ts b/lib/routes/javlibrary/namespace.ts index a9ec2ee2e2cbe8..02abfb62dae648 100644 --- a/lib/routes/javlibrary/namespace.ts +++ b/lib/routes/javlibrary/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'JAVLibrary', url: 'javlibrary.com', + lang: 'en', }; diff --git a/lib/routes/javlibrary/newrelease.ts b/lib/routes/javlibrary/newrelease.ts index 3310f8058da1dc..c7f99d7228f4e9 100644 --- a/lib/routes/javlibrary/newrelease.ts +++ b/lib/routes/javlibrary/newrelease.ts @@ -8,8 +8,8 @@ export const route: Route = { maintainers: [], handler, description: `| videos with comments (by date) | everything (by date) | - | ------------------------------ | -------------------- | - | 1 | 2 |`, +| ------------------------------ | -------------------- | +| 1 | 2 |`, }; async function handler(ctx) { diff --git a/lib/routes/javlibrary/star.ts b/lib/routes/javlibrary/star.ts index 526454e688ed5b..ecc606ba69d051 100644 --- a/lib/routes/javlibrary/star.ts +++ b/lib/routes/javlibrary/star.ts @@ -19,14 +19,14 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| videos with comments (by date) | everything (by date) | - | ------------------------------ | -------------------- | - | 1 | 2 | +| ------------------------------ | -------------------- | +| 1 | 2 | - :::tip +::: tip See [Ranking](https://www.javlibrary.com/en/star_mostfav.php) to view stars by ranks. See [Directory](https://www.javlibrary.com/en/star_list.php) to view all stars. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/javlibrary/user.ts b/lib/routes/javlibrary/user.ts index 719beb5f2d2c67..e85759c2ad8ccc 100644 --- a/lib/routes/javlibrary/user.ts +++ b/lib/routes/javlibrary/user.ts @@ -8,8 +8,8 @@ export const route: Route = { maintainers: [], handler, description: `| Wanted | Watched | Owned | - | ---------- | ----------- | --------- | - | userwanted | userwatched | userowned |`, +| ---------- | ----------- | --------- | +| userwanted | userwatched | userowned |`, }; async function handler(ctx) { diff --git a/lib/routes/javtiful/actress.ts b/lib/routes/javtiful/actress.ts new file mode 100644 index 00000000000000..7ddf8a5af0760d --- /dev/null +++ b/lib/routes/javtiful/actress.ts @@ -0,0 +1,37 @@ +import { Route, Data, DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseItems } from './utils'; + +export const route: Route = { + path: '/actress/:id', + name: 'Actress', + maintainers: ['huanfe1'], + example: '/javtiful/actress/akari-tsumugi', + parameters: { id: 'Actress name' }, + handler, + categories: ['multimedia'], + radar: [ + { + source: ['javtiful.com/actress/:id', 'javtiful.com/actress/:id/*'], + target: '/actress/:id', + }, + ], +}; + +async function handler(ctx): Promise { + const { id } = ctx.req.param(); + const html = await ofetch(`https://javtiful.com/actress/${id}`); + const $ = load(html); + const items: DataItem[] = $('section .card:not(:has(.bg-danger))') + .toArray() + .map((item) => parseItems($(item))); + return { + title: $('.channel-item__name_details a').text(), + link: `https://javtiful.com/actress/${id}`, + allowEmpty: true, + item: items, + image: $('.content-section-title img').attr('src'), + language: $('html').attr('lang'), + }; +} diff --git a/lib/routes/javtiful/channel.ts b/lib/routes/javtiful/channel.ts new file mode 100644 index 00000000000000..f0c82b2585a1d4 --- /dev/null +++ b/lib/routes/javtiful/channel.ts @@ -0,0 +1,37 @@ +import { Route, Data, DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseItems } from './utils'; + +export const route: Route = { + path: '/channel/:id', + name: 'Channel', + maintainers: ['huanfe1'], + example: '/javtiful/channel/madonna', + parameters: { id: 'Channel name' }, + handler, + categories: ['multimedia'], + radar: [ + { + source: ['javtiful.com/channel/:id', 'javtiful.com/channel/:id/*'], + target: '/channel/:id', + }, + ], +}; + +async function handler(ctx): Promise { + const { id } = ctx.req.param(); + const html = await ofetch(`https://javtiful.com/channel/${id}`); + const $ = load(html); + const items: DataItem[] = $('section .card:not(:has(.bg-danger))') + .toArray() + .map((item) => parseItems($(item))); + return { + title: $('.channel-item__name_details a').text(), + link: `https://javtiful.com/channel/${id}`, + allowEmpty: true, + item: items, + image: $('.content-section-title img').attr('src'), + language: $('html').attr('lang'), + }; +} diff --git a/lib/routes/watasuke/namespace.ts b/lib/routes/javtiful/namespace.ts similarity index 67% rename from lib/routes/watasuke/namespace.ts rename to lib/routes/javtiful/namespace.ts index 611159d8ecb99d..1d4e4d25fba00a 100644 --- a/lib/routes/watasuke/namespace.ts +++ b/lib/routes/javtiful/namespace.ts @@ -1,7 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Watasuke', - url: 'watasuke.net', - categories: ['blog'], + name: 'Javtiful', + url: 'javtiful.com', + lang: 'en', }; diff --git a/lib/routes/javtiful/templates/description.art b/lib/routes/javtiful/templates/description.art new file mode 100644 index 00000000000000..393036a449aaba --- /dev/null +++ b/lib/routes/javtiful/templates/description.art @@ -0,0 +1,7 @@ +{{ if previewVideo }} + +{{else if poster}} + +{{ /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 = /

    (\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 }} +

    + {{ image.alt }} +
    + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/jin10/index.ts b/lib/routes/jin10/index.ts index 939ddb98f7fd9c..ff4d02af6ead42 100644 --- a/lib/routes/jin10/index.ts +++ b/lib/routes/jin10/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); @@ -12,7 +12,8 @@ import { config } from '@/config'; export const route: Route = { path: '/:important?', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Notifications, example: '/jin10', parameters: { important: '只看重要,任意值开启,留空关闭' }, features: { diff --git a/lib/routes/jin10/namespace.ts b/lib/routes/jin10/namespace.ts index 79e353aa4bb2a3..12a8315c424995 100644 --- a/lib/routes/jin10/namespace.ts +++ b/lib/routes/jin10/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '金十数据', url: 'jin10.com', + lang: 'zh-CN', }; diff --git a/lib/routes/jin10/topic.ts b/lib/routes/jin10/topic.ts index 8d69d9744f059e..73f2b538ef09ac 100644 --- a/lib/routes/jin10/topic.ts +++ b/lib/routes/jin10/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 { parseDate } from '@/utils/parse-date'; @@ -7,7 +7,8 @@ import { config } from '@/config'; export const route: Route = { path: '/topic/:id', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/jin10/topic/396', parameters: { id: 'N' }, features: { diff --git a/lib/routes/jingzhengu/namespace.ts b/lib/routes/jingzhengu/namespace.ts new file mode 100644 index 00000000000000..6831d5f9a93e9c --- /dev/null +++ b/lib/routes/jingzhengu/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '精真估', + url: 'www.jingzhengu.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/jingzhengu/news.ts b/lib/routes/jingzhengu/news.ts new file mode 100644 index 00000000000000..6f434f9cecc8b4 --- /dev/null +++ b/lib/routes/jingzhengu/news.ts @@ -0,0 +1,77 @@ +import { Route } from '@/types'; +import { NewsInfo, NewsDetail } from './types'; + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import { sign } from './utils'; + +export const route: Route = { + path: '/news', + categories: ['other'], + example: '/jingzhengu/news', + radar: [ + { + source: ['www.jingzhengu.com'], + }, + ], + name: '资讯', + maintainers: ['TonyRL'], + handler, + url: 'www.jingzhengu.com', +}; + +async function handler() { + const baseUrl = 'https://www.jingzhengu.com'; + + const payload: Map = new Map([ + ['pageNo', 1], + ['middleware', String(Date.now())], + ]); + const response = await ofetch(`${baseUrl}/news/makeNewsInfo`, { + method: 'POST', + body: { + ...Object.fromEntries(payload), + sign: sign(payload), + }, + }); + + const list = response.data.articles.map((item) => ({ + title: item.title, + description: item.summary, + link: `${baseUrl}/#/cn/Details_${item.addDate.split(' ')[0].replaceAll('-', '')}${item.id}.html`, + pubDate: timezone(parseDate(item.addDate, 'YYYY-MM-DD HH:mm:ss'), 8), + author: item.author, + id: item.id, + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const payload: Map = new Map([ + ['id', item.id], + ['middleware', String(Date.now())], + ]); + + const response = await ofetch(`${baseUrl}/news/makeNewsDetail`, { + method: 'POST', + body: { + ...Object.fromEntries(payload), + sign: sign(payload), + }, + }); + + item.description = response.data.content; + + return item; + }) + ) + ); + + return { + title: '精真估 > 资讯', + link: `${baseUrl}/#/index/boot`, + item: items, + }; +} diff --git a/lib/routes/jingzhengu/types.ts b/lib/routes/jingzhengu/types.ts new file mode 100644 index 00000000000000..1958a20a0a7e2b --- /dev/null +++ b/lib/routes/jingzhengu/types.ts @@ -0,0 +1,39 @@ +interface Article { + addDate: string; + author: string; + channelId: number; + content: string; + externalUrl: string; + firstPage: string; + fromArticleId: number; + fromChannelId: number; + id: number; + img2: string; + imgUrl: string; + isImg: number; + summary: string; + title: string; + viewNum: number; +} + +interface pageInf { + currentPage: number; + pageCount: number; + pageSize: number; + pageTotal: number; +} + +export interface NewsInfo { + code: number; + data: { + articles: Article[]; + pageInf: pageInf; + }; + msg: string; +} + +export interface NewsDetail { + code: number; + data: Article; + msg: string; +} diff --git a/lib/routes/jingzhengu/utils.ts b/lib/routes/jingzhengu/utils.ts new file mode 100644 index 00000000000000..242826a9cdd344 --- /dev/null +++ b/lib/routes/jingzhengu/utils.ts @@ -0,0 +1,35 @@ +import md5 from '@/utils/md5'; + +function link(str: string, ...args: string[]): string { + let result = args.map((arg) => arg + str).join(''); + if (result.search('-')) { + result = result.substring(0, result.length - 1); + } + return result; +} + +function replaceCharAt(str: string, index: number, replacement: string) { + return index < 0 || index >= str.length ? str : str.slice(0, index) + replacement + str.slice(index + 1); +} + +export function sign(payload: Map) { + const map = new Map(); + const lowerCaseKeys: string[] = []; + + for (const [key, value] of payload.entries()) { + const lowerCaseKey = key.toLowerCase(); + lowerCaseKeys.push(lowerCaseKey); + map.set(lowerCaseKey, typeof value === 'string' ? value.toLowerCase() : value); + } + + const sortedString = lowerCaseKeys + .sort() + .map((key) => key + '=' + map.get(key)) + .join(''); + const linkedString = link('--'.substring(0, 1), '#CEAIWER', '892F', 'KB97', 'JKB6', 'HJ7OC7C8', 'GJZG'); + const lastSeparatorIndex = linkedString.lastIndexOf('--'.substring(0, 1)); // 32 + const replacedString = replaceCharAt(linkedString, lastSeparatorIndex, ''); // #CEAIWER-892F-KB97-JKB6-HJ7OC7C8GJZG + const finalString = (sortedString + replacedString).toLowerCase(); + + return md5(finalString); +} diff --git a/lib/routes/jinritemai/docs.ts b/lib/routes/jinritemai/docs.ts new file mode 100644 index 00000000000000..daffd9c41863ab --- /dev/null +++ b/lib/routes/jinritemai/docs.ts @@ -0,0 +1,74 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +const typeMap = { + '5': '全部公告', + '19': '产品发布', + '21': '规则变更', + '20': '维护公告', + '22': '其他公告', +}; + +/** + * + * @param ctx {import('koa').Context} + */ +export const route: Route = { + path: '/docs/:dirId?', + categories: ['programming'], + example: '/jinritemai/docs/19', + parameters: { dirId: '公告分类, 可在页面URL获取 默认为全部' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '平台公告', + maintainers: ['blade0910'], + handler, + description: `| 类型 | type | +| --------- | ---------- | +| 全部公告 | 5 | +| 产品发布 | 19 | +| 规则变更 | 21 | +| 维护公告 | 20 | +| 其他公告 | 22 |`, +}; + +async function handler(ctx) { + const dirId = ctx.req.param('dirId') || '5'; + const url = `https://op.jinritemai.com/doc/external/open/queryDocArticleList?pageIndex=0&pageSize=10&status=1&dirId=${dirId}&orderType=3`; + const response = await got({ method: 'get', url }); + + const list = response.data.data.articles.map((item) => ({ + title: item.title, + id: item.id, + dirName: item.dirName, + link: `https://op.jinritemai.com/docs/notice-docs/${dirId}/${item.id}`, + pubDate: parseDate(item.updateTime * 1000), + })); + + const result = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const itemResponse = await got({ + method: 'get', + url: `https://op.jinritemai.com/doc/external/open/queryDocArticleDetail?articleId=${item.id}&onlyView=false`, + }); + item.description = itemResponse.data.data.article.content; + return item; + }) + ) + ); + + return { + title: `抖店开放平台 - ${typeMap[dirId] ?? '平台公告'}`, + link: `https://op.jinritemai.com/docs/notice-docs/${dirId}`, + item: result, + }; +} diff --git a/lib/routes/jinritemai/namespace.ts b/lib/routes/jinritemai/namespace.ts new file mode 100644 index 00000000000000..91426b58bb4cb9 --- /dev/null +++ b/lib/routes/jinritemai/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '抖店开放平台', + url: 'op.jinritemai.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/jinse/catalogue.ts b/lib/routes/jinse/catalogue.ts index 3fadc032736d4c..931dc2b8018b44 100644 --- a/lib/routes/jinse/catalogue.ts +++ b/lib/routes/jinse/catalogue.ts @@ -39,12 +39,12 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 政策 | 行情 | DeFi | 矿业 | 以太坊 2.0 | - | ------- | ------------ | ---- | ----- | ---------- | - | zhengce | fenxishishuo | defi | kuang | 以太坊 2.0 | +| ------- | ------------ | ---- | ----- | ---------- | +| zhengce | fenxishishuo | defi | kuang | 以太坊 2.0 | - | 产业 | IPFS | 技术 | 百科 | 研报 | - | -------- | ---- | ---- | ----- | ------------- | - | industry | IPFS | tech | baike | capitalmarket |`, +| 产业 | IPFS | 技术 | 百科 | 研报 | +| -------- | ---- | ---- | ----- | ------------- | +| industry | IPFS | tech | baike | capitalmarket |`, }; async function handler(ctx) { diff --git a/lib/routes/jinse/lives.ts b/lib/routes/jinse/lives.ts index ed2cb67a493d19..13f8b72952cba6 100644 --- a/lib/routes/jinse/lives.ts +++ b/lib/routes/jinse/lives.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); @@ -19,9 +19,16 @@ const categories = { export const route: Route = { path: '/lives/:category?', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Notifications, example: '/jinse/lives', - parameters: { category: '分类,见下表,默认为全部' }, + parameters: { + category: { + description: '分类', + options: Object.entries(categories).map(([value, label]) => ({ value, label })), + default: '0', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -34,8 +41,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 全部 | 精选 | 政策 | 数据 | NFT | 项目 | - | ---- | ---- | ---- | ---- | --- | ---- | - | 0 | 1 | 2 | 3 | 4 | 5 |`, +| ---- | ---- | ---- | ---- | --- | ---- | +| 0 | 1 | 2 | 3 | 4 | 5 |`, }; async function handler(ctx) { diff --git a/lib/routes/jinse/namespace.ts b/lib/routes/jinse/namespace.ts index 549bba2c8b2ed7..ae1b0686377d2a 100644 --- a/lib/routes/jinse/namespace.ts +++ b/lib/routes/jinse/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '金色财经', url: 'jinse.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/jinse/timeline.ts b/lib/routes/jinse/timeline.ts index ff7532a332b8c1..f3d7a41ca1d8bc 100644 --- a/lib/routes/jinse/timeline.ts +++ b/lib/routes/jinse/timeline.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); @@ -11,9 +11,31 @@ import path from 'node:path'; export const route: Route = { path: '/timeline/:category?', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/jinse/timeline', - parameters: { category: '分类,见下表,默认为头条' }, + parameters: { + category: { + description: '分类', + options: [ + { value: '头条', label: '头条' }, + { value: '独家', label: '独家' }, + { value: '铭文', label: '铭文' }, + { value: '产业', label: '产业' }, + { value: '项目', label: '项目' }, + { value: '政策', label: '政策' }, + { value: 'AI', label: 'AI' }, + { value: 'Web 3.0', label: 'Web 3.0' }, + { value: '以太坊 2.0', label: '以太坊 2.0' }, + { value: 'DeFi', label: 'DeFi' }, + { value: 'Layer2', label: 'Layer2' }, + { value: 'NFT', label: 'NFT' }, + { value: 'DAO', label: 'DAO' }, + { value: '百科', label: '百科' }, + ], + default: '头条', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -26,9 +48,9 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 头条 | 独家 | 铭文 | 产业 | 项目 | - | ------ | ---- | ------- | ---------- | ---- | - | 政策 | AI | Web 3.0 | 以太坊 2.0 | DeFi | - | Layer2 | NFT | DAO | 百科 | |`, +| ------ | ---- | ------- | ---------- | ---- | +| 政策 | AI | Web 3.0 | 以太坊 2.0 | DeFi | +| Layer2 | NFT | DAO | 百科 | |`, }; async function handler(ctx) { diff --git a/lib/routes/jisilu/category.ts b/lib/routes/jisilu/category.ts new file mode 100644 index 00000000000000..528b8f090ccd06 --- /dev/null +++ b/lib/routes/jisilu/category.ts @@ -0,0 +1,115 @@ +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 { rootUrl, processItems } from './util'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +export const handler = async (ctx: Context): Promise => { + const { id } = ctx.req.param(); + + if (!id) { + throw new InvalidParameterError('请填入合法的分类 id,参见广场 https://www.jisilu.cn/explore/'); + } + + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`/category/${id}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'zh'; + + const items: DataItem[] = await processItems($, $('div.aw-question-list'), limit); + + $('div.pagination').remove(); + + const author = $('meta[name="keywords"]').prop('content').split(/,/)[0]; + const feedImage = $('div.aw-logo img').prop('src'); + + return { + title: `${$('title').text()} - ${$('li.active') + .slice(1) + .toArray() + .map((l) => $(l).text()) + .join('|')}`, + description: $('meta[name="description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/category/:id', + name: '分类', + url: 'www.jisilu.cn', + maintainers: ['nczitzk'], + handler, + example: '/jisilu/category/4', + parameters: { + id: '分类 id,可在对应分类页 URL 中找到', + }, + description: `::: tip +若订阅 [债券/可转债](https://www.jisilu.cn/category/4),网址为 \`https://www.jisilu.cn/category/4\`,请截取 \`https://www.jisilu.cn/category/\` 到末尾的部分 \`4\` 作为 \`id\` 参数填入,此时目标路由为 [\`/jisilu/category/4\`](https://rsshub.app/jisilu/category/4)。 +::: + +| 新股 | 债券/可转债 | 套利 | 其他 | 基金 | 股票 | +| ---- | ----------- | ---- | ---- | ---- | ---- | +| 3 | 4 | 5 | 6 | 7 | 8 | +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.jisilu.cn/category/:id'], + target: '/category/:id', + }, + { + title: '新股', + source: ['www.jisilu.cn/category/3'], + target: '/category/3', + }, + { + title: '债券/可转债', + source: ['www.jisilu.cn/category/4'], + target: '/category/4', + }, + { + title: '套利', + source: ['www.jisilu.cn/category/5'], + target: '/category/5', + }, + { + title: '其他', + source: ['www.jisilu.cn/category/6'], + target: '/category/6', + }, + { + title: '基金', + source: ['www.jisilu.cn/category/7'], + target: '/category/7', + }, + { + title: '股票', + source: ['www.jisilu.cn/category/8'], + target: '/category/8', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/jisilu/explore.ts b/lib/routes/jisilu/explore.ts new file mode 100644 index 00000000000000..e88d2e8697f905 --- /dev/null +++ b/lib/routes/jisilu/explore.ts @@ -0,0 +1,79 @@ +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 { rootUrl, processItems } from './util'; + +export const handler = async (ctx: Context): Promise => { + const { filter } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`/${filter ? 'home/' : ''}explore/${filter ?? ''}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'zh'; + + const items: DataItem[] = await processItems($, $('div.aw-question-list'), limit); + + $('div.pagination').remove(); + + const author = $('meta[name="keywords"]').prop('content').split(/,/)[0]; + const feedImage = $('div.aw-logo img').prop('src'); + + return { + title: `${$('title').text()} - ${$('li.active') + .slice(1) + .toArray() + .map((l) => $(l).text()) + .join('|')}`, + description: $('meta[name="description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/explore/:filter?', + name: '广场', + url: 'www.jisilu.cn', + maintainers: ['nczitzk'], + handler, + example: '/jisilu/explore', + parameters: { + category: '过滤器,默认为空,可在对应页 URL 中找到', + }, + description: `::: tip +若订阅 [债券/可转债 - 热门 - 30天](https://www.jisilu.cn/home/explore/category-4__sort_type-hot__day-30),网址为 \`https://www.jisilu.cn/home/explore/category-4__sort_type-hot__day-30\`,请截取 \`https://www.jisilu.cn/home/explore/\` 到末尾的部分 \`category-4__sort_type-hot__day-30\` 作为 \`filter\` 参数填入,此时目标路由为 [\`/jisilu/explore/category-4__sort_type-hot__day-30\`](https://rsshub.app/jisilu/explore/category-4__sort_type-hot__day-30)。 +::: + `, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.jisilu.cn/home/explore/:filter', 'www.jisilu.cn/home/explore', 'www.jisilu.cn/explore'], + target: (params) => { + const filter = params.filter; + + return `/jisilu/explore${filter ? `/${filter}` : ''}`; + }, + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/jisilu/index.ts b/lib/routes/jisilu/index.ts deleted file mode 100644 index 5c0efdf420f2b8..00000000000000 --- a/lib/routes/jisilu/index.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; - -export const route: Route = { - path: '/:category?/:sort?/:day?', - categories: ['bbs'], - example: '/jisilu', - parameters: { category: '分类,见下表,默认为全部,可在 URL 中找到', sort: '排序,见下表,默认为最新,可在 URL 中找到', day: '几天内,见下表,默认为30天,本参数仅在排序参数设定为 `热门` 后才可生效' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['jisilu.cn/home/explore', 'jisilu.cn/explore', 'jisilu.cn/'], - }, - ], - name: '广场', - maintainers: ['nczitzk'], - handler, - url: 'jisilu.cn/home/explore', - description: `分类 - - | 全部 | 债券 / 可转债 | 基金 | 套利 | 新股 | - | ---- | ------------- | ---- | ---- | ---- | - | | 4 | 7 | 5 | 3 | - - 排序 - - | 最新 | 热门 | 按发表时间 | - | ---- | ---- | ---------- | - | | hot | add\_time | - - 几天内 - - | 30 天 | 7 天 | 当天 | - | ----- | ---- | ---- | - | 30 | 7 | 1 |`, -}; - -async function handler(ctx) { - const category = ctx.req.param('category') ?? ''; - const sort = ctx.req.param('sort') ?? ''; - const day = ctx.req.param('day') ?? ''; - - const rootUrl = 'https://www.jisilu.cn'; - let currentUrl = '', - name = '', - response; - - if (category === 'reply' || category === 'topic') { - if (sort) { - currentUrl = `${rootUrl}/people/${sort}`; - response = await got({ - method: 'get', - url: currentUrl, - }); - name = response.data.match(/(.*) 的个人主页 - 集思录<\/title>/)[1]; - response = await got({ - method: 'get', - url: `${rootUrl}/people/ajax/user_actions/uid-${response.data.match(/var PEOPLE_USER_ID = '(.*)'/)[1]}__actions-${category === 'topic' ? 1 : 2}01__page-0`, - }); - } else { - throw new Error('No user.'); - } - } else { - currentUrl = `${rootUrl}/home/explore/category-${category}__sort_type-${sort}__day-${day}`; - response = await got({ - method: 'get', - url: currentUrl, - }); - } - - const $ = load(response.data); - - $('.nav').prevAll('.aw-item').remove(); - - let items = $('.aw-item') - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 30) - .toArray() - .map((item) => { - item = $(item); - const a = item.find('h4 a'); - - return { - title: a.text(), - link: a.attr('href'), - pubDate: timezone( - parseDate( - item - .find('.aw-text-color-999') - .text() - .match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2})/)[1] - ), - +8 - ), - author: category === 'reply' || category === 'topic' ? name : decodeURI(item.find('.aw-user-name').first().attr('href').split('/people/').pop()), - }; - }); - - items = await Promise.all( - items.map((item) => - cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - - const content = load(detailResponse.data); - - content('.aw-dynamic-topic-more-operate').remove(); - - item.description = content('.aw-question-detail-txt').html() + content('.aw-dynamic-topic-content').html(); - - return item; - }) - ) - ); - - return { - title: `${name ? `${name}的${category === 'topic' ? '主题' : '回复'}` : '广场'} - 集思录`, - link: currentUrl, - item: items, - }; -} diff --git a/lib/routes/jisilu/namespace.ts b/lib/routes/jisilu/namespace.ts index 4f5c9450bfe42e..62af0d54ea1f77 100644 --- a/lib/routes/jisilu/namespace.ts +++ b/lib/routes/jisilu/namespace.ts @@ -3,4 +3,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '集思录', url: 'jisilu.cn', + description: '一个以数据为本的投资社区', + lang: 'zh-CN', }; diff --git a/lib/routes/jisilu/people.ts b/lib/routes/jisilu/people.ts new file mode 100644 index 00000000000000..875789e095b97e --- /dev/null +++ b/lib/routes/jisilu/people.ts @@ -0,0 +1,95 @@ +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 { rootUrl, processItems } from './util'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +const actions: { [key: string]: string } = { + questions: '101', + answers: '201', +}; + +export const handler = async (ctx: Context): Promise<Data> => { + const { id, type = 'questions' } = ctx.req.param(); + + if (type && type !== 'answers' && type !== 'questions') { + throw new InvalidParameterError('请填入合法的类型 id,可选值为 `questions` 即 `主题` 或 `answer` 即 `回复`,默认为空,即全部'); + } + + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`/people/${id}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'zh'; + const userId: string | undefined = response.match(/var\sPEOPLE_USER_ID\s=\s'(\d+)';/)?.[1]; + + if (!userId) { + throw new InvalidParameterError('请填入合法的用户 id,参见用户排名 https://www.jisilu.cn/users/'); + } + + const apiUrl: string = new URL(`people/ajax/user_actions/uid-${userId}__actions-${actions[type]}__page-1`, rootUrl).href; + + const detailResponse = await ofetch(apiUrl); + const $$: CheerioAPI = load(detailResponse); + + const items: DataItem[] = await processItems($$, $$('*'), limit); + + const author = $('meta[name="keywords"]').prop('content').split(/,/)[0]; + const feedImage = $('div.aw-logo img').prop('src'); + + return { + title: `${$('title').text()}${type ? ` - ${$(`div#${type} h3`).text()}` : ''}`, + description: $('meta[name="description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/people/:id/:type?', + name: '用户', + url: 'www.jisilu.cn', + maintainers: ['nczitzk'], + handler, + example: '/jisilu/people/天书', + parameters: { + id: '用户 id,可在对应用户页 URL 中找到', + type: '类型,可选值为 `questions` 即 `主题` 或 `answer` 即 `回复`,默认为 `questions` 即 `主题`', + }, + description: `::: tip +若订阅 [天书的主题](https://www.jisilu.cn/people/天书),网址为 \`https://www.jisilu.cn/people/天书\`,请截取 \`https://www.jisilu.cn/people/\` 到末尾的部分 \`天书\` 作为 \`id\` 参数填入,此时目标路由为 [\`/jisilu/people/天书\`](https://rsshub.app/jisilu/people/天书)。 +::: + +::: tip +前往 [用户排名](https://www.jisilu.cn/users/) 查看更多用户。 +::: +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.jisilu.cn/people/:id'], + target: '/people/:id', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/jisilu/topic.ts b/lib/routes/jisilu/topic.ts new file mode 100644 index 00000000000000..9c66af1e0d4759 --- /dev/null +++ b/lib/routes/jisilu/topic.ts @@ -0,0 +1,76 @@ +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 { rootUrl, processItems } from './util'; + +export const handler = async (ctx: Context): Promise<Data> => { + const { id } = ctx.req.param(); + + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`/topic/${id}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'zh'; + + const items: DataItem[] = await processItems($, $('div.aw-question-list'), limit); + + $('div.pagination').remove(); + + const author = $('meta[name="keywords"]').prop('content').split(/,/)[0]; + const feedImage = $('div.aw-logo img').prop('src'); + + return { + title: $('title').text(), + description: $('meta[name="description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/topic/:id', + name: '话题', + url: 'www.jisilu.cn', + maintainers: ['nczitzk'], + handler, + example: '/jisilu/topic/可转债', + parameters: { + id: '话题 id,可在对应话题页 URL 中找到', + }, + description: `::: tip +若订阅 [可转债](https://www.jisilu.cn/topic/可转债),网址为 \`https://www.jisilu.cn/topic/可转债\`,请截取 \`https://www.jisilu.cn/topic/\` 到末尾的部分 \`可转债\` 作为 \`id\` 参数填入,此时目标路由为 [\`/jisilu/topic/可转债\`](https://rsshub.app/jisilu/topic/可转债)。 +::: + +::: tip +前往 [话题广场](https://www.jisilu.cn/topic) 查看更多话题。 +::: +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.jisilu.cn/topic/:id'], + target: '/topic/:id', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/jisilu/util.ts b/lib/routes/jisilu/util.ts new file mode 100644 index 00000000000000..7254beb1c86c5f --- /dev/null +++ b/lib/routes/jisilu/util.ts @@ -0,0 +1,111 @@ +import { type CheerioAPI, type Cheerio, type Element, load } from 'cheerio'; + +import { type DataItem } from '@/types'; + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const rootUrl: string = 'https://www.jisilu.cn'; + +const processItems: ($: CheerioAPI, targetEl: Cheerio<Element>, limit: number) => Promise<DataItem[]> = async ($: CheerioAPI, targetEl: Cheerio<Element>, limit: number) => { + const items: DataItem[] = targetEl + .find('div.aw-item') + .toArray() + .map((item): DataItem => { + const $item: Cheerio<Element> = $(item); + + const aEl: Cheerio<Element> = $item.find('h4 a'); + + const title: string = aEl.text(); + const link: string | undefined = aEl.prop('href'); + + const pubDateStr: string | undefined = $item + .find('.aw-text-color-999') + .text() + .match(/(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})/)?.[1]; + + const authorEl: Cheerio<Element> = $item.find('a.aw-user-name'); + const author: DataItem['author'] = authorEl.prop('href') + ? [ + { + name: authorEl.text(), + url: authorEl.prop('href'), + }, + ] + : authorEl.text(); + + return { + title, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : undefined, + link, + category: $item + .find('span.aw-question-tags a, a.aw-topic-name') + .toArray() + .map((c) => $(c).text()), + author, + }; + }); + + return ( + await Promise.all( + items.map((item) => { + if (!item.link && typeof item.link !== 'string') { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.aw-mod-head h1').text(); + + if (!title) { + return item; + } + + const isAnswer: boolean = item.link ? /answer_id/.test(item.link) : false; + + const description: string = (isAnswer ? $$('div.markitup-box').last() : $$('div.markitup-box').first()).html() ?? ''; + + const metaStr: string = $$(isAnswer ? 'div.aw-dynamic-topic-meta' : 'div.aw-question-detail-meta') + .find('span.aw-text-color-999') + .text(); + + const pubDateStr = metaStr.match(isAnswer ? /(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})/ : /发表时间\s(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})/)?.[1]; + const updatedStr = metaStr.match(/最后修改时间\s(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})/)?.[1]; + + const authorEl: Cheerio<Element> = $$(isAnswer ? 'p.publisher a.aw-user-name' : 'div.aw-side-bar-mod-body a.aw-user-name').first(); + const author: DataItem['author'] = authorEl.prop('href') + ? [ + { + name: authorEl.text(), + url: authorEl.prop('href'), + avatar: authorEl.parent().parent().find('img').first().prop('src'), + }, + ] + : authorEl.text(); + + return { + title, + description, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : item.pubDate, + link: item.link, + category: item.category, + author, + content: { + html: description, + text: $$('div.aw-question-detail-txt').first().text(), + }, + updated: updatedStr ? timezone(parseDate(updatedStr), +8) : item.updated, + }; + }); + }) + ) + ) + .filter((_): _ is DataItem => true) + .slice(0, limit); +}; + +export { rootUrl, processItems }; diff --git a/lib/routes/jiuyangongshe/community.ts b/lib/routes/jiuyangongshe/community.ts new file mode 100644 index 00000000000000..d739cd5831efac --- /dev/null +++ b/lib/routes/jiuyangongshe/community.ts @@ -0,0 +1,149 @@ +import { Data, Route, ViewType } from '@/types'; +import type { Context } from 'hono'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import md5 from '@/utils/md5'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +import { art } from '@/utils/render'; + +const __dirname = getCurrentPath(import.meta.url); + +interface User { + follow_type: number; + investment_style_id: null | string; + user_id: string; + nickname: string; + follow_status: number; + faction_id: string; + avatar: string; + medal_count: number; + style_str: null | string; +} + +interface Stock { + stock_id: string; + name: string; + code: string; +} + +interface ResultItem { + article_id: string; + is_top: number; + user_id: string; + is_like: number; + categoryIdSet: string[]; + source_id: null | string; + title: string; + subtitle: string; + image: null | string; + cover: null | string; + url: null | string; + type: number; + read_limit: null | number; + comment_count: number; + collect_count: number; + like_count: number; + step_count: number; + forward_count: number; + integral: number; + is_user_top: number; + read_integral: number; + read_limit_time: null | string; + fans_limit: number; + copy_limit: number; + old_type: number; + hide: number; + create_time: string; + is_make_word_cloud: number; + sync_time: string; + is_flatter: number; + feature_img: null | string; + interest_disclosure: number; + new_interaction_time: string; + sensitive_words: null | string; + content: string; + user: User; + stock_list: Stock[]; +} + +interface CommunityData { + pageNo: number; + pageSize: number; + orderBy: null | string; + order: null | string; + autoCount: boolean; + map: Record<string, unknown>; + params: string; + result: ResultItem[]; + totalCount: number; + first: number; + totalPages: number; + hasNext: boolean; + nextPage: number; + hasPre: boolean; + prePage: number; +} + +interface Community { + msg: string; + data: CommunityData; + errCode: string; + serverTime: number; +} + +const render = (data) => art(path.join(__dirname, 'templates', 'community-description.art'), data); + +export const route: Route = { + path: '/community', + categories: ['finance', 'popular'], + view: ViewType.Articles, + example: '/jiuyangongshe/community', + maintainers: ['TonyRL'], + name: '社群', + handler, + radar: [ + { + source: ['www.jiuyangongshe.com'], + }, + ], +}; + +async function handler(ctx: Context): Promise<Data> { + const link = `https://www.jiuyangongshe.com`; + + const time = String(Date.now()); + const response = await ofetch<Community>('https://app.jiuyangongshe.com/jystock-app/api/v2/article/community', { + method: 'POST', + headers: { + platform: '3', + timestamp: time, + token: md5(`Uu0KfOB8iUP69d3c:${time}`), + }, + body: { + category_id: '', + limit: ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit') as string, 10) : 30, + order: 0, + start: 1, + type: 0, + back_garden: 0, + }, + }); + + const items = response.data.result.map((item) => ({ + title: item.title, + description: render({ cover: item.cover, content: item.content }), + link: `${link}/a/${item.article_id}`, + pubDate: timezone(parseDate(item.create_time, 'YYYY-MM-DD HH:mm:ss'), 8), + author: item.user.nickname, + category: item.stock_list.map((stock) => stock.name), + })); + + return { + title: '社群 - 韭研公社-研究共享,茁壮成长(原韭菜公社)', + link, + language: 'zh-CN', + item: items, + }; +} diff --git a/lib/routes/jiuyangongshe/namespace.ts b/lib/routes/jiuyangongshe/namespace.ts new file mode 100644 index 00000000000000..690d4829e4e4b8 --- /dev/null +++ b/lib/routes/jiuyangongshe/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '韭研公社', + url: 'www.jiuyangongshe.com', + categories: ['finance'], + lang: 'zh-CN', +}; diff --git a/lib/routes/jiuyangongshe/templates/community-description.art b/lib/routes/jiuyangongshe/templates/community-description.art new file mode 100644 index 00000000000000..6059320b51228f --- /dev/null +++ b/lib/routes/jiuyangongshe/templates/community-description.art @@ -0,0 +1,4 @@ +{{ if cover }} +<img src="{{ cover }}"><br> +{{ /if }} +{{ content }} diff --git a/lib/routes/jjwxc/book.ts b/lib/routes/jjwxc/book.ts index dcfa370563ae9b..c111a1cfa3d710 100644 --- a/lib/routes/jjwxc/book.ts +++ b/lib/routes/jjwxc/book.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 @@ import path from 'node:path'; export const route: Route = { path: '/book/:id?', - categories: ['reading'], + categories: ['reading', 'popular'], + view: ViewType.Notifications, example: '/jjwxc/book/7013024', parameters: { id: '作品 id,可在对应作品页中找到' }, features: { @@ -24,7 +25,7 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, - name: '作品', + name: '作品章节', maintainers: ['nczitzk'], handler, }; @@ -63,6 +64,7 @@ async function handler(ctx) { const chapterUpdatedTime = item.find('td').last().text().trim(); const isVip = item.find('span[itemprop="headline"] font').last().text() === '[VIP]'; + const isLock = item.find('td').eq(1).last().text().trim() === '[锁]'; return { title: `${chapterName} ${chapterIntro}`, @@ -81,6 +83,7 @@ async function handler(ctx) { guid: `jjwxc-${id}#${chapterId}`, pubDate: timezone(parseDate(chapterUpdatedTime), +8), isVip, + isLock, }; }); @@ -88,25 +91,27 @@ async function handler(ctx) { items = await Promise.all( items.slice(0, limit).map((item) => - cache.tryGet(item.link, async () => { - if (!item.isVip) { - const { data: detailResponse } = await got(item.link, { - responseType: 'buffer', - }); + item.isLock + ? Promise.resolve(item) + : cache.tryGet(item.link, async () => { + if (!item.isVip) { + const { data: detailResponse } = await got(item.link, { + responseType: 'buffer', + }); - const content = load(iconv.decode(detailResponse, 'gbk')); + const content = load(iconv.decode(detailResponse, 'gbk')); - content('span.favorite_novel').parent().remove(); + content('span.favorite_novel').parent().remove(); - item.description += art(path.join(__dirname, 'templates/book.art'), { - description: content('div.novelbody').html(), - }); - } + item.description += art(path.join(__dirname, 'templates/book.art'), { + description: content('div.novelbody').html(), + }); + } - delete item.isVip; + delete item.isVip; - return item; - }) + return item; + }) ) ); diff --git a/lib/routes/jjwxc/namespace.ts b/lib/routes/jjwxc/namespace.ts index 46868a4cb8534d..dca8c5578fc9ee 100644 --- a/lib/routes/jjwxc/namespace.ts +++ b/lib/routes/jjwxc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '晋江文学城', url: 'jjwxc.net', + lang: 'zh-CN', }; diff --git a/lib/routes/jlu/ccst/xwzx/index.ts b/lib/routes/jlu/ccst/xwzx/index.ts new file mode 100644 index 00000000000000..c46566b9005049 --- /dev/null +++ b/lib/routes/jlu/ccst/xwzx/index.ts @@ -0,0 +1,63 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; // Custom got instance +import { load } from 'cheerio'; // HTML parser with jQuery-like API + +export const route: Route = { + path: '/ccst/xwzx/:category', + categories: ['university'], + example: '/jlu/ccst/xwzx/gsl', + radar: [ + { + source: ['ccst.jlu.edu.cn/xwzx/gsl.htm', 'ccst.jlu.edu.cn/xwzx/xstd.htm', 'ccst.jlu.edu.cn/xwzx/xytz.htm', 'ccst.jlu.edu.cn/xwzx/xyxw.htm', 'ccst.jlu.edu.cn/xwzx/zsjy.htm'], + }, + ], + name: '吉林大学计算机科学与技术学院 - 新闻中心', + maintainers: ['mayouxi'], + handler, + url: 'ccst.jlu.edu.cn', +}; + +async function handler(ctx: any) { + const category = ctx.req.param('category'); + const baseUrl = 'https://ccst.jlu.edu.cn'; + const url = `${baseUrl}/xwzx/${category}.htm`; + const response = await got(url); + const $ = load(response.body); + + const list = $('.section.container .main .list3 ul li'); + + const titles: { [key: string]: string } = { + gsl: '公示栏', + xstd: '学生天地', + xytz: '学院通知', + xyxw: '学院新闻', + zsjy: '招生就业', + }; + + const titleSuffix = titles[category] || '新闻中心'; // Fallback to '新闻中心' if category is not found + + return { + title: `吉林大学计算机科学与技术学院 - 新闻中心${titleSuffix}`, + link: baseUrl, + description: `吉林大学计算机科学与技术学院 - 新闻中心${titleSuffix}`, + + item: list.toArray().map((item) => { + const el = $(item); + + const linkEl = el.find('a'); + const dateEl = el.find('.date'); + const dateStr = dateEl.text().trim(); + const title = linkEl.text().trim(); + const rawLink = linkEl.attr('href')!.replaceAll('..', ''); // Replace all occurrences of '..' + const link = `${baseUrl}${encodeURI(rawLink)}`; // Encode the URL properly + + const newsDate = new Date(dateStr); + + return { + title, + link, + pubDate: newsDate, + }; + }), + }; +} diff --git a/lib/routes/jlu/jwc.ts b/lib/routes/jlu/jwc.ts new file mode 100644 index 00000000000000..b3056275e0b179 --- /dev/null +++ b/lib/routes/jlu/jwc.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; // 自订的 got +import { load } from 'cheerio'; // 可以使用类似 jQuery 的 API HTML 解析器 + +export const route: Route = { + path: '/jwc', + categories: ['university'], + example: '/jlu/jwc', + radar: [ + { + source: ['jwc.jlu.edu.cn', 'jwc.jlu.edu.cn/index.htm'], + }, + ], + name: '教务通知', + maintainers: ['mayouxi'], + handler, + url: 'jwc.jlu.edu.cn', +}; + +async function handler() { + const baseUrl = 'https://jwc.jlu.edu.cn'; + const response = await got(baseUrl); + const $ = load(response.body); + + const list = $('.section2 .s2-r .s3-list ul li'); + + return { + title: '吉林大学教务处', + link: baseUrl, + description: '吉林大学教务处通知公告', + + item: list?.toArray().map((item) => { + const el = $(item); + + const linkEl = el.find('a'); + const YMDiv = el.find('.tm p'); + const YMStr = YMDiv.text().trim(); + const DDiv = el.find('.tm span'); + const DStr = DDiv.text().trim(); + + const titleDiv = el.find('.s3-info p'); + const title = titleDiv.text().trim(); + + const link = `${baseUrl}/${linkEl.attr('href')}`; + + const newsDate = new Date(YMStr + '-' + DStr); + + return { + title, + link, + pubDate: newsDate, + }; + }), + }; +} diff --git a/lib/routes/jlu/namespace.ts b/lib/routes/jlu/namespace.ts new file mode 100644 index 00000000000000..5cb5bfdc5e288f --- /dev/null +++ b/lib/routes/jlu/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '吉林大学', + url: 'jlu.edu.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/jlu/phy/index.ts b/lib/routes/jlu/phy/index.ts new file mode 100644 index 00000000000000..4b0ed4ad4f501a --- /dev/null +++ b/lib/routes/jlu/phy/index.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/phy/:category/:column/:subcolumn?', + categories: ['university'], + example: '/jlu/phy/xzgz/tzgg', + parameters: { + category: '分类,为「行政工作」、「科学研究」、「人才培养」的拼音小写首字母。', + column: '栏目,当分类为「行政工作」时,为「通知公告」、「学院新闻」、「学院文件」的拼音小写首字母。当分类为「科学研究」时,为「科研动态」、「学术活动」的拼音小写首字母。当分类为「人才培养」时。为「本科生教育」、「研究生教育」、「学团工作」的拼音小写首字母。', + subcolumn: '子栏目。当栏目为「本科生教育」时,为「本科资讯」的拼音大写首字母,或为「教育思想大讨论系列活动」、「培养方案」的拼音小写首字母。当栏目为「研究生教育」时,为「教学通知」的拼音小写首字母。', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['phy.jlu.edu.cn/:category/:column', 'phy.jlu.edu.cn/:category/:column/:subcolumn'], + }, + ], + name: '物理学院', + maintainers: ['tsurumi-yizhou'], + url: 'phy.jlu.edu.cn', + handler: async (ctx) => { + const { category, column, subcolumn } = ctx.req.param(); + const query = subcolumn ? `${column}/${subcolumn}` : column; + const response = await got(`https://phy.jlu.edu.cn/${category}/${query}.htm`); + const $ = load(response.body); + const list = $('.tit-list ul li'); + + return { + title: '吉林大学物理学院', + link: 'https://phy.jlu.edu.cn/', + description: '吉林大学物理学院', + item: list.toArray().map((item) => { + const element = $(item).find('a'); + const title = element.find('.tl-top').find('h3').text().trim(); + const link = element.attr('href')!.replaceAll('../', 'https://phy.jlu.edu.cn/'); + const date = element.find('.tl-top').find('.tl-date'); + const pubDate = date.find('span').text().replaceAll('/', '').trim() + '-' + date.find('b').text(); + return { + title, + link, + pubDate: new Date(pubDate), + }; + }), + }; + }, +}; diff --git a/lib/routes/joins/chinese.ts b/lib/routes/joins/chinese.ts new file mode 100644 index 00000000000000..93be06161bc1f0 --- /dev/null +++ b/lib/routes/joins/chinese.ts @@ -0,0 +1,218 @@ +import { Route } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +export const handler = async (ctx) => { + const { category = '' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + + const rootUrl = 'https://chinese.joins.com'; + const currentUrl = new URL(`news/articleList.html?view_type=s${category ? `&sc_section_code=${category}` : ''}`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('section.article-list-content div.table-row') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + return { + title: item.find('strong').text(), + pubDate: timezone(parseDate(item.find('div.list-dated').text().split(/\|/).pop()), +8), + link: new URL(item.find('a.links').prop('href'), rootUrl).href, + author: item.find('div.list-dated').text().split(/\|/)[0], + language, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + $$('a.articles').remove(); + $$('div.view-copyright, div.ad-template, div.view-editors, div.tag-group').remove(); + + const title = $$('div.article-head-title, div.viewer-titles').text(); + const description = art(path.join(__dirname, 'templates/description.art'), { + images: + $$('div.photo-box').length === 0 + ? undefined + : $$('div.photo-box') + .toArray() + .map((i) => { + const image = $$(i).find('img'); + + return image.prop('src') + ? { + src: image.prop('src'), + } + : undefined; + }), + description: $$('div#article-view-content-div').html(), + }); + const image = $$('meta[property="og:image"]').prop('content'); + + item.title = title; + item.description = description; + item.pubDate = parseDate($$('meta[property="article:published_time"]').prop('content')); + item.category = $$('meta[name="keywords"]').prop('content')?.split(/,/) ?? $$('meta[name="news_keywords"]').prop('content')?.split(/,/) ?? []; + item.author = $$('meta[property="og:article:author"]').prop('content'); + item.content = { + html: description, + text: $$('div#article-view-content-div').text(), + }; + item.image = image; + item.banner = image; + item.language = language; + + return item; + }) + ) + ); + + const image = new URL($('div.user-logo img').prop('src'), rootUrl).href; + + return { + title: `${$(`a[data-code="${category}"]`)?.text() || $('ul#user-menu a').first().text()} - ${$('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: '/chinese/:category?', + name: '中央日报中文版', + url: 'chinese.joins.com', + maintainers: ['nczitzk'], + handler, + example: '/chinese', + parameters: { category: '分类,默认为空,可在对应分类页 URL 中找到 `sc_section_code`' }, + description: `::: tip + 若订阅 [财经](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N1),网址为 \`https://chinese.joins.com/news/articleList.html?sc_section_code=S1N1\`。截取 \`sc_section_code\` 的值作为参数填入,此时路由为 [\`/joins/chinese/S1N1\`](https://rsshub.app/joins/chinese/S1N1)。 +::: + +| 分类 | \`sc_section_code\` | +| ------------------------------------------------------------------------------------------ | ----------------------------------------------- | +| [财经](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N1) | [S1N1](https://rsshub.app/joins/chinese/S1N1) | +| [国际](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N2) | [S1N2](https://rsshub.app/joins/chinese/S1N2) | +| [北韩](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N3) | [S1N3](https://rsshub.app/joins/chinese/S1N3) | +| [政治·社会](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N4) | [S1N4](https://rsshub.app/joins/chinese/S1N4) | +| [中国观察](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N5) | [S1N5](https://rsshub.app/joins/chinese/S1N5) | +| [社论](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N26) | [S1N26](https://rsshub.app/joins/chinese/S1N26) | +| [专栏·观点](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N11) | [S1N11](https://rsshub.app/joins/chinese/S1N11) | +| [军事·科技](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N6) | [S1N6](https://rsshub.app/joins/chinese/S1N6) | +| [娱乐体育](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N7) | [S1N7](https://rsshub.app/joins/chinese/S1N7) | +| [教育](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N8) | [S1N8](https://rsshub.app/joins/chinese/S1N8) | +| [旅游美食](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N9) | [S1N9](https://rsshub.app/joins/chinese/S1N9) | +| [时尚](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N10) | [S1N10](https://rsshub.app/joins/chinese/S1N10) | +| [图集](https://chinese.joins.com/news/articleList.html?sc_section_code=S1N12&view_type=tm) | [S1N12](https://rsshub.app/joins/chinese/S1N12) | + + `, + categories: ['traditional-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['chinese.joins.com/news/articleList.html'], + target: (url) => { + const category = url.searchParams.get('sc_section_code'); + + return `/joins/chinese${category ? `/${category}` : ''}`; + }, + }, + { + title: '财经', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N1', + }, + { + title: '国际', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N2', + }, + { + title: '北韩', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N3', + }, + { + title: '政治·社会', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N4', + }, + { + title: '中国观察', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N5', + }, + { + title: '社论', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N26', + }, + { + title: '专栏·观点', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N11', + }, + { + title: '军事·科技', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N6', + }, + { + title: '娱乐体育', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N7', + }, + { + title: '教育', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N8', + }, + { + title: '旅游美食', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N9', + }, + { + title: '时尚', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N10', + }, + { + title: '图集', + source: ['chinese.joins.com/news/articleList.html'], + target: '/chinese/S1N12', + }, + ], +}; diff --git a/lib/routes/joins/namespace.ts b/lib/routes/joins/namespace.ts new file mode 100644 index 00000000000000..1e2aeec80d7fc7 --- /dev/null +++ b/lib/routes/joins/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中央日报', + url: 'joins.com', + categories: ['traditional-media'], + description: '', + lang: 'ko', +}; diff --git a/lib/routes/joins/templates/description.art b/lib/routes/joins/templates/description.art new file mode 100644 index 00000000000000..e8cc00cbc2ccda --- /dev/null +++ b/lib/routes/joins/templates/description.art @@ -0,0 +1,17 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} + <figure> + <img + {{ if image.alt }} + alt="{{ image.alt }}" + {{ /if }} + src="{{ image.src }}"> + </figure> + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} diff --git a/lib/routes/joneslanglasalle/index.ts b/lib/routes/joneslanglasalle/index.ts new file mode 100644 index 00000000000000..8c73a3eed8ba29 --- /dev/null +++ b/lib/routes/joneslanglasalle/index.ts @@ -0,0 +1,311 @@ +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); + +const cleanHtml = (html: string, preservedTags: string[]): string => { + const $ = load(html); + + $('div.informationbox').remove(); + $('div.contributors').remove(); + + $('*') + .not(preservedTags.join(', ')) + .contents() + .filter((_, el) => el.type === 'text') + .remove(); + + $('*') + .not(preservedTags.join(', ')) + .filter((_, el) => $(el).children().length === 0) + .remove(); + + return $.html() || ''; +}; + +export const handler = async (ctx: Context): Promise<Data> => { + const { language: lang = 'zh', category = 'trends-and-insights' } = ctx.req.param(); + // default limit is 12 + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '12', 10); + + const rootUrl: string = 'https://www.joneslanglasalle.com.cn'; + const targetUrl: string = new URL(`${lang}/${category}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'en'; + + let items: DataItem[] = $('div.ti-title') + .slice(0, limit) + .toArray() + .map((item): DataItem => { + const $item: Cheerio<Element> = $(item); + const aEl = $item.closest('a'); + + const title: string = $item.text(); + const link: string | undefined = aEl.prop('href'); + + const description: string = art(path.join(__dirname, 'templates/description.art'), { + intro: aEl.find('p.ti-teaser').text(), + }); + + const image: string | undefined = aEl.find('div.ti-image-container img').prop('src') ? new URL(aEl.find('div.ti-image-container img').prop('src') as string, rootUrl).href : undefined; + + return { + title, + description, + pubDate: parseDate(aEl.find('span.ti-date').text(), ['MM月DD日', 'MMMM DD']), + link: link ? new URL(link, rootUrl).href : undefined, + category: [aEl.find('span.ti-type').text()].filter(Boolean), + content: { + html: description, + text: aEl.find('p.ti-teaser').text(), + }, + 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<DataItem> => { + try { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('meta[property="og:title"]').prop('content'); + const guid: string = $$('meta[property="og:url"]').prop('content'); + const image: string | undefined = $$('meta[property="og:image"]').prop('content'); + + const pubDate: Date = parseDate($$('div.publicationdate').text().trim(), ['YYYY 年MM 月DD 日', 'MMMM DD, YYYY']); + + const author: DataItem['author'] = $$('div.contributors ul li') + .toArray() + .map((el) => ({ + name: $$(el).text(), + })); + + const media: Record<string, Record<string, string>> = {}; + + $$('picture').each((_, el) => { + const $$el = $$(el); + + const src = $$el.find('source').last().prop('srcset') ? new URL($$el.find('source').last().prop('srcset') as string, rootUrl).href : undefined; + + if (src) { + $$el.replaceWith( + art(path.join(__dirname, 'templates/description.art'), { + images: [ + { + src, + }, + ], + }) + ); + + const mediaType: string | undefined = src.split(/\./).pop(); + + if (mediaType) { + media[mediaType] = { url: src }; + } + } + }); + + const extraLinks = $$('div.related-content a.content-card') + .toArray() + .map((el) => { + const $$el: Cheerio<Element> = $$(el); + + return { + url: new URL($$el.prop('href') as string, rootUrl).href, + type: 'related', + content_html: $$el.find('div.content-card__body').html(), + }; + }) + .filter((link): link is { url: string; type: string; content_html: string } => true); + + const description: string = art(path.join(__dirname, 'templates/description.art'), { + description: cleanHtml($$('div.page-section').eq(1).html() ?? $$('div.copy-block').html() ?? '', ['div.richtext p', 'h3', 'h4', 'h5', 'h6', 'figure', 'img', 'ul', 'li', 'span', 'b']), + }); + + return { + title, + description, + pubDate, + category: $$('meta[property="article:tag"]').prop('content').split(/,\s/), + author, + guid, + id: guid, + content: { + html: description, + text: description, + }, + image, + banner: image, + language, + media: Object.keys(media).length > 0 ? media : undefined, + _extra: { + links: extraLinks.length > 0 ? extraLinks : undefined, + }, + }; + } catch { + return item; + } + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const title = $('title').text(); + const feedImage = $('img.logo').prop('src') ? new URL($('img.logo').prop('src') as string, rootUrl).href : undefined; + + return { + title, + description: $('meta[property="og:description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author: title.split(/\|/).pop(), + language, + id: $('meta[property="og:url"]').prop('content'), + }; +}; + +export const route: Route = { + path: '/:language?/:category{.+}?', + name: 'Trends & Insights', + url: 'joneslanglasalle.com.cn', + maintainers: ['nczitzk', 'pseudoyu'], + handler, + example: '/joneslanglasalle/en/trends-and-insights', + parameters: { + language: 'Language, `zh` by default', + category: 'Category, `trends-and-insights` by default', + }, + description: `::: tip +If you subscribe to [Trends & Insights](https://www.joneslanglasalle.com.cn/en/trends-and-insights),where the URL is \`https://www.joneslanglasalle.com.cn/en/trends-and-insights\`, extract the part \`https://joneslanglasalle.com.cn/\` to the end. Use \`zh\` and \`trends-and-insights\` as the parameters to fill in. Therefore, the route will be [\`/joneslanglasalle/en/trends-and-insights\`](https://rsshub.app/joneslanglasalle/en/trends-and-insights). +::: + +| Category | ID | +| --------- | ----------------------------- | +| Latest | trends-and-insights | +| Workplace | trends-and-insights/workplace | +| Investor | trends-and-insights/investor | +| Cities | trends-and-insights/cities | +| Research | trends-and-insights/research | +`, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['joneslanglasalle.com.cn/:language/:category'], + target: (params) => { + const language = params.language; + const category = params.category; + + return language ? `/${language}${category ? `/${category}` : ''}` : ''; + }, + }, + { + title: 'Latest', + source: ['joneslanglasalle.com.cn/en/trends-and-insights'], + target: '/en/trends-and-insights', + }, + { + title: 'Workplace', + source: ['joneslanglasalle.com.cn/en/trends-and-insights/workplace'], + target: '/en/trends-and-insights/workplace', + }, + { + title: 'Investor', + source: ['joneslanglasalle.com.cn/en/trends-and-insights/investor'], + target: '/en/trends-and-insights/investor', + }, + { + title: 'Cities', + source: ['joneslanglasalle.com.cn/en/trends-and-insights/cities'], + target: '/en/trends-and-insights/cities', + }, + { + title: 'Research', + source: ['joneslanglasalle.com.cn/en/trends-and-insights/research'], + target: '/en/trends-and-insights/research', + }, + { + title: '房地产趋势与洞察', + source: ['joneslanglasalle.com.cn/zh/trends-and-insights'], + target: '/zh/trends-and-insights', + }, + { + title: '办公空间', + source: ['joneslanglasalle.com.cn/zh/trends-and-insights/workplace'], + target: '/zh/trends-and-insights/workplace', + }, + { + title: '投资者', + source: ['joneslanglasalle.com.cn/zh/trends-and-insights/investor'], + target: '/zh/trends-and-insights/investor', + }, + { + title: '城市', + source: ['joneslanglasalle.com.cn/zh/trends-and-insights/cities'], + target: '/zh/trends-and-insights/cities', + }, + { + title: '研究报告', + source: ['joneslanglasalle.com.cn/zh/trends-and-insights/research'], + target: '/zh/trends-and-insights/research', + }, + ], + view: ViewType.Articles, + + zh: { + path: '/:language?/:category{.+}?', + name: '房地产趋势与洞察', + url: 'joneslanglasalle.com.cn', + maintainers: ['nczitzk'], + handler, + example: '/joneslanglasalle/zh/trends-and-insights', + parameters: { + language: '语言,默认为 `zh`,可在对应分类页 URL 中找到', + category: '分类,默认为 `trends-and-insights`,可在对应分类页 URL 中找到', + }, + description: `::: tip +若订阅 [房地产趋势与洞察](https://www.joneslanglasalle.com.cn/zh/trends-and-insights),网址为 \`https://www.joneslanglasalle.com.cn/zh/trends-and-insights\`,请截取 \`https://joneslanglasalle.com.cn/\` 到末尾的部分 \`zh\` 和 \`trends-and-insights\` 作为 \`language\` 和 \`category\` 参数填入,此时目标路由为 [\`/joneslanglasalle/zh/trends-and-insights\`](https://rsshub.app/joneslanglasalle/zh/trends-and-insights)。 +::: + +| 分类名称 | 分类 ID | +| ---------- | ----------------------------- | +| 趋势及洞察 | trends-and-insights | +| 办公空间 | trends-and-insights/workplace | +| 投资者 | trends-and-insights/investor | +| 城市 | trends-and-insights/cities | +| 研究报告 | trends-and-insights/research | +`, + }, +}; diff --git a/lib/routes/joneslanglasalle/namespace.ts b/lib/routes/joneslanglasalle/namespace.ts new file mode 100644 index 00000000000000..2167344f3b2f3a --- /dev/null +++ b/lib/routes/joneslanglasalle/namespace.ts @@ -0,0 +1,13 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Jones Lang LaSalle', + url: 'joneslanglasalle.com.cn', + categories: ['new-media'], + description: 'JLL is a global real estate services firm in commercial property and investment management, providing services for real estate owners, occupiers and investors.', + lang: 'zh-CN', + zh: { + name: '仲量联行JLL', + description: '仲量联行JLL是全球领先的房地产专业服务和投资管理公司,为企业、房地产业主、投资者及政府提供各类资产的施工、租赁、管理、投资咨询服务。仲量联行也致力于高质量城市发展、打造理想空间、提供可持续的房地产解决方案。', + }, +}; diff --git a/lib/routes/joneslanglasalle/templates/description.art b/lib/routes/joneslanglasalle/templates/description.art new file mode 100644 index 00000000000000..aced21ab986b46 --- /dev/null +++ b/lib/routes/joneslanglasalle/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if !videos?.[0]?.src && image?.src }} + <figure> + <img + {{ if image.alt }} + alt="{{ image.alt }}" + {{ /if }} + src="{{ image.src }}"> + </figure> + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} + <blockquote>{{ intro }}</blockquote> +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/jornada/index.ts b/lib/routes/jornada/index.ts index 2eb382664692e7..f833b25faea84f 100644 --- a/lib/routes/jornada/index.ts +++ b/lib/routes/jornada/index.ts @@ -42,19 +42,19 @@ export const route: Route = { handler, description: `Provides a way to get an specific rss feed by date and category over the official one. - | Category | \`:category\` | - | -------------------- | ----------- | - | Capital | capital | - | Cartones | cartones | - | Ciencia y Tecnología | ciencia | - | Cultura | cultura | - | Deportes | deportes | - | Economía | economia | - | Estados | estados | - | Mundo | mundo | - | Opinión | opinion | - | Política | politica | - | Sociedad | sociedad |`, +| Category | \`:category\` | +| -------------------- | ----------- | +| Capital | capital | +| Cartones | cartones | +| Ciencia y Tecnología | ciencia | +| Cultura | cultura | +| Deportes | deportes | +| Economía | economia | +| Estados | estados | +| Mundo | mundo | +| Opinión | opinion | +| Política | politica | +| Sociedad | sociedad |`, }; async function handler(ctx) { diff --git a/lib/routes/jornada/namespace.ts b/lib/routes/jornada/namespace.ts index b26b021d54c4c2..faba71ee0d5ab5 100644 --- a/lib/routes/jornada/namespace.ts +++ b/lib/routes/jornada/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'La Jornada', url: 'jornada.com.mx', + lang: 'es', }; diff --git a/lib/routes/joshwcomeau/latest.ts b/lib/routes/joshwcomeau/latest.ts new file mode 100644 index 00000000000000..2483403361b989 --- /dev/null +++ b/lib/routes/joshwcomeau/latest.ts @@ -0,0 +1,59 @@ +import { Data, Route } from '@/types'; +import { getRelativeUrlList, processList, rootUrl } from './utils'; + +export const route: Route = { + path: '/latest/:category?', + categories: ['programming'], + example: '/joshwcomeau/latest/css', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + parameters: { + category: { + description: 'Category', + options: [ + { value: 'css', label: 'CSS' }, + { value: 'react', label: 'React' }, + { value: 'animation', label: 'Animation' }, + { value: 'javascript', label: 'JavaScript' }, + { value: 'career', label: 'Career' }, + { value: 'blog', label: 'Blog' }, + ], + }, + }, + radar: [ + { + source: ['joshwcomeau.com/'], + target: '/latest', + }, + { + source: ['joshwcomeau.com/:category'], + target: '/latest/:category', + }, + ], + name: 'Articles and Tutorials', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler(ctx) { + const category: string = ctx.req.param('category') || ''; + const currentUrl = category ? `${rootUrl}/${category}` : rootUrl; + const selector = category ? 'div > article > a:first-child' : 'article[data-include-enter-animation="false"] > a:first-child'; + const { heading, urls } = await getRelativeUrlList(currentUrl, selector); + const items = await processList(urls); + const title = category ? `${heading} | ` : ''; + return { + title: `${title}Articles and Tutorials | Josh W. Comeau`, + description: `Friendly tutorials for developers. Focus on ${category ? title : 'React, CSS, Animation, and more!'}`, + link: currentUrl, + item: items, + icon: `${rootUrl}/favicon.png`, + logo: `${rootUrl}/favicon.png`, + } as Data; +} diff --git a/lib/routes/joshwcomeau/namespace.ts b/lib/routes/joshwcomeau/namespace.ts new file mode 100644 index 00000000000000..f0125e4c9b2b0a --- /dev/null +++ b/lib/routes/joshwcomeau/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Josh W Comeau', + url: 'www.joshwcomeau.com', + categories: ['programming'], + lang: 'en', +}; diff --git a/lib/routes/joshwcomeau/popular.ts b/lib/routes/joshwcomeau/popular.ts new file mode 100644 index 00000000000000..dd5c352ce0e929 --- /dev/null +++ b/lib/routes/joshwcomeau/popular.ts @@ -0,0 +1,38 @@ +import { Data, Route } from '@/types'; +import { getRelativeUrlList, processList, rootUrl } from './utils'; + +export const route: Route = { + path: '/popular', + categories: ['programming'], + example: '/joshwcomeau/popular', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['joshwcomeau.com/'], + target: '/popular', + }, + ], + name: 'Popular Content', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler() { + const { urls } = await getRelativeUrlList(rootUrl, 'section > ol > li > a'); + const items = await processList(urls); + return { + title: 'Popular Content | Josh W. Comeau', + description: 'Friendly tutorials for developers. Focus on React, CSS, Animation, and more!', + link: rootUrl, + item: items, + icon: `${rootUrl}/favicon.png`, + logo: `${rootUrl}/favicon.png`, + } as Data; +} diff --git a/lib/routes/joshwcomeau/utils.ts b/lib/routes/joshwcomeau/utils.ts new file mode 100644 index 00000000000000..90a71a73edec75 --- /dev/null +++ b/lib/routes/joshwcomeau/utils.ts @@ -0,0 +1,61 @@ +import { DataItem } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +export const rootUrl = 'https://www.joshwcomeau.com'; + +export async function getRelativeUrlList(url, selector) { + const response = await ofetch(url); + const $ = load(response); + const heading = $('header>h1').text(); + const urls = $(selector) + .toArray() + .map((element) => { + const itemRelativeUrl = $(element).attr('href'); + const cardTitle = $(element).find('span').text(); + return { url: itemRelativeUrl as string, cardTitle }; + }); + return { heading, urls }; +} + +export async function processList(list) { + const listPromise = await Promise.allSettled(list.map(async (item) => await cache.tryGet(`joshwcomeau:${item.url}`, async () => await getPostContent(item)))); + return listPromise.map((item, index) => (item.status === 'fulfilled' ? item.value : ({ title: 'Error Reading Item', link: `${rootUrl}${list[index]?.url}` } as DataItem))); +} + +export async function getPostContent({ url, cardTitle }) { + if (url.startsWith('https')) { + return { + title: cardTitle ?? 'External Content', + description: 'Read it on external Site', + link: url, + } as DataItem; + } + const response = await ofetch(`${rootUrl}${url}`); + const $ = load(response); + const title = $('meta[property="og:title"]').attr('content')?.replace('• Josh W. Comeau', ''); + const summary = $('meta[property="og:description"]').attr('content'); + const author = $('meta[name="author"]').attr('content'); + const dateDiv = $('div[data-parent-layout]'); + const tag = dateDiv.find('dl:first-child > dd > a').text(); + const pubDate = dateDiv.find('dl:first-child > dd:has(span):not(:last-child)').text(); + const updateDate = dateDiv.find('dl:last-child > dd:has(span):not(:last-child)').text(); + const description = $('main > article').html(); + return { + title, + description, + author, + pubDate: processDate(pubDate), + updated: processDate(updateDate), + link: `${rootUrl}${url}`, + content: { html: description, text: summary }, + category: [tag], + } as DataItem; +} + +function processDate(date: string) { + const dateWithSlash = date.trim().replaceAll(' ', '/').replace(',', ''); + return parseDate(dateWithSlash, 'MMMM/Do/YYYY', 'en'); +} diff --git a/lib/routes/jou/namespace.ts b/lib/routes/jou/namespace.ts index f08df3e23915c0..d65f8530805af4 100644 --- a/lib/routes/jou/namespace.ts +++ b/lib/routes/jou/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '江苏海洋大学', url: 'www.jou.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/jpxgmn/namespace.ts b/lib/routes/jpxgmn/namespace.ts index a0b9011ec702c4..ef4d3942b4b5bf 100644 --- a/lib/routes/jpxgmn/namespace.ts +++ b/lib/routes/jpxgmn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '极品性感美女', url: 'www.jpxgmn.com', + lang: 'zh-CN', }; diff --git a/lib/routes/jrj/index.ts b/lib/routes/jrj/index.ts new file mode 100644 index 00000000000000..70d71c1abe55ea --- /dev/null +++ b/lib/routes/jrj/index.ts @@ -0,0 +1,127 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { ofetch } from 'ofetch'; + +const options = { + '103': '财经资讯', + '508': '科技资讯', + '106': '商业资讯', + '632': '消费资讯', + '630': '医疗资讯', + '119': '康养资讯', + '004': '汽车资讯', + '009': '房产资讯', + '629': 'ESG 资讯', + '010': 'A股资讯', + '001': '港股资讯', + '102': '美股资讯', + '113': '银行资讯', + '115': '保险资讯', + '104': '基金资讯', + '503': '私募资讯', + '112': '信托资讯', + '007': '外汇资讯', + '107': '期货资讯', + '118': '债券资讯', + '603': '券商资讯', + '105': '观点', +}; + +export const route: Route = { + path: '/:channelNum', + categories: ['finance'], + example: '/jrj/103', + parameters: { + channelNum: { + description: '栏目编号', + options: Object.entries(options).map(([value, label]) => ({ value, label })), + }, + }, + url: 'www.jrj.com.cn', + name: '资讯', + description: ` +| column | Description | +| --- | --- | +| 103 | 财经资讯 | +| 508 | 科技资讯 | +| 106 | 商业资讯 | +| 632 | 消费资讯 | +| 630 | 医疗资讯 | +| 119 | 康养资讯 | +| 004 | 汽车资讯 | +| 009 | 房产资讯 | +| 629 | ESG 资讯 | +| 001 | 港股资讯 | +| 102 | 美股资讯 | +| 113 | 银行资讯 | +| 115 | 保险资讯 | +| 104 | 基金资讯 | +| 503 | 私募资讯 | +| 112 | 信托资讯 | +| 007 | 外汇资讯 | +| 107 | 期货资讯 | +| 118 | 债券资讯 | +| 603 | 券商资讯 | +| 105 | 观点 | + `, + maintainers: ['p3psi-boo'], + handler, +}; + +async function handler(ctx) { + const channelNum = ctx.req.param('channelNum'); + + const url = 'https://gateway.jrj.com/jrj-news/news/queryNewsList'; + + const response = await ofetch(url, { + method: 'post', + body: { + sortBy: 1, + pageSize: 20, + makeDate: '', + channelNum, + infoCls: '', + }, + }); + + const alist = response.data.data; + + const list = alist.map((item) => { + const link = item.pcInfoUrl; + const title = item.title; + const author = item.paperMediaSource; + const pubDate = parseDate(item.makeDate); + + return { + title, + link, + author, + pubDate, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const articleUrl = item.link; + const response = await ofetch(articleUrl); + + const $ = load(response); + + const content = $('.article_content').html(); + + item.description = content; + return item; + }) + ) + ); + + return { + title: `${options[channelNum]} - 金融界`, + link: 'https://jrj.com', + item: items, + }; +} diff --git a/lib/routes/jrj/namespace.ts b/lib/routes/jrj/namespace.ts new file mode 100644 index 00000000000000..3961cae5b5fcaa --- /dev/null +++ b/lib/routes/jrj/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '金融界', + url: 'www.jrj.com.cn', + description: '金融界是国内领先的金融信息服务平台,日均触达千万用户,年度访问量超过3亿,受众覆盖中国主流金融机构、上市公司和活跃投资理财群体', + lang: 'zh-CN', +}; diff --git a/lib/routes/jseea/namespace.ts b/lib/routes/jseea/namespace.ts index 0226a3eb4783bb..93129b017a48c6 100644 --- a/lib/routes/jseea/namespace.ts +++ b/lib/routes/jseea/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Unknown', url: 'jseea.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/jsu/jwc.ts b/lib/routes/jsu/jwc.ts index 5ff7142af64a53..2fad40add0ed1e 100644 --- a/lib/routes/jsu/jwc.ts +++ b/lib/routes/jsu/jwc.ts @@ -23,8 +23,8 @@ export const route: Route = { maintainers: ['wenjia03'], handler, description: `| 教务通知 | 教务动态 | - | -------- | -------- | - | jwtz | jwdt |`, +| -------- | -------- | +| jwtz | jwdt |`, }; async function handler(ctx) { diff --git a/lib/routes/jsu/namespace.ts b/lib/routes/jsu/namespace.ts index 4a8c8ebc967f80..b707e5ff1e5ad6 100644 --- a/lib/routes/jsu/namespace.ts +++ b/lib/routes/jsu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '吉首大学', url: 'jsu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/juejin/books.ts b/lib/routes/juejin/books.ts index a1c2caae20fa56..8bd1dccbe5ea88 100644 --- a/lib/routes/juejin/books.ts +++ b/lib/routes/juejin/books.ts @@ -1,5 +1,5 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { @@ -28,14 +28,12 @@ export const route: Route = { }; async function handler() { - const response = await got({ - method: 'post', - url: 'https://api.juejin.cn/booklet_api/v1/booklet/listbycategory', - json: { category_id: '0', cursor: '0', limit: 20 }, + const response = await ofetch('https://api.juejin.cn/booklet_api/v1/booklet/listbycategory', { + method: 'POST', + body: { category_id: '0', cursor: '0', limit: 20 }, }); - const { data } = response.data; - const items = data.map(({ base_info }) => ({ + const items = response.data.map(({ base_info }) => ({ title: base_info.title, link: `https://juejin.cn/book/${base_info.booklet_id}`, description: ` diff --git a/lib/routes/juejin/category.ts b/lib/routes/juejin/category.ts index bf54bbcccb371a..15252a5d74d502 100644 --- a/lib/routes/juejin/category.ts +++ b/lib/routes/juejin/category.ts @@ -1,7 +1,6 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import util from './utils'; +import ofetch from '@/utils/ofetch'; +import { getCategoryBrief, parseList, ProcessFeed } from './utils'; export const route: Route = { path: '/category/:category', @@ -16,29 +15,33 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, + radar: [ + { + source: ['juejin.cn/:category'], + }, + ], name: '分类', maintainers: ['DIYgod'], handler, description: `| 后端 | 前端 | Android | iOS | 人工智能 | 开发工具 | 代码人生 | 阅读 | - | ------- | -------- | ------- | --- | -------- | -------- | -------- | ------- | - | backend | frontend | android | ios | ai | freebie | career | article |`, +| ------- | -------- | ------- | --- | -------- | -------- | -------- | ------- | +| backend | frontend | android | ios | ai | freebie | career | article |`, }; async function handler(ctx) { const category = ctx.req.param('category'); - const idResponse = await got({ - method: 'get', - url: 'https://api.juejin.cn/tag_api/v1/query_category_briefs?show_type=0', - }); + const idResponse = await getCategoryBrief(); - const cat = idResponse.data.data.find((item) => item.category_url === category); + const cat = idResponse.find((item) => item.category_url === category); + if (!cat) { + throw new Error('分类不存在'); + } const id = cat.category_id; - const response = await got({ - method: 'post', - url: 'https://api.juejin.cn/recommend_api/v1/article/recommend_cate_feed', - json: { + const response = await ofetch('https://api.juejin.cn/recommend_api/v1/article/recommend_cate_feed', { + method: 'POST', + body: { id_type: 2, sort_type: 300, cate_id: id, @@ -47,11 +50,8 @@ async function handler(ctx) { }, }); - let originalData = []; - if (response.data.data) { - originalData = response.data.data; - } - const resultItems = await util.ProcessFeed(originalData, cache); + const list = parseList(response.data); + const resultItems = await ProcessFeed(list); return { title: `掘金 ${cat.category_name}`, diff --git a/lib/routes/juejin/collection.ts b/lib/routes/juejin/collection.ts index a571afe0c9bf10..bfac8037724470 100644 --- a/lib/routes/juejin/collection.ts +++ b/lib/routes/juejin/collection.ts @@ -1,7 +1,5 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import util from './utils'; +import { getCollection, parseList, ProcessFeed } from './utils'; export const route: Route = { path: '/collection/:collectionId', @@ -22,27 +20,21 @@ export const route: Route = { }, ], name: '单个收藏夹', - maintainers: ['isQ'], + maintainers: ['yang131323'], handler, }; async function handler(ctx) { const collectionId = ctx.req.param('collectionId'); - const collectPage = await got({ - method: 'get', - url: `https://api.juejin.cn/interact_api/v1/collectionSet/get?tag_id=${collectionId}&cursor=0`, - }); + const collectPage = await getCollection(collectionId); - let items = []; - if (collectPage.data.data && collectPage.data.data.article_list) { - items = collectPage.data.data.article_list.slice(0, 10); - } + const items = parseList(collectPage.article_list); - const result = await util.ProcessFeed(items, cache); + const result = await ProcessFeed(items); return { - title: '掘金 - 单个收藏夹', + title: `${collectPage.detail.tag_name} - ${collectPage.create_user.user_name}的收藏集 - 掘金`, link: `https://juejin.cn/collection/${collectionId}`, description: '掘金,用户单个收藏夹', item: result, diff --git a/lib/routes/juejin/favorites.ts b/lib/routes/juejin/collections.ts similarity index 50% rename from lib/routes/juejin/favorites.ts rename to lib/routes/juejin/collections.ts index 4dc1a6027864a0..b28f1c2e08f574 100644 --- a/lib/routes/juejin/favorites.ts +++ b/lib/routes/juejin/collections.ts @@ -1,7 +1,7 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import util from './utils'; +import ofetch from '@/utils/ofetch'; +import { getCollection, parseList, ProcessFeed } from './utils'; +import { Article } from './types'; export const route: Route = { path: '/collections/:userId', @@ -23,37 +23,29 @@ export const route: Route = { }, ], name: '收藏集', - maintainers: ['isQ'], + maintainers: ['yang131323'], handler, }; +// 获取所有收藏夹文章内容 +async function getArticleList(collectionId) { + const collectPage = await getCollection(collectionId); + + return collectPage.article_list; +} + async function handler(ctx) { const userId = ctx.req.param('userId'); - const response = await got({ - method: 'get', - url: `https://api.juejin.cn/interact_api/v1/collectionSet/list?user_id=${userId}&cursor=0&limit=20`, - }); + const response = await ofetch(`https://api.juejin.cn/interact_api/v1/collectionSet/list?user_id=${userId}&cursor=0&limit=20`); // 获取用户所有收藏夹id - const collectionId = response.data.data.map((item) => item.tag_id); - - // 获取所有收藏夹文章内容 - async function getPostId(item) { - const collectPage = await got({ - method: 'get', - url: `https://api.juejin.cn/interact_api/v1/collectionSet/get?tag_id=${item}&cursor=0`, - }); - - return (Array.isArray(collectPage.data.data.article_list) && collectPage.data.data.article_list.slice(0, 10)) || []; - } + const collectionId = response.data.map((item) => item.tag_id); - const temp = await Promise.all(collectionId.map((element) => getPostId(element))); - const posts = []; - for (const item of temp) { - posts.push(...item); - } + const temp = (await Promise.all(collectionId.map((id) => getArticleList(id)))) as Article[][]; + const posts = temp.flat().filter(Boolean); + const list = parseList(posts); - const result = await util.ProcessFeed(posts, cache); + const result = await ProcessFeed(list); return { title: '掘金 - 收藏集', diff --git a/lib/routes/juejin/column.ts b/lib/routes/juejin/column.ts index fad5bb71feaf27..2f8a89494ca739 100644 --- a/lib/routes/juejin/column.ts +++ b/lib/routes/juejin/column.ts @@ -1,7 +1,6 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import util from './utils'; +import ofetch from '@/utils/ofetch'; +import { parseList, ProcessFeed } from './utils'; export const route: Route = { path: '/column/:id', @@ -28,29 +27,25 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const detail = await got({ - method: 'get', - url: `https://api.juejin.cn/content_api/v1/column/detail?column_id=${id}`, - }); - const response = await got({ - method: 'post', - url: 'https://api.juejin.cn/content_api/v1/column/articles_cursor', - json: { + const columnDetail = await ofetch(`https://api.juejin.cn/content_api/v1/column/detail?column_id=${id}`); + const response = await ofetch('https://api.juejin.cn/content_api/v1/column/articles_cursor', { + method: 'POST', + body: { column_id: id, - limit: 20, cursor: '0', + limit: 20, sort: 0, }, }); - const { data } = response.data; - const detailData = detail.data.data; - const columnName = detailData && detailData.column_version && detailData.column_version.title; - const resultItems = await util.ProcessFeed(data, cache); + const detailData = columnDetail.data; + const list = parseList(response.data); + const resultItems = await ProcessFeed(list); return { - title: `掘金专栏-${columnName}`, + title: `${detailData.column_version.title} - ${detailData.author.user_name}的专栏 - 掘金`, link: `https://juejin.cn/column/${id}`, - description: `掘金专栏-${columnName}`, + description: detailData.column_version.description, + image: columnDetail.data.column_version.cover, item: resultItems, }; } diff --git a/lib/routes/juejin/dynamic.ts b/lib/routes/juejin/dynamic.ts new file mode 100644 index 00000000000000..d37bebcc8b4d1b --- /dev/null +++ b/lib/routes/juejin/dynamic.ts @@ -0,0 +1,124 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/dynamic/:id', + categories: ['programming'], + example: '/juejin/dynamic/3051900006845944', + parameters: { id: '用户 id, 可在用户页 URL 中找到' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['juejin.cn/user/:id'], + }, + ], + name: '用户动态', + maintainers: ['CaoMeiYouRen'], + handler, +}; + +async function handler(ctx) { + const id = ctx.req.param('id'); + + const response = await ofetch('https://api.juejin.cn/user_api/v1/user/dynamic', { + query: { + user_id: id, + cursor: 0, + }, + }); + const list = response.data.list; + + const user = list[0].user; + const username = user.user_name; + + const items = list.map((e) => { + const { target_type, target_data, action, time } = e; // action: 0.发布文章;1.点赞文章;2.发布沸点;3.点赞沸点;4.关注用户;5.关注标签 + let title: string | undefined; + let description: string | undefined; + let pubDate: Date | undefined; + let author: string | undefined; + let link: string | undefined; + let category: string[] | undefined; + switch (target_type) { + case 'short_msg': { + // 沸点/点赞等 + const { msg_Info, author_user_info, msg_id, topic } = target_data; + const { content, pic_list, ctime } = msg_Info; + title = content; + const imgs = pic_list.map((img) => `<img src="${img}"><br>`).join(''); + description = `${content.replaceAll('\n', '<br>')}<br>${imgs}`; + pubDate = parseDate(Number(ctime) * 1000); + author = author_user_info.user_name; + link = `https://juejin.cn/pin/${msg_id}`; + category = topic.title; + if (action === 3) { + title = `${username} 赞了这篇沸点//@${author}:${title}`; + description = `${username} 赞了这篇沸点//@${author}:${description}`; + } + break; + } + case 'article': { + // 文章 + const { article_id, article_info, author_user_info, tags } = target_data; + const { ctime, brief_content } = article_info; + title = article_info.title; + description = brief_content; + pubDate = parseDate(Number(ctime) * 1000); + author = author_user_info.user_name; + link = `https://juejin.cn/post/${article_id}`; + category = [...new Set([target_data.category.category_name, ...tags.map((t) => t.tag_name)])]; + if (action === 1) { + title = `${username} 赞了这篇文章//@${author}:${title}`; + } + break; + } + case 'user': { + // 关注用户 + const { user_name, user_id } = target_data; + title = `${username} 关注了 ${user_name}`; + description = `${user_name}<br>简介:${target_data.description}`; + author = user_name; + link = `https://juejin.cn/user/${user_id}`; + pubDate = parseDate(time * 1000); + break; + } + case 'tag': { + // 关注标签 + const { tag_name } = target_data; + title = `${username} 关注了标签 ${tag_name}`; + description = tag_name; + category = [tag_name]; + link = `https://juejin.cn/tag/${encodeURIComponent(tag_name)}`; + pubDate = parseDate(time * 1000); + break; + } + default: + break; + } + return { + title, + description, + pubDate, + author, + link, + category, + guid: link, + }; + }); + return { + title: `掘金用户动态-${username}`, + link: `https://juejin.cn/user/${id}/`, + description: user.description || `掘金用户动态-${username}`, + image: user.avatar_large, + item: items, + author: username, + }; +} diff --git a/lib/routes/juejin/namespace.ts b/lib/routes/juejin/namespace.ts index 35eac45d8f9715..a24e9c68ae74ea 100644 --- a/lib/routes/juejin/namespace.ts +++ b/lib/routes/juejin/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '掘金', url: 'juejin.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/juejin/pins.ts b/lib/routes/juejin/pins.ts index f6267c7d096587..2f19d2bc7b94b5 100644 --- a/lib/routes/juejin/pins.ts +++ b/lib/routes/juejin/pins.ts @@ -1,5 +1,5 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { @@ -19,8 +19,8 @@ export const route: Route = { maintainers: ['xyqfer', 'laampui'], handler, description: `| 推荐 | 热门 | 上班摸鱼 | 内推招聘 | 一图胜千言 | 今天学到了 | 每天一道算法题 | 开发工具推荐 | 树洞一下 | - | --------- | ---- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | - | recommend | hot | 6824710203301167112 | 6819970850532360206 | 6824710202487472141 | 6824710202562969614 | 6824710202378436621 | 6824710202000932877 | 6824710203112423437 |`, +| --------- | ---- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | +| recommend | hot | 6824710203301167112 | 6819970850532360206 | 6824710202487472141 | 6824710202562969614 | 6824710202378436621 | 6824710202000932877 | 6824710203112423437 |`, }; async function handler(ctx) { @@ -38,7 +38,7 @@ async function handler(ctx) { }; let url = ''; - let json = null; + let json = {}; if (/^\d+$/.test(type)) { url = `https://api.juejin.cn/recommend_api/v1/short_msg/topic`; json = { id_type: 4, sort_type: 500, cursor: '0', limit: 20, topic_id: type }; @@ -47,10 +47,9 @@ async function handler(ctx) { json = { id_type: 4, sort_type: 200, cursor: '0', limit: 20 }; } - const response = await got({ - method: 'post', - url, - json, + const response = await ofetch(url, { + method: 'POST', + body: json, }); const items = response.data.data.map((item) => { diff --git a/lib/routes/juejin/posts.ts b/lib/routes/juejin/posts.ts index 58ceea04d9dcee..a35a4031d4d896 100644 --- a/lib/routes/juejin/posts.ts +++ b/lib/routes/juejin/posts.ts @@ -1,7 +1,7 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import util from './utils'; +import ofetch from '@/utils/ofetch'; +import { parseList, ProcessFeed } from './utils'; +import { Article, AuthorUserInfo } from './types'; export const route: Route = { path: '/posts/:id', @@ -26,25 +26,32 @@ export const route: Route = { handler, }; +const getUserInfo = (data: AuthorUserInfo) => ({ + username: data.user_name, + description: data.description, + avatar: data.avatar_large, +}); + async function handler(ctx) { const id = ctx.req.param('id'); - const response = await got({ - method: 'post', - url: 'https://api.juejin.cn/content_api/v1/article/query_list', - json: { + const response = await ofetch('https://api.juejin.cn/content_api/v1/article/query_list', { + method: 'POST', + body: { user_id: id, sort_type: 2, }, }); - const { data } = response.data; - const username = data[0] && data[0].author_user_info && data[0].author_user_info.user_name; - const resultItems = await util.ProcessFeed(data, cache); + const data = response.data as Article[]; + const list = parseList(data); + const authorInfo = getUserInfo(data[0].author_user_info); + const resultItems = await ProcessFeed(list); return { - title: `掘金专栏-${username}`, + title: `掘金专栏-${authorInfo.username}`, link: `https://juejin.cn/user/${id}/posts`, - description: `掘金专栏-${username}`, + description: authorInfo.description, + image: authorInfo.avatar, item: resultItems, }; } diff --git a/lib/routes/juejin/tag.ts b/lib/routes/juejin/tag.ts index 934e70b80d6a32..072ce3cb7871a7 100644 --- a/lib/routes/juejin/tag.ts +++ b/lib/routes/juejin/tag.ts @@ -1,7 +1,6 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import util from './utils'; +import ofetch from '@/utils/ofetch'; +import { getTag, parseList, ProcessFeed } from './utils'; export const route: Route = { path: '/tag/:tag', @@ -29,37 +28,28 @@ export const route: Route = { async function handler(ctx) { const tag = ctx.req.param('tag'); - const idResponse = await got({ - method: 'post', - url: 'https://api.juejin.cn/tag_api/v1/query_tag_detail', - json: { - key_word: tag, - }, - }); + const idResponse = await getTag(tag); - const id = idResponse.data.data.tag_id; + const id = idResponse.tag_id; - const response = await got({ - method: 'post', - url: 'https://api.juejin.cn/recommend_api/v1/article/recommend_tag_feed', - json: { + const response = await ofetch('https://api.juejin.cn/recommend_api/v1/article/recommend_tag_feed', { + method: 'POST', + body: { id_type: 2, cursor: '0', tag_ids: [id], - sort_type: 200, + sort_type: 300, }, }); - let originalData = []; - if (response.data.data) { - originalData = response.data.data.slice(0, 10); - } - const resultItems = await util.ProcessFeed(originalData, cache); + const originalData = parseList(response.data); + const resultItems = await ProcessFeed(originalData); return { title: `掘金 ${tag}`, link: `https://juejin.cn/tag/${encodeURIComponent(tag)}`, description: `掘金 ${tag}`, + image: idResponse.tag.icon, item: resultItems, }; } diff --git a/lib/routes/juejin/trending.ts b/lib/routes/juejin/trending.ts index f06cdbf7fd664f..91f2fb0fe948e8 100644 --- a/lib/routes/juejin/trending.ts +++ b/lib/routes/juejin/trending.ts @@ -1,7 +1,6 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import util from './utils'; +import ofetch from '@/utils/ofetch'; +import { getCategoryBrief, parseList, ProcessFeed } from './utils'; export const route: Route = { path: '/trending/:category/:type', @@ -20,24 +19,24 @@ export const route: Route = { maintainers: ['moaix'], handler, description: `| category | 标签 | - | -------- | -------- | - | android | Android | - | frontend | 前端 | - | ios | iOS | - | backend | 后端 | - | design | 设计 | - | product | 产品 | - | freebie | 工具资源 | - | article | 阅读 | - | ai | 人工智能 | - | devops | 运维 | - | all | 全部 | +| -------- | -------- | +| android | Android | +| frontend | 前端 | +| ios | iOS | +| backend | 后端 | +| design | 设计 | +| product | 产品 | +| freebie | 工具资源 | +| article | 阅读 | +| ai | 人工智能 | +| devops | 运维 | +| all | 全部 | - | type | 类型 | - | ---------- | -------- | - | weekly | 本周最热 | - | monthly | 本月最热 | - | historical | 历史最热 |`, +| type | 类型 | +| ---------- | -------- | +| weekly | 本周最热 | +| monthly | 本月最热 | +| historical | 历史最热 |`, }; async function handler(ctx) { @@ -46,11 +45,8 @@ async function handler(ctx) { let id = ''; let name = ''; let url = 'recommended'; - const idResponse = await got({ - method: 'get', - url: 'https://api.juejin.cn/tag_api/v1/query_category_briefs', - }); - const cat = idResponse.data.data.find((item) => item.category_url === category); + const idResponse = await getCategoryBrief(); + const cat = idResponse.find((item) => item.category_url === category); if (cat) { id = cat.category_id; name = cat.category_name; @@ -97,18 +93,18 @@ async function handler(ctx) { getJson.cate_id = id; } - const trendingResponse = await got({ - method: 'post', - url: getUrl, - json: getJson, + const trendingResponse = await ofetch(getUrl, { + method: 'POST', + body: getJson, }); - let entrylist = trendingResponse.data.data; + let entrylist = trendingResponse.data; if (category === 'all' || category === 'devops' || category === 'product' || category === 'design') { - entrylist = trendingResponse.data.data.filter((item) => item.item_type === 2).map((item) => item.item_info); + entrylist = trendingResponse.data.filter((item) => item.item_type === 2).map((item) => item.item_info); } + const list = parseList(entrylist); - const resultItems = await util.ProcessFeed(entrylist, cache); + const resultItems = await ProcessFeed(list); return { title, diff --git a/lib/routes/juejin/types.ts b/lib/routes/juejin/types.ts new file mode 100644 index 00000000000000..881eb90bee7218 --- /dev/null +++ b/lib/routes/juejin/types.ts @@ -0,0 +1,232 @@ +interface University { + university_id: string; + name: string; + logo: string; +} + +interface Major { + major_id: string; + parent_id: string; + name: string; +} + +interface UserGrowthInfo { + user_id: number; + jpower: number; + jscore: number; + jpower_level: number; + jscore_level: number; + jscore_title: string; + author_achievement_list: number[]; + vip_level: number; + vip_title: string; + jscore_next_level_score: number; + jscore_this_level_mini_score: number; + vip_score: number; +} + +interface UserPrivInfo { + administrator: number; + builder: number; + favorable_author: number; + book_author: number; + forbidden_words: number; + can_tag_cnt: number; + auto_recommend: number; + signed_author: number; + popular_author: number; + can_add_video: number; +} + +interface ArticleInfo { + article_id: string; + user_id: string; + category_id: string; + tag_ids: number[]; + visible_level: number; + link_url: string; + cover_image: string; + is_gfw: number; + title: string; + brief_content: string; + is_english: number; + is_original: number; + user_index: number; + original_type: number; + original_author: string; + content: string; + ctime: string; + mtime: string; + rtime: string; + draft_id: string; + view_count: number; + collect_count: number; + digg_count: number; + comment_count: number; + hot_index: number; + is_hot: number; + rank_index: number; + status: number; + verify_status: number; + audit_status: number; + mark_content: string; + display_count: number; + is_markdown: number; + app_html_content: string; + version: number; + web_html_content: null; + meta_info: null; + catalog: null; + homepage_top_time: number; + homepage_top_status: number; + content_count: number; + read_time: string; +} + +export interface AuthorUserInfo { + user_id: string; + user_name: string; + company: string; + job_title: string; + avatar_large: string; + level: number; + description: string; + followee_count: number; + follower_count: number; + post_article_count: number; + digg_article_count: number; + got_digg_count: number; + got_view_count: number; + post_shortmsg_count: number; + digg_shortmsg_count: number; + isfollowed: boolean; + favorable_author: number; + power: number; + study_point: number; + university: University; + major: Major; + student_status: number; + select_event_count: number; + select_online_course_count: number; + identity: number; + is_select_annual: boolean; + select_annual_rank: number; + annual_list_type: number; + extraMap: Record<string, unknown>; + is_logout: number; + annual_info: any[]; + account_amount: number; + user_growth_info: UserGrowthInfo; + is_vip: boolean; + become_author_days: number; + collection_set_article_count: number; + recommend_article_count_daily: number; + article_collect_count_daily: number; + user_priv_info: UserPrivInfo; +} + +export interface Category { + category_id: string; + category_name: string; + category_url: string; + rank: number; + back_ground: string; + icon: string; + ctime: number; + mtime: number; + show_type: number; + item_type: number; + promote_tag_cap: number; + promote_priority: number; +} + +export interface Tag { + id: number; + tag_id: string; + tag_name: string; + color: string; + icon: string; + back_ground: string; + show_navi: number; + ctime: number; + mtime: number; + id_type: number; + tag_alias: string; + post_article_count: number; + concern_user_count: number; +} + +interface UserInteract { + id: number; + omitempty: number; + user_id: number; + is_digg: boolean; + is_follow: boolean; + is_collect: boolean; + collect_set_count: number; +} + +interface OrgVersion { + version_id: string; + icon: string; + background: string; + name: string; + introduction: string; + weibo_link: string; + github_link: string; + homepage_link: string; + ctime: number; + mtime: number; + org_id: string; + brief_introduction: string; + introduction_preview: string; +} + +interface OrgInfo { + org_type: number; + org_id: string; + online_version_id: number; + latest_version_id: number; + power: number; + ctime: number; + mtime: number; + audit_status: number; + status: number; + org_version: OrgVersion; + follower_count: number; + article_view_count: number; + article_digg_count: number; +} + +interface Org { + is_followed: boolean; + org_info: OrgInfo; +} + +interface Status { + push_status: number; +} + +interface Extra { + extra: string; +} + +export interface Article { + article_id: string; + article_info: ArticleInfo; + author_user_info: AuthorUserInfo; + category: Category; + tags: Tag[]; + user_interact: UserInteract; + org: Org; + req_id: string; + status: Status; + theme_list: any[]; + extra: Extra; +} + +export interface Collection { + article_list: Article[]; + create_user: AuthorUserInfo; + detail: Tag; +} diff --git a/lib/routes/juejin/utils.ts b/lib/routes/juejin/utils.ts index 0c0119743d7fb0..024392c67062dc 100644 --- a/lib/routes/juejin/utils.ts +++ b/lib/routes/juejin/utils.ts @@ -1,55 +1,154 @@ -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 MarkdownIt from 'markdown-it'; -const md = MarkdownIt({ - html: true, -}); +// import MarkdownIt from 'markdown-it'; +// const md = MarkdownIt({ +// html: true, +// }); +import crypto from 'node:crypto'; +import cache from '@/utils/cache'; +import { Category, Collection, Tag } from './types'; + +const b64tou8a = (str) => Uint8Array.from(Buffer.from(str, 'base64')); +const b64tohex = (str) => Buffer.from(str, 'base64').toString('hex'); +const s256 = (s1: Uint8Array, s2: string) => { + const sha = crypto.createHash('sha256'); + sha.update(s1); + sha.update(s2); + return sha.digest('hex'); +}; + // 加载文章页 -async function loadContent(id) { - const response = await got({ - method: 'post', - url: 'https://api.juejin.cn/content_api/v1/article/detail', - json: { - article_id: id, - }, - }); - let description; - if (response.data.data) { - description = md.render(response.data.data.article_info.mark_content) || response.data.data.article_info.content; +// async function loadContent(id) { +// const response = await ofetch('https://api.juejin.cn/content_api/v1/article/detail', { +// method: 'post', +// body: { +// article_id: id, +// }, +// }); +// let description; +// if (response.data) { +// description = md.render(response.data.article_info.mark_content) || response.data.article_info.content; +// } + +// return { description }; +// } + +const solveWafChallenge = (cs) => { + const c = JSON.parse(Buffer.from(cs, 'base64').toString()); + const prefix = b64tou8a(c.v.a); + const expect = b64tohex(c.v.c); + + for (let i = 0; i < 1_000_000; i++) { + const hash = s256(prefix, i.toString()); + if (hash === expect) { + c.d = Buffer.from(i.toString()).toString('base64'); + break; + } } + return Buffer.from(JSON.stringify(c)).toString('base64'); +}; + +export const getArticle = async (link) => { + let response = await ofetch(link); + let $ = cheerio.load(response); + if ($('script').text().includes('_wafchallengeid')) { + const cs = $('script:contains("_wafchallengeid")') + .text() + .match(/cs="(.*?)",c/)?.[1]; + const cookie = solveWafChallenge(cs); + + response = await ofetch(link, { + headers: { + cookie: `_wafchallengeid=${cookie};`, + }, + }); - return { description }; -} + $ = cheerio.load(response); + } -const loadNews = async (link) => { - const response = await got(link); - const $ = load(response.data); - $('h1.title, .main-box .message').remove(); - return { description: $('.main-box .article').html() }; + return $('.article-viewer').html(); }; -const ProcessFeed = (list, caches) => +// const loadNews = async (link) => { +// const response = await ofetch(link); +// const $ = cheerio.load(response); +// $('h1.title, .main-box .message').remove(); +// return { description: $('.main-box .article').html() }; +// }; + +export const parseList = (data) => + data.map((item) => { + const isArticle = !!item.article_info; + + return { + title: isArticle ? item.article_info.title : item.content_info.title, + description: (isArticle ? item.article_info.brief_content : item.content_info.brief) || '无描述', + pubDate: parseDate(isArticle ? item.article_info.ctime : item.content_info.ctime, 'X'), + author: item.author_user_info.user_name, + link: `https://juejin.cn${isArticle ? `/post/${item.article_id}` : `/news/${item.content_id}`}`, + category: [...new Set([item.category.category_name, ...item.tags.map((tag) => tag.tag_name)])], + }; + }); + +export const ProcessFeed = (list) => Promise.all( - list.map(async (item) => { - const isArticle = !!item.article_info; - const pubDate = parseDate((isArticle ? item.article_info.ctime : item.content_info.ctime) * 1000); - const link = `https://juejin.cn${isArticle ? '/post/' + item.article_id : '/news/' + item.content_id}`; - // 列表上提取到的信息 - const single = { - title: isArticle ? item.article_info.title : item.content_info.title, - description: ((isArticle ? item.article_info.brief_content : item.content_info.brief) || '无描述').replaceAll(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ''), - pubDate, - author: item.author_user_info.user_name, - link, - }; - - // 使用tryGet方法从缓存获取内容。 - // 当缓存中无法获取到链接内容的时候,则使用load方法加载文章内容。 - const other = await caches.tryGet(link, () => (isArticle ? loadContent(item.article_id) : loadNews(link))); - // 合并解析后的结果集作为该篇文章最终的输出结果 - return { ...single, ...other }; - }) + list.map((item) => + cache.tryGet(item.link, async () => { + item.description = (await getArticle(item.link)) || item.description; + + return item; + }) + ) ); -export default { ProcessFeed }; +// export const ProcessFeed = (list, caches) => +// Promise.all( +// list.map(async (item) => { +// const isArticle = !!item.article_info; +// const pubDate = parseDate((isArticle ? item.article_info.ctime : item.content_info.ctime) * 1000); +// const link = `https://juejin.cn${isArticle ? '/post/' + item.article_id : '/news/' + item.content_id}`; +// // 列表上提取到的信息 +// const single = { +// title: isArticle ? item.article_info.title : item.content_info.title, +// description: ((isArticle ? item.article_info.brief_content : item.content_info.brief) || '无描述').replaceAll(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ''), +// pubDate, +// author: item.author_user_info.user_name, +// link, +// }; + +// // 使用tryGet方法从缓存获取内容。 +// // 当缓存中无法获取到链接内容的时候,则使用load方法加载文章内容。 +// const other = await caches.tryGet(link, () => (isArticle ? loadContent(item.article_id) : loadNews(link))); +// // 合并解析后的结果集作为该篇文章最终的输出结果 +// return { ...single, ...other }; +// }) +// ); + +export const getCategoryBrief = () => + cache.tryGet('juejin:categoryBriefs', async () => { + const response = await ofetch('https://api.juejin.cn/tag_api/v1/query_category_briefs'); + return response.data; + }) as Promise<Category[]>; + +export const getCollection = (collectionId) => + cache.tryGet(`juejin:collectionId:${collectionId}`, async () => { + const response = await ofetch('https://api.juejin.cn/interact_api/v1/collectionSet/get', { + query: { + tag_id: collectionId, + cursor: 0, + }, + }); + return response.data; + }) as Promise<Collection>; + +export const getTag = (tag) => + cache.tryGet(`juejin:tag:${tag}`, async () => { + const response = await ofetch('https://api.juejin.cn/tag_api/v1/query_tag_detail', { + method: 'POST', + body: { + key_word: tag, + }, + }); + return response.data; + }) as Promise<{ tag_id: string; tag: Tag }>; diff --git a/lib/routes/jump/discount.ts b/lib/routes/jump/discount.ts index 301595cdf5f386..9e372cd4a749e9 100644 --- a/lib/routes/jump/discount.ts +++ b/lib/routes/jump/discount.ts @@ -114,20 +114,20 @@ export const route: Route = { maintainers: ['zytomorrow'], handler, description: `| switch | ps4 | ps5 | xbox | steam | epic | - | ------ | ---- | ---- | ------ | ----- | ------ | - | 可用 | 可用 | 可用 | 不可用 | 可用 | 不可用 | - - | filter | switch | ps4 | ps5 | steam | - | ------ | ------ | --- | --- | ----- | - | all | ✔ | ✔ | ✔ | ✔ | - | jx | ✔ | ✔ | ❌ | ✔ | - | sd | ✔ | ✔ | ✔ | ✔ | - | dl | ❌ | ✔ | ❌ | ✔ | - | vip | ❌ | ❌ | ✔ | ❌ | - - | 北美 | 欧洲(英语) | 法国 | 德国 | 日本 | - | ---- | ------------ | ---- | ---- | ---- | - | na | eu | fr | de | jp |`, +| ------ | ---- | ---- | ------ | ----- | ------ | +| 可用 | 可用 | 可用 | 不可用 | 可用 | 不可用 | + +| filter | switch | ps4 | ps5 | steam | +| ------ | ------ | --- | --- | ----- | +| all | ✔ | ✔ | ✔ | ✔ | +| jx | ✔ | ✔ | ❌ | ✔ | +| sd | ✔ | ✔ | ✔ | ✔ | +| dl | ❌ | ✔ | ❌ | ✔ | +| vip | ❌ | ❌ | ✔ | ❌ | + +| 北美 | 欧洲(英语) | 法国 | 德国 | 日本 | +| ---- | ------------ | ---- | ---- | ---- | +| na | eu | fr | de | jp |`, }; async function handler(ctx) { diff --git a/lib/routes/jump/namespace.ts b/lib/routes/jump/namespace.ts index a7ca8ff68eb182..6c148f2c4bb59d 100644 --- a/lib/routes/jump/namespace.ts +++ b/lib/routes/jump/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'JUMP', url: 'switch.jumpvg.com', + lang: 'zh-CN', }; diff --git a/lib/routes/junhe/namespace.ts b/lib/routes/junhe/namespace.ts index d4f55988191e91..56a6e2eba0faa5 100644 --- a/lib/routes/junhe/namespace.ts +++ b/lib/routes/junhe/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: 'junhe.com', categories: ['new-media'], description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/kadokawa/blog.ts b/lib/routes/kadokawa/blog.ts new file mode 100644 index 00000000000000..b44109db5255fc --- /dev/null +++ b/lib/routes/kadokawa/blog.ts @@ -0,0 +1,134 @@ +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'; + +export const handler = async (ctx) => { + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + + const rootUrl = 'https://www.kadokawa.com.tw'; + const currentUrl = new URL('blog/posts', rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('div.List-item') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const image = item.find('div.List-item-excerpt img').prop('src')?.split(/\?/)[0] ?? undefined; + const title = item.find('h2.List-item-title').text(); + const description = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + intro: item.find('div.List-item-preview').text(), + }); + + return { + title, + description, + pubDate: parseDate(item.find('span.primary-border-color-after').text()), + link: new URL(item.find('a').prop('href'), rootUrl).href, + content: { + html: description, + text: item.find('div.List-item-preview').text(), + }, + image, + banner: image, + language, + enclosure_url: image, + enclosure_type: image ? `image/${image.split(/\./).pop()}` : undefined, + enclosure_title: title, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const title = $$('h1.Post-title').text().trim(); + const description = art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.Post-content').html(), + }); + const image = $$('meta[property="og:image"]').prop('content')?.split(/\?/)[0] ?? undefined; + + item.title = title; + item.description = description; + item.pubDate = parseDate($$('div.Post-date').text().trim()); + item.content = { + html: description, + text: $$('div.Post-content').text(), + }; + item.image = image; + item.banner = image; + item.language = language; + item.enclosure_url = image; + item.enclosure_type = image ? `image/${image.split(/\./).pop()}` : undefined; + item.enclosure_title = title; + + 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: '/blog', + name: '角編新聞台', + url: 'kadokawa.com.tw', + maintainers: ['nczitzk'], + handler, + example: '/kadokawa/blog', + parameters: undefined, + description: '', + categories: ['blog'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['kadokawa.com.tw/blog/posts'], + target: '/blog', + }, + ], +}; diff --git a/lib/routes/kadokawa/namespace.ts b/lib/routes/kadokawa/namespace.ts new file mode 100644 index 00000000000000..801c821dfa36df --- /dev/null +++ b/lib/routes/kadokawa/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '台灣角川', + url: 'kadokawa.com.tw', + categories: ['shopping'], + description: 'TAIWAN KADOKAWA', + lang: 'zh-TW', +}; diff --git a/lib/routes/kadokawa/templates/description.art b/lib/routes/kadokawa/templates/description.art new file mode 100644 index 00000000000000..249654e7e618a4 --- /dev/null +++ b/lib/routes/kadokawa/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} + <figure> + <img + {{ if image.alt }} + alt="{{ image.alt }}" + {{ /if }} + src="{{ image.src }}"> + </figure> + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} + <blockquote>{{ intro }}</blockquote> +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/kakuyomu/namespace.ts b/lib/routes/kakuyomu/namespace.ts new file mode 100644 index 00000000000000..a9556a7f51f917 --- /dev/null +++ b/lib/routes/kakuyomu/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'カクヨム', + url: 'kakuyomu.jp', + lang: 'ja', +}; diff --git a/lib/routes/kakuyomu/types.ts b/lib/routes/kakuyomu/types.ts new file mode 100644 index 00000000000000..80e46dab9b6b4a --- /dev/null +++ b/lib/routes/kakuyomu/types.ts @@ -0,0 +1,6 @@ +export interface NextDataEpisode { + __typename: 'Episode'; + id: string; + title: string; + publishedAt: string; +} diff --git a/lib/routes/kakuyomu/works.ts b/lib/routes/kakuyomu/works.ts new file mode 100644 index 00000000000000..a882f51ffad9b4 --- /dev/null +++ b/lib/routes/kakuyomu/works.ts @@ -0,0 +1,73 @@ +import type { Data, DataItem, Route } from '@/types'; +import { load } from 'cheerio'; +import type { Context } from 'hono'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import type { NextDataEpisode } from './types'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + name: '投稿', + categories: ['reading'], + path: '/works/:id', + example: '/kakuyomu/works/1177354054894027232', + parameters: { + id: '投稿 ID', + }, + maintainers: ['KarasuShin'], + handler, + features: { + supportRadar: true, + }, + radar: [ + { + source: ['kakuyomu.jp/works/:id'], + target: '/works/:id', + }, + ], +}; + +async function handler(ctx: Context): Promise<Data> { + const id = ctx.req.param('id'); + const url = `https://kakuyomu.jp/works/${id}`; + const limit = Number.parseInt(ctx.req.query('limit') || '10'); + const $ = load(await ofetch(url)); + + const nextData = JSON.parse($('#__NEXT_DATA__').text()); + + const { + props: { + pageProps: { __APOLLO_STATE__ }, + }, + } = nextData; + + const { + [`Work:${id}`]: { title, catchphrase }, + } = __APOLLO_STATE__; + + const values = Object.values(__APOLLO_STATE__); + const episodes = values.filter((value) => value.__typename === 'Episode') as NextDataEpisode[]; + const items = (await Promise.all( + episodes + .sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)) + .slice(0, limit) + .map((item) => { + const episodeUrl = `https://kakuyomu.jp/works/${id}/episodes/${item.id}`; + return cache.tryGet(episodeUrl, async () => { + const $ = load(await ofetch(episodeUrl)); + const description = $('.widget-episodeBody').html(); + return { + title: item.title, + description, + pubDate: parseDate(item.publishedAt), + }; + }); + }) + )) as DataItem[]; + + return { + title, + description: catchphrase, + item: items, + }; +} diff --git a/lib/routes/kamen-rider-official/namespace.ts b/lib/routes/kamen-rider-official/namespace.ts index 0056a34f5a513a..c8ef8defb04298 100644 --- a/lib/routes/kamen-rider-official/namespace.ts +++ b/lib/routes/kamen-rider-official/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '仮面ライダ', url: 'kamen-rider-official.com', + lang: 'ja', }; diff --git a/lib/routes/kamen-rider-official/news.ts b/lib/routes/kamen-rider-official/news.ts index 164d6add590630..e360f7f6130ac7 100644 --- a/lib/routes/kamen-rider-official/news.ts +++ b/lib/routes/kamen-rider-official/news.ts @@ -26,37 +26,37 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| Category | - | -------------------------------------- | - | すべて | - | テレビ | - | 映画・V シネマ等 | - | Blu-ray・DVD、配信等 | - | 20 作記念グッズ・東映 EC 商品 | - | 石ノ森章太郎生誕 80 周年記念商品 | - | 玩具・カード | - | 食品・飲料・菓子 | - | 子供生活雑貨 | - | アパレル・大人向け雑貨 | - | フィギュア・ホビー・一番くじ・プライズ | - | ゲーム・デジタル | - | 雑誌・書籍・漫画 | - | 音楽 | - | 映像 | - | イベント | - | ホテル・レストラン等 | - | キャンペーン・タイアップ等 | - | その他 | - | KAMEN RIDER STORE | - | THE 鎧武祭り | - | 鎧武外伝 | - | 仮面ライダーリバイス | - | ファイナルステージ | - | THE50 周年展 | - | 風都探偵 | - | 仮面ライダーギーツ | - | 仮面ライダーアウトサイダーズ | - | 仮面ライダーガッチャード | - | 仮面ライダー BLACK SUN |`, +| -------------------------------------- | +| すべて | +| テレビ | +| 映画・V シネマ等 | +| Blu-ray・DVD、配信等 | +| 20 作記念グッズ・東映 EC 商品 | +| 石ノ森章太郎生誕 80 周年記念商品 | +| 玩具・カード | +| 食品・飲料・菓子 | +| 子供生活雑貨 | +| アパレル・大人向け雑貨 | +| フィギュア・ホビー・一番くじ・プライズ | +| ゲーム・デジタル | +| 雑誌・書籍・漫画 | +| 音楽 | +| 映像 | +| イベント | +| ホテル・レストラン等 | +| キャンペーン・タイアップ等 | +| その他 | +| KAMEN RIDER STORE | +| THE 鎧武祭り | +| 鎧武外伝 | +| 仮面ライダーリバイス | +| ファイナルステージ | +| THE50 周年展 | +| 風都探偵 | +| 仮面ライダーギーツ | +| 仮面ライダーアウトサイダーズ | +| 仮面ライダーガッチャード | +| 仮面ライダー BLACK SUN |`, }; async function handler(ctx) { diff --git a/lib/routes/kantarworldpanel/namespace.ts b/lib/routes/kantarworldpanel/namespace.ts index 04926f660f992e..febed85ce34256 100644 --- a/lib/routes/kantarworldpanel/namespace.ts +++ b/lib/routes/kantarworldpanel/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Kantar Worldpanel', url: 'kantarworldpanel.com', + lang: 'en', }; diff --git a/lib/routes/kanxue/namespace.ts b/lib/routes/kanxue/namespace.ts index a0e4fde11ebdb8..2b27999c446f83 100644 --- a/lib/routes/kanxue/namespace.ts +++ b/lib/routes/kanxue/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '看雪', url: 'kanxue.com', + lang: 'zh-CN', }; diff --git a/lib/routes/kanxue/topic.ts b/lib/routes/kanxue/topic.ts index c4856d2706ad59..d4b3de1c85dc2b 100644 --- a/lib/routes/kanxue/topic.ts +++ b/lib/routes/kanxue/topic.ts @@ -32,28 +32,28 @@ export const route: Route = { maintainers: ['renzhexigua'], handler, description: `| 版块 | category | - | -------------- | --------- | - | 智能设备 | iot | - | Android 安全 | android | - | iOS 安全 | ios | - | HarmonyOS 安全 | harmonyos | - | 软件逆向 | re | - | 编程技术 | coding | - | 加壳脱壳 | unpack | - | 密码应用 | crypto | - | 二进制漏洞 | vuln | - | CTF 对抗 | ctf | - | Pwn | pwn | - | WEB 安全 | web | - | 茶余饭后 | chat | - | 极客空间 | geekzone | - | 外文翻译 | translate | - | 全站 | all | - - | 类型 | type | - | -------- | ------ | - | 最新主题 | latest | - | 精华主题 | digest |`, +| -------------- | --------- | +| 智能设备 | iot | +| Android 安全 | android | +| iOS 安全 | ios | +| HarmonyOS 安全 | harmonyos | +| 软件逆向 | re | +| 编程技术 | coding | +| 加壳脱壳 | unpack | +| 密码应用 | crypto | +| 二进制漏洞 | vuln | +| CTF 对抗 | ctf | +| Pwn | pwn | +| WEB 安全 | web | +| 茶余饭后 | chat | +| 极客空间 | geekzone | +| 外文翻译 | translate | +| 全站 | all | + +| 类型 | type | +| -------- | ------ | +| 最新主题 | latest | +| 精华主题 | digest |`, }; const timeDiff = 1000 * 60 * 60 * 24 * 3; diff --git a/lib/routes/kaopu/namespace.ts b/lib/routes/kaopu/namespace.ts index cfc43a76a33529..ff82086cf56e86 100644 --- a/lib/routes/kaopu/namespace.ts +++ b/lib/routes/kaopu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '靠谱新闻', url: 'kaopu.news', + lang: 'zh-CN', }; diff --git a/lib/routes/kaopu/news.ts b/lib/routes/kaopu/news.ts index c7b6e9aac616d8..6ebd6ba0ab394d 100644 --- a/lib/routes/kaopu/news.ts +++ b/lib/routes/kaopu/news.ts @@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/news/:language?', categories: ['new-media'], - example: '/news/zh-hans', + example: '/kaopu/news/zh-hans', parameters: { language: '语言', }, @@ -16,9 +16,9 @@ export const route: Route = { ], name: '全部', maintainers: ['fashioncj'], - description: `| 简体中文 | 繁体中文 | - | ------- | -------- | - | zh-hans | zh-hant | `, + description: `| 简体中文 | 繁体中文 | +| ------- | -------- | +| zh-hans | zh-hant | `, handler, }; diff --git a/lib/routes/kbs/namespace.ts b/lib/routes/kbs/namespace.ts index 272b89de2e0cd5..af50551dc9d1c7 100644 --- a/lib/routes/kbs/namespace.ts +++ b/lib/routes/kbs/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'KBS', url: 'world.kbs.co.kr', + lang: 'ko', }; diff --git a/lib/routes/kbs/news.ts b/lib/routes/kbs/news.ts index 043289f79d9d45..6d441fffbf766c 100644 --- a/lib/routes/kbs/news.ts +++ b/lib/routes/kbs/news.ts @@ -29,8 +29,8 @@ export const route: Route = { handler, url: 'world.kbs.co.kr/', description: `| 한국어 | عربي | 中国语 | English | Français | Deutsch | Bahasa Indonesia | 日本語 | Русский | Español | Tiếng Việt | - | ------ | ---- | ------ | ------- | -------- | ------- | ---------------- | ------ | ------- | ------- | ---------- | - | k | a | c | e | f | g | i | j | r | s | v |`, +| ------ | ---- | ------ | ------- | -------- | ------- | ---------------- | ------ | ------- | ------- | ---------- | +| k | a | c | e | f | g | i | j | r | s | v |`, }; async function handler(ctx) { diff --git a/lib/routes/kbs/today.ts b/lib/routes/kbs/today.ts index 07b47f1de4631e..6998cc1055708c 100644 --- a/lib/routes/kbs/today.ts +++ b/lib/routes/kbs/today.ts @@ -29,8 +29,8 @@ export const route: Route = { handler, url: 'world.kbs.co.kr/', description: `| 한국어 | عربي | 中国语 | English | Français | Deutsch | Bahasa Indonesia | 日本語 | Русский | Español | Tiếng Việt | - | ------ | ---- | ------ | ------- | -------- | ------- | ---------------- | ------ | ------- | ------- | ---------- | - | k | a | c | e | f | g | i | j | r | s | v |`, +| ------ | ---- | ------ | ------- | -------- | ------- | ---------------- | ------ | ------- | ------- | ---------- | +| k | a | c | e | f | g | i | j | r | s | v |`, }; async function handler(ctx) { diff --git a/lib/routes/kcna/namespace.ts b/lib/routes/kcna/namespace.ts index 0483df08256b95..4f12743a3bc6a1 100644 --- a/lib/routes/kcna/namespace.ts +++ b/lib/routes/kcna/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Korean Central News Agency (KCNA) 朝鲜中央通讯社', url: 'www.kcna.kp', + lang: 'ko', }; diff --git a/lib/routes/kcna/news.ts b/lib/routes/kcna/news.ts index 66bc4493974e79..f1bd2e6f2a91ee 100644 --- a/lib/routes/kcna/news.ts +++ b/lib/routes/kcna/news.ts @@ -7,9 +7,11 @@ import got from '@/utils/got'; import { load } from 'cheerio'; import asyncPool from 'tiny-async-pool'; import { art } from '@/utils/render'; -import { parseJucheDate, fixDesc, fetchPhoto, fetchVideo } from './utils'; +import { fixDesc, fetchPhoto, fetchVideo } from './utils'; import path from 'node:path'; import sanitizeHtml from 'sanitize-html'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; export const route: Route = { path: '/:lang/:category?', @@ -34,20 +36,20 @@ export const route: Route = { maintainers: ['Rongronggg9'], handler, description: `| Language | 조선어 | English | 中国语 | Русский | Español | 日本語 | - | -------- | ------ | ------- | ------ | ------- | ------- | ------ | - | \`:lang\` | \`kp\` | \`en\` | \`cn\` | \`ru\` | \`es\` | \`jp\` | +| -------- | ------ | ------- | ------ | ------- | ------- | ------ | +| \`:lang\` | \`kp\` | \`en\` | \`cn\` | \`ru\` | \`es\` | \`jp\` | - | Category | \`:category\` | - | ---------------------------------------------------------------- | ---------------------------------- | - | WPK General Secretary **Kim Jong Un**'s Revolutionary Activities | \`54c0ca4ca013a92cc9cf95bd4004c61a\` | - | Latest News (default) | \`1ee9bdb7186944f765208f34ecfb5407\` | - | Top News | \`5394b80bdae203fadef02522cfb578c0\` | - | Home News | \`b2b3bcc1b0a4406ab0c36e45d5db58db\` | - | Documents | \`a8754921399857ebdbb97a98a1e741f5\` | - | World | \`593143484cf15d48ce85c26139582395\` | - | Society-Life | \`93102e5a735d03979bc58a3a7aefb75a\` | - | External | \`0f98b4623a3ef82aeea78df45c423fd0\` | - | News Commentary | \`12c03a49f7dbe829bceea8ac77088c21\` |`, +| Category | \`:category\` | +| ---------------------------------------------------------------- | ---------------------------------- | +| WPK General Secretary **Kim Jong Un**'s Revolutionary Activities | \`54c0ca4ca013a92cc9cf95bd4004c61a\` | +| Latest News (default) | \`1ee9bdb7186944f765208f34ecfb5407\` | +| Top News | \`5394b80bdae203fadef02522cfb578c0\` | +| Home News | \`b2b3bcc1b0a4406ab0c36e45d5db58db\` | +| Documents | \`a8754921399857ebdbb97a98a1e741f5\` | +| World | \`593143484cf15d48ce85c26139582395\` | +| Society-Life | \`93102e5a735d03979bc58a3a7aefb75a\` | +| External | \`0f98b4623a3ef82aeea78df45c423fd0\` | +| News Commentary | \`12c03a49f7dbe829bceea8ac77088c21\` |`, }; async function handler(ctx) { @@ -66,12 +68,12 @@ async function handler(ctx) { .map((_, item) => { item = $(item); const dateElem = item.find('.publish-time'); - const dateString = dateElem.text(); + const dateString = dateElem.text().match(/\d+\.\d+\.\d+/); dateElem.remove(); return { title: item.text(), link: rootUrl + item.attr('href'), - pubDate: parseJucheDate(dateString), + pubDate: timezone(parseDate(dateString[0]), +9), }; }) .get(); @@ -87,9 +89,9 @@ async function handler(ctx) { item.title = $('article-main-title').text() || item.title; const dateElem = $('.publish-time'); - const dateString = dateElem.text(); + const dateString = dateElem.text().match(/\d+\.\d+\.\d+/); dateElem.remove(); - item.pubDate = parseJucheDate(dateString) || item.pubDate; + item.pubDate = dateString ? timezone(parseDate(dateString[0]), +9) : item.pubDate; const description = fixDesc($, $('.article-content-body .content-wrapper')); diff --git a/lib/routes/ke/namespace.ts b/lib/routes/ke/namespace.ts index 263b40f8c59938..5336403639058c 100644 --- a/lib/routes/ke/namespace.ts +++ b/lib/routes/ke/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '贝壳研究院', url: 'www.research.ke.com', + lang: 'zh-CN', }; diff --git a/lib/routes/keep/namespace.ts b/lib/routes/keep/namespace.ts index cdde3a1c62e593..e8c82891801ec3 100644 --- a/lib/routes/keep/namespace.ts +++ b/lib/routes/keep/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Keep', url: 'gotokeep.com', + lang: 'zh-CN', }; diff --git a/lib/routes/keep/user.ts b/lib/routes/keep/user.ts index fa2fc31efb6e99..0b4a82f0ef26c3 100644 --- a/lib/routes/keep/user.ts +++ b/lib/routes/keep/user.ts @@ -8,7 +8,7 @@ import path from 'node:path'; export const route: Route = { path: '/user/:id', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/keep/user/556b02c1ab59390afea671ea', parameters: { id: 'Keep 用户 id' }, features: { diff --git a/lib/routes/keepass/namespace.ts b/lib/routes/keepass/namespace.ts index 9b00957e005148..6bce1502b38aea 100644 --- a/lib/routes/keepass/namespace.ts +++ b/lib/routes/keepass/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'KeePass', url: 'keepass.info', + lang: 'en', }; diff --git a/lib/routes/kelownacapnews/namespace.ts b/lib/routes/kelownacapnews/namespace.ts new file mode 100644 index 00000000000000..d3f3c17c07a1f3 --- /dev/null +++ b/lib/routes/kelownacapnews/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Kelowna Capital News', + url: 'www.kelownacapnews.com', + lang: 'en', +}; diff --git a/lib/routes/kelownacapnews/news.ts b/lib/routes/kelownacapnews/news.ts new file mode 100644 index 00000000000000..71b1d680c2f5d9 --- /dev/null +++ b/lib/routes/kelownacapnews/news.ts @@ -0,0 +1,93 @@ +import { DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/:type', + categories: ['new-media'], + example: '/kelownacapnews/local-news', + parameters: { type: 'Type of news' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.kelownacapnews.com/:type'], + target: '/:type', + }, + ], + name: 'News', + maintainers: ['hualiong'], + url: 'www.kelownacapnews.com', + description: `\`type\` is as follows: + +| News type | Value | News type | Value | +| ------------- | ------------- | ------------ | ------------ | +| News | news | Sports | sports | +| Local News | local-news | Business | business | +| Canadian News | national-news | Trending Now | trending-now | +| World News | world-news | Opinion | opinion | +| Entertainment | entertainment | | |`, + handler: async (ctx) => { + const type = ctx.req.param('type'); + const baseURL = 'https://www.kelownacapnews.com'; + + const response = await ofetch(`${baseURL}/${type}`); + const $ = load(response); + + const list = $('.media') + .toArray() + .map((item): DataItem => { + const a = $(item); + return { + title: a.find('.media-heading').text(), + pubDate: parseDate(a.find('.media-links time').attr('datetime')!), + link: baseURL + a.attr('href'), + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link!, async () => { + const response = await ofetch(item.link!); + const $ = load(response); + + let image = $('.details-file')!; + if (!image.length) { + image = $('#sliderImgs .tablist-item .galleryWrap'); + } + const byline = $('.details-byline'); + const profileTitle = byline.find('.profile-title'); + if (profileTitle.length) { + item.author = profileTitle.find('a').text(); + } + let label = ''; + if (image.length > 1) { + for (const e of image.toArray()) { + const img = $(e); + label += `<figure style="margin: 10px 0 0 0"><img src='${img.data('src')}' /><figcaption>${img.attr('title')}</figcaption></figure>`; + } + } else { + label = `<figure style="margin: 0">${image.html()!}</figure>`; + } + item.description = label + $('.details-body').html()!; + + return item; + }) + ) + ); + + return { + title: `${$('.body-title').text()} - Kelowna Capital News`, + link: `${baseURL}/${type}`, + item: items as DataItem[], + }; + }, +}; diff --git a/lib/routes/kemono/index.ts b/lib/routes/kemono/index.ts index 22b8cc1b3f394b..2d92eb5a1365f0 100644 --- a/lib/routes/kemono/index.ts +++ b/lib/routes/kemono/index.ts @@ -32,14 +32,14 @@ export const route: Route = { handler, description: `Sources - | Posts | Patreon | Pixiv Fanbox | Gumroad | SubscribeStar | DLsite | Discord | Fantia | - | ----- | ------- | ------------ | ------- | ------------- | ------ | ------- | ------ | - | posts | patreon | fanbox | gumroad | subscribestar | dlsite | discord | fantia | +| Posts | Patreon | Pixiv Fanbox | Gumroad | SubscribeStar | DLsite | Discord | Fantia | +| ----- | ------- | ------------ | ------- | ------------- | ------ | ------- | ------ | +| posts | patreon | fanbox | gumroad | subscribestar | dlsite | discord | fantia | - :::tip +::: tip When \`posts\` is selected as the value of the parameter **source**, the parameter **id** does not take effect. There is an optinal parameter **limit** which controls the number of posts to fetch, default value is 25. - :::`, +:::`, }; async function handler(ctx) { @@ -97,7 +97,8 @@ async function handler(ctx) { const author = isPosts ? '' : await getAuthor(currentUrl, headers); title = isPosts ? 'Kemono Posts' : `Posts of ${author} from ${source} | Kemono`; image = isPosts ? `${rootUrl}/favicon.ico` : `https://img.kemono.su/icons/${source}/${id}`; - items = response.data + const responseData = isPosts ? response.data.posts : response.data; + items = responseData .filter((i) => i.content || i.attachments) .slice(0, limit) .map((i) => { @@ -140,6 +141,26 @@ async function handler(ctx) { desc += kemonoFile; } + let enclosureInfo = {}; + load(desc)('audio source, video source').each(function () { + const src = $(this).attr('src') ?? ''; + const mimeType = + { + m4a: 'audio/mp4', + mp3: 'audio/mpeg', + mp4: 'video/mp4', + }[src.replace(/.*\./, '').toLowerCase()] || null; + + if (mimeType === null) { + return; + } + + enclosureInfo = { + enclosure_url: new URL(src, rootUrl).toString(), + enclosure_type: mimeType, + }; + }); + return { title: i.title, description: desc, @@ -147,6 +168,7 @@ async function handler(ctx) { pubDate: parseDate(i.published), guid: `${apiUrl}/${i.service}/user/${i.user}/post/${i.id}`, link: `${rootUrl}/${i.service}/user/${i.user}/post/${i.id}`, + ...enclosureInfo, }; }); } @@ -154,7 +176,7 @@ async function handler(ctx) { return { title, image, - link: isPosts ? `${rootUrl}/posts` : source === 'discord' ? `${rootUrl}/${source}/server/${id}` : `${rootUrl}/${source}/user/${id}`, + link: isPosts ? `${rootUrl}/posts` : (source === 'discord' ? `${rootUrl}/${source}/server/${id}` : `${rootUrl}/${source}/user/${id}`), item: items, }; } diff --git a/lib/routes/kemono/namespace.ts b/lib/routes/kemono/namespace.ts index f0b7f6fe85eea9..fd4dd081e1e406 100644 --- a/lib/routes/kemono/namespace.ts +++ b/lib/routes/kemono/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Kemono', url: 'kemono.su', + lang: 'en', }; diff --git a/lib/routes/kepu/namespace.ts b/lib/routes/kepu/namespace.ts index 7b9c11f1bacd21..99ffb99b3c1383 100644 --- a/lib/routes/kepu/namespace.ts +++ b/lib/routes/kepu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国科普博览', url: 'live.kepu.net.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/keylol/index.ts b/lib/routes/keylol/index.ts index 2fff6f24c12eec..a77ce0299be278 100644 --- a/lib/routes/keylol/index.ts +++ b/lib/routes/keylol/index.ts @@ -1,55 +1,147 @@ import { Route } from '@/types'; -import { getSubPath } from '@/utils/common-utils'; import cache from '@/utils/cache'; +import { config } from '@/config'; import got from '@/utils/got'; import { load } from 'cheerio'; import timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; +import { parseDate, parseRelativeDate } from '@/utils/parse-date'; +import parser from '@/utils/rss-parser'; +import queryString from 'query-string'; + +const threadIdRegex = /(\d+)-\d+-\d+/; +const header = { + Cookie: config.keylol.cookie ?? undefined, +}; export const route: Route = { - path: '*', - name: 'Unknown', - maintainers: [], + path: '/:path', + name: '论坛', + parameters: { path: '路径,默认为热点聚焦' }, + categories: ['game'], + example: '/keylol/f161-1', + features: { + requireConfig: [ + { + name: 'KEYLOL_COOKIE', + optional: true, + description: `配置后可抓取具有阅读权限的帖子內容`, + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['keylol.com/:path'], + target: (params, url) => url.replaceAll('forum.php?', ''), + }, + ], + maintainers: ['nczitzk', 'kennyfong19931'], handler, + description: `::: tip + 若订阅 [热点聚焦](https://keylol.com/f161-1),网址为 \`https://keylol.com/f161-1\`。截取 \`https://keylol.com/\` 到末尾的部分 \`f161-1\` 作为参数,此时路由为 [\`/keylol/f161-1\`](https://rsshub.app/keylol/f161-1)。 + 若订阅子分类 [试玩免费 - 热点聚焦](https://keylol.com/forum.php?mod=forumdisplay&fid=161&filter=typeid&typeid=459),网址为 \`https://keylol.com/forum.php?mod=forumdisplay&fid=161&filter=typeid&typeid=459\`。提取\`fid\`及\`typeid\` 作为参数,此时路由为 [\`/keylol/fid=161&typeid=459\`](https://rsshub.app/keylol/fid=161&typeid=459)。注意不要包括\`filter\`,会调用[全局的内容过滤](https://docs.rsshub.app/guide/parameters#filtering)。 +:::`, }; async function handler(ctx) { - let thePath = getSubPath(ctx).replace(/^\//, ''); - - if (/^f\d+-\d+/.test(thePath)) { - thePath = `fid=${thePath.match(/^f(\d+)-\d+/)[1]}`; + let queryParams = {}; + const path = ctx.req.param('path'); + if (/^f\d+-\d+/.test(path)) { + queryParams.fid = path.match(/^f(\d+)-\d+/)[1]; + } else { + queryParams = queryString.parse(path); + } + queryParams.mod = 'forumdisplay'; + queryParams.orderby = 'dateline'; + queryParams.filter = 'author'; + + // get real author name from official rss feed + let authorNameMap; + try { + const feed = await parser.parseURL(`https://keylol.com/forum.php?mod=rss&fid=${queryParams.fid}&auth=0`); + authorNameMap = feed.items.map((item) => ({ + threadId: item.link.match(threadIdRegex)[1], + author: item.author, + })); + } catch { + authorNameMap = []; } const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; const rootUrl = 'https://keylol.com'; - const currentUrl = new URL(`forum.php?mod=forumdisplay&${thePath.replaceAll(/mod=\w+&/g, '')}`, rootUrl).href; + const currentUrl = queryString.stringifyUrl({ url: `${rootUrl}/forum.php`, query: queryParams }); - const { data: response } = await got(currentUrl); + const { data: response } = await got({ + method: 'get', + url: currentUrl, + headers: header, + }); const $ = load(response); - let items = $('a.xst') + let items = $('tbody[id^="normalthread_"]') .slice(0, limit) .toArray() .map((item) => { item = $(item); return { - title: item.text(), - link: new URL(item.prop('href').split('&extra=')[0], rootUrl).href, + title: item.find('a.xst').text(), + link: new URL(item.find(' a.xst').prop('href').split('&extra=')[0], rootUrl).href, + author: item.find('td.by-author cite').text(), + pubDate: parseRelativeDate(item.find('td.by-author em').text().replaceAll(' 发表', '')), }; }); items = await Promise.all( items.map((item) => cache.tryGet(item.link, async () => { - const { data: detailResponse } = await got(item.link); + const threadId = threadIdRegex.test(item.link) ? item.link.match(threadIdRegex)[1] : queryString.parseUrl(item.link).query.tid; + const { data: detailResponse } = await got({ + method: 'get', + url: item.link, + headers: header, + }); const content = load(detailResponse); - item.description = content('td.t_f').html(); - item.author = content('a.xw1').first().text(); + let descriptionList: string[] = []; + const indexDiv = content('div#threadindex'); + if (indexDiv.length > 0) { + // post with page + const postId = content('div.t_fsz > script') + .text() + .match(/show_threadindex\((\d+),/)[1]; + descriptionList = await Promise.all( + indexDiv.find('a').map((i, a) => { + const pageTitle = $(a).text(); + const page = $(a).attr('page'); + return getPage( + `${rootUrl}/forum.php?${queryString.stringify({ + mod: 'viewthread', + tid: threadId, + viewpid: postId, + cp: page, + })}`, + pageTitle + ); + }) + ); + } else { + // normal post + descriptionList.push(getDescription(content)); + } + + item.description = descriptionList.join('<br/>'); + const realAuthorName = authorNameMap.find((a) => a.threadId === threadId); + if (realAuthorName) { + item.author = realAuthorName.author; + } item.category = content('#keyloL_thread_tags a') .toArray() .map((c) => content(c).text()); @@ -90,3 +182,32 @@ async function handler(ctx) { author: $('meta[name="author"]').prop('content'), }; } + +function getDescription($) { + const descriptionEl = $('td.t_f'); + descriptionEl.find('div.rnd_ai_pr').remove(); // remove ad image + + // handle lazyload image + descriptionEl.find('img').each((_, img) => { + img = $(img); + if (img.attr('src')?.endsWith('none.gif') && img.attr('file')) { + img.attr('src', img.attr('file')); + img.removeAttr('file'); + img.removeAttr('zoomfile'); + } + }); + + return descriptionEl.length > 0 ? descriptionEl.html() : $('div.alert_info').html(); +} + +async function getPage(url, pageTitle) { + const { data: detailResponse } = await got({ + method: 'get', + url, + headers: header, + }); + + const $ = load(detailResponse, { xmlMode: true }); + const content = $('root').text(); + return '<h3>' + pageTitle + '</h3>' + getDescription(load(content)); +} diff --git a/lib/routes/keylol/namespace.ts b/lib/routes/keylol/namespace.ts index 05d8e559f97048..fb2e33f7ca9c05 100644 --- a/lib/routes/keylol/namespace.ts +++ b/lib/routes/keylol/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '其乐', url: 'keylol.com', + lang: 'zh-CN', }; diff --git a/lib/routes/kimlaw/namespace.ts b/lib/routes/kimlaw/namespace.ts index e05a6a2631efbd..0d645f53035880 100644 --- a/lib/routes/kimlaw/namespace.ts +++ b/lib/routes/kimlaw/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The Korea Institute of Marine Law', url: 'kimlaw.or.kr', + lang: 'ko', }; diff --git a/lib/routes/kisskiss/blog.ts b/lib/routes/kisskiss/blog.ts new file mode 100644 index 00000000000000..e2444dba3af753 --- /dev/null +++ b/lib/routes/kisskiss/blog.ts @@ -0,0 +1,74 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const baseUrl = 'https://www.kisskiss.tv/kiss/diary.php'; + +export const route: Route = { + path: '/blog/:category?', + categories: ['game'], + example: '/blog/DLC', + parameters: { category: 'category' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.kisskiss.tv/kiss/diary.php'], + target: '/blog', + }, + ], + name: 'ブログ', + maintainers: ['keocheung'], + handler, +}; + +async function handler(ctx) { + const { category } = ctx.req.param(); + const url = category ? `${baseUrl}?category=${category}` : baseUrl; + + const response = await got(url); + const $ = load(response.data); + + const items = $('table.blog_frame_top') + .toArray() + .map((item) => { + const title = $(item); + const body = title.next('div.blog_frame_middle'); + const i = { + title: title.find('tbody tr td').text(), + link: title.find('tbody tr td a').attr('href'), + pubDate: timezone( + parseDate( + body + .find('div.blog_data div.data_r') + .text() + .match(/\d+年\d+月\d+日 \(\d+:\d+\)/)[0], + 'YYYY年M月D日 (HH:mm)' + ), + 9 + ), + }; + body.find('a img').each(function () { + $(this).attr('src', $(this).parent('a').attr('href')); + $(this).unwrap(); + }); + body.find('div.blog_data').remove(); + i.description = `<div lang="ja-JP">${body.html()}</div>`; + return i; + }); + + return { + title: 'KISS ブログ', + link: url, + item: items, + language: 'ja', + }; +} diff --git a/lib/routes/kisskiss/namespace.ts b/lib/routes/kisskiss/namespace.ts new file mode 100644 index 00000000000000..c745836996df92 --- /dev/null +++ b/lib/routes/kisskiss/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'KISS', + url: 'www.kisskiss.tv', + lang: 'ja', +}; diff --git a/lib/routes/komiic/comic.ts b/lib/routes/komiic/comic.ts new file mode 100644 index 00000000000000..b0cc6e6c602d99 --- /dev/null +++ b/lib/routes/komiic/comic.ts @@ -0,0 +1,88 @@ +import { Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import got from '@/utils/got'; + +export const route: Route = { + path: '/comic/:id', + categories: ['anime'], + example: '/komiic/comic/533', + parameters: { id: '漫画 ID' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['komiic.com/comic/:id'], + target: '/comic/:id', + }, + ], + name: '漫画更新', + maintainers: ['NekoAria'], + handler, +}; + +async function handler(ctx) { + const { id } = ctx.req.param(); + const { limit = 0 } = ctx.req.query(); + const baseUrl = 'https://komiic.com'; + + const { data: comicInfo } = await got.post(`${baseUrl}/api/query`, { + json: { + operationName: 'comicById', + variables: { comicId: id }, + query: `query comicById($comicId: ID!) { + comicById(comicId: $comicId) { + title + imageUrl + } + }`, + }, + }); + + const { title, imageUrl } = comicInfo.data.comicById; + + const { data: chapterData } = await got.post(`${baseUrl}/api/query`, { + json: { + operationName: 'chapterByComicId', + variables: { comicId: id }, + query: `query chapterByComicId($comicId: ID!) { + chaptersByComicId(comicId: $comicId) { + id + serial + type + dateUpdated + size + } + }`, + }, + }); + + const sortedChapters = chapterData.data.chaptersByComicId.sort((a, b) => Date.parse(b.dateUpdated) - Date.parse(a.dateUpdated)); + + const chapterLimit = Number(limit) || sortedChapters.length; + const filteredChapters = sortedChapters.slice(0, chapterLimit); + + const generateChapterDescription = (chapter) => + ` + <h1>${chapter.size}p</h1> + <img src="${imageUrl}" /> + `.trim(); + + const items = filteredChapters.map((chapter) => ({ + title: chapter.type === 'book' ? `第 ${chapter.serial} 卷` : `第 ${chapter.serial} 话`, + link: `${baseUrl}/comic/${id}/chapter/${chapter.id}/images/all`, + pubDate: parseDate(chapter.dateUpdated), + description: generateChapterDescription(chapter), + })); + + return { + title: `Komiic - ${title}`, + link: `${baseUrl}/comic/${id}`, + item: items, + }; +} diff --git a/lib/routes/komiic/namespace.ts b/lib/routes/komiic/namespace.ts new file mode 100644 index 00000000000000..34f6dae4277d50 --- /dev/null +++ b/lib/routes/komiic/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Komiic', + url: 'komiic.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/konachan/namespace.ts b/lib/routes/konachan/namespace.ts new file mode 100644 index 00000000000000..772118e013e3f0 --- /dev/null +++ b/lib/routes/konachan/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Konachan.com Anime Wallpapers', + url: 'konachan.com', + description: `konachan post`, + lang: 'en', +}; diff --git a/lib/routes/konachan/post.ts b/lib/routes/konachan/post.ts new file mode 100644 index 00000000000000..106146892c1fc9 --- /dev/null +++ b/lib/routes/konachan/post.ts @@ -0,0 +1,94 @@ +import { Route, ViewType } from '@/types'; +import got from '@/utils/got'; +import queryString from 'query-string'; + +export const route: Route = { + path: '/post/popular_recent/:period?', + categories: ['picture', 'popular'], + view: ViewType.Pictures, + example: '/konachan/post/popular_recent/1d', + parameters: { + period: { + description: '展示时间', + options: [ + { value: '1d', label: '最近 24 小时' }, + { value: '1w', label: '最近一周' }, + { value: '1m', label: '最近一月' }, + { value: '1y', label: '最近一年' }, + ], + default: '1d', + }, + }, + radar: [ + { + source: ['konachan.com/post'], + }, + ], + name: 'Popular Recent Posts', + maintainers: ['magic-akari', 'NekoAria'], + description: `| 最近 24 小时 | 最近一周 | 最近一月 | 最近一年 | +| ------- | -------- | ------- | -------- | +| 1d | 1w | 1m | 1y |`, + handler, +}; + +async function handler(ctx) { + const { period = '1d' } = ctx.req.param(); + + const response = await got({ + url: 'https://konachan.com/post/popular_recent.json', + searchParams: queryString.stringify({ + period, + }), + }); + + const posts = response.data; + + const titles = { + '1d': 'Last 24 hours', + '1w': 'Last week', + '1m': 'Last month', + '1y': 'Last year', + }; + + const mime = { + jpg: 'jpeg', + png: 'png', + }; + + const title = titles[period]; + + return { + title: `${title} - konachan.com`, + link: `https://konachan.com/post/popular_recent?period=${period}`, + item: posts.map((post) => ({ + title: post.tags, + id: `${ctx.path}#${post.id}`, + guid: `${ctx.path}#${post.id}`, + link: `https://konachan.com/post/show/${post.id}`, + author: post.author, + pubDate: new Date(post.created_at * 1e3).toUTCString(), + description: (() => { + const result = [`<img src="${post.sample_url}" />`]; + result.push(`<p>Rating:${post.rating}</p> <p>Score:${post.score}</p>`); + if (post.source) { + result.push(`<a href="${post.source}">Source</a>`); + } + if (post.parent_id) { + result.push(`<a href="https://konachan.com/post/show/${post.parent_id}">Parent</a>`); + } + return result.join(''); + })(), + media: { + content: { + url: post.file_url, + type: `image/${mime[post.file_ext]}`, + }, + thumbnail: { + url: post.preview_url, + }, + }, + category: post.tags.split(/\s+/), + })), + }; +} diff --git a/lib/routes/konghq/namespace.ts b/lib/routes/konghq/namespace.ts index eaa71c0a55124a..227dfd37e97076 100644 --- a/lib/routes/konghq/namespace.ts +++ b/lib/routes/konghq/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: 'Kong API 网关平台', url: 'konghq.com', description: `[Kong](https://konghq.com/) 是一家开源的 API 网关服务商,此处收集其官网的最新博客文章。`, + lang: 'zh-CN', }; diff --git a/lib/routes/koreaherald/index.ts b/lib/routes/koreaherald/index.ts new file mode 100644 index 00000000000000..95263bd12b48d9 --- /dev/null +++ b/lib/routes/koreaherald/index.ts @@ -0,0 +1,70 @@ +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: '/:category{.+}?', + categories: ['traditional-media'], + example: '/koreaherald/National', + parameters: { + category: 'Category from the path of the URL of the corresponding site, `National` by default', + }, + features: { + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + requireConfig: false, + }, + name: 'News', + maintainers: ['quiniapiezoelectricity'], + handler, + description: ` +::: tip +For example, the category for the page https://www.koreaherald.com/Business and https://www.koreaherald.com/Business/Market would be \`/Business\` and \`/Business/Market\` respectively. +::: +`, + radar: [ + { + source: ['www.koreaherald.com/:category'], + target: '/:category', + }, + ], +}; + +async function handler(ctx) { + const category = ctx.req.param('category') ?? 'National'; + const baseUrl = 'https://www.koreaherald.com/'; + + const response = await got(new URL(category, baseUrl).href); + const $ = load(response.data); + const title = $('ul.gnb').find('[class="on"]').length > 0 ? $('ul.gnb').find('[class="on"]').text() : $('div.nav_area > a.category').text(); + const list = $('article.recent_news > ul.news_list > li') + .toArray() + .map((item) => new URL($(item).find('a').attr('href'), baseUrl).href); + const items = await Promise.all( + list.map((url) => + cache.tryGet(url, async () => { + const response = await got(url); + const $ = load(response.data); + const metadata = JSON.parse($('[type="application/ld+json"]').text()); + return { + title: metadata.headline, + link: url, + pubDate: timezone(parseDate(metadata.datePublished), +9), + author: metadata.author.name, + description: $('article.article-body').html(), + }; + }) + ) + ); + return { + title: `The Korea Herald - ${title}`, + link: new URL(category, baseUrl).href, + item: items, + }; +} diff --git a/lib/routes/koreaherald/namespace.ts b/lib/routes/koreaherald/namespace.ts new file mode 100644 index 00000000000000..e275a2543ce8ab --- /dev/null +++ b/lib/routes/koreaherald/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'The Korea Herald', + url: 'koreaherald.com', +}; diff --git a/lib/routes/kpmg/namespace.ts b/lib/routes/kpmg/namespace.ts index 04ff3866e0a9d3..0846a2f4292ffb 100644 --- a/lib/routes/kpmg/namespace.ts +++ b/lib/routes/kpmg/namespace.ts @@ -7,4 +7,5 @@ export const namespace: Namespace = { zh: { name: '毕马威', }, + lang: 'en', }; diff --git a/lib/routes/kpopping/kpics.ts b/lib/routes/kpopping/kpics.ts new file mode 100644 index 00000000000000..420bfd380f4a30 --- /dev/null +++ b/lib/routes/kpopping/kpics.ts @@ -0,0 +1,218 @@ +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<Data> => { + const { filter } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'https://kpopping.com'; + const targetUrl: string = new URL(`kpics${filter ? `/${filter}` : ''}`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'en'; + + let items: DataItem[] = []; + + items = $('div.pics div.matrix div.cell') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio<Element> = $(el); + + const title: string = $el.find('figcaption section').text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + images: $el.find('a.picture img').attr('src') + ? [ + { + src: $el.find('a.picture img').attr('src'), + alt: title, + }, + ] + : undefined, + }); + const linkUrl: string | undefined = $el.find('a').first().attr('href'); + const authors: DataItem['author'] = $el.find('figcaption section a').contents().last().text(); + const image: string | undefined = $el.find('a.picture img').attr('src'); + + const processedItem: DataItem = { + title, + description, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + author: authors, + content: { + html: description, + text: description, + }, + image, + banner: image, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('h1').contents().first().text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.pics').first().html(), + }); + const pubDateStr: string | undefined = $$('meta[property="article:published_time"]').attr('content'); + const categoryEls: Element[] = $$('div.buttons a').toArray(); + const categories: string[] = [...new Set(categoryEls.map((el) => $$(el).text()).filter(Boolean))]; + const authorEls: Element[] = $$('div.content-snippet aside:not(.like)').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $$authorEl: Cheerio<Element> = $$(authorEl); + const $$authorAEl: Cheerio<Element> = $$authorEl.find('a').last(); + + return { + name: $$authorAEl.text(), + url: new URL($$authorAEl.attr('href') as string, baseUrl).href, + avatar: $$authorEl.find('img').attr('src'), + }; + }); + const image: string | undefined = $$('meta[name="twitter:image"]').attr('content'); + const upDatedStr: string | undefined = $$('meta[property="article:modified_time"]').attr('content'); + + let processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + const mediaEls: Element[] = $$('div.pics').first().find('img').toArray(); + const medias: Record<string, Record<string, string>> = mediaEls.reduce((acc: Record<string, Record<string, string>>, mediaEl) => { + const $$mediaEl: Cheerio<Element> = $$(mediaEl); + const url: string | undefined = $$mediaEl.attr('src') ? new URL($$mediaEl.attr('src') as string, baseUrl).href : undefined; + + if (!url) { + return acc; + } + + const medium: string = 'image'; + const count: number = Object.values(acc).filter((m) => m.medium === medium).length + 1; + const key: string = `${medium}${count}`; + + acc[key] = { + url, + medium, + title: $$mediaEl.attr('alt') || title, + description: $$mediaEl.attr('alt') || title, + thumbnail: url, + }; + + return acc; + }, {}); + + if (medias) { + processedItem = { + ...processedItem, + media: medias, + }; + } + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[property="og:description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('meta[property="og:image"]').attr('content'), + author: $('meta[property="og:site_name"]').attr('content'), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/kpics/:filter{.+}?', + name: 'Pics', + url: 'kpopping.com', + maintainers: ['nczitzk'], + handler, + example: '/kpopping/kpics/gender-male/category-all/idol-any/group-any/order', + parameters: { + filter: 'Filter', + }, + description: `:::tip +If you subscribe to [All male photo albums](https://kpopping.com/kpics/gender-male/category-all/idol-any/group-any/order),where the URL is \`https://kpopping.com/kpics/gender-male/category-all/idol-any/group-any/order\`, extract the part \`https://kpopping.com/kpics/\` to the end, which is \`gender-male/category-all/idol-any/group-any/order\`, and use it as the parameter to fill in. Therefore, the route will be [\`/kpopping/kpics/gender-male/category-all/idol-any/group-any/order\`](https://rsshub.app/kpopping/kpics/gender-male/category-all/idol-any/group-any/order). +::: +`, + categories: ['picture'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['kpopping.com/kpics/:filter'], + target: (params) => { + const filter: string = params.filter; + + return `/kpopping/kpics${filter ? `/${filter}` : ''}`; + }, + }, + ], + view: ViewType.Articles, + + zh: { + path: '/kpics/:filter{.+}?', + name: 'Pics', + url: 'kpopping.com', + maintainers: ['nczitzk'], + handler, + example: '/kpopping/kpics/gender-male/category-all/idol-any/group-any/order', + parameters: { + filter: '筛选,可在对应分类页 URL 中找到', + }, + description: `:::tip +若订阅 [All male photo albums](https://kpopping.com/kpics/gender-male/category-all/idol-any/group-any/order),网址为 \`https://kpopping.com/kpics/gender-male/category-all/idol-any/group-any/order\`,请截取 \`https://kpopping.com/kpics/\` 到末尾的部分 \`gender-male/category-all/idol-any/group-any/order\` 作为 \`filter\` 参数填入,此时目标路由为 [\`/kpopping/kpics/gender-male/category-all/idol-any/group-any/order\`](https://rsshub.app/kpopping/kpics/gender-male/category-all/idol-any/group-any/order)。 +::: +`, + }, +}; diff --git a/lib/routes/kpopping/namespace.ts b/lib/routes/kpopping/namespace.ts new file mode 100644 index 00000000000000..9160b3c0fbca1d --- /dev/null +++ b/lib/routes/kpopping/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'kpopping', + url: 'kpopping.com', + categories: ['new-media'], + description: '', + lang: 'en', +}; diff --git a/lib/routes/kpopping/news.ts b/lib/routes/kpopping/news.ts new file mode 100644 index 00000000000000..6c9e0f5bd090cc --- /dev/null +++ b/lib/routes/kpopping/news.ts @@ -0,0 +1,197 @@ +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 timezone from '@/utils/timezone'; + +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<Data> => { + const { filter } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '2', 10); + + const baseUrl: string = 'https://kpopping.com'; + const targetUrl: string = new URL(`news${filter ? `/${filter}` : ''}`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'en'; + + let items: DataItem[] = []; + + items = $('section.news-list-item') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio<Element> = $(el); + const $aEl: Cheerio<Element> = $el.find('h4.title-wr a').last(); + + const title: string = $aEl.text(); + const pubDateStr: string | undefined = $el.find('time.datetime-wr').attr('datetime'); + const linkUrl: string | undefined = $aEl.attr('href'); + const categoryEls: Element[] = [$el.find('h4.title-wr a').first()]; + const categories: string[] = [...new Set(categoryEls.map((el) => $(el).text()).filter(Boolean))]; + const authorEls: Element[] = $el.find('aside.author-wr').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $authorEl: Cheerio<Element> = $(authorEl); + const $authorAEl: Cheerio<Element> = $authorEl.find('a').last(); + + return { + name: $authorAEl.text(), + url: new URL($authorAEl.attr('href') as string, baseUrl).href, + avatar: new URL($el.find('aside.author-wr a img').attr('src') as string, baseUrl).href, + }; + }); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr, 'MMM D, YYYY h:mma'), +8) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + category: categories, + author: authors, + doi: $el.find('meta[name="citation_doi"]').attr('content'), + updated: upDatedStr ? timezone(parseDate(upDatedStr, 'MMM D, YYYY h:mma'), +8) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('h1').contents().first().text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + images: $$('figure.opening img').attr('src') + ? [ + { + src: new URL($$('figure.opening img').attr('src') as string, baseUrl).href, + alt: title, + }, + ] + : undefined, + description: $$('div#article-content').html(), + }); + const pubDateStr: string | undefined = $$('meta[property="article:published_time"]').attr('content'); + const categoryEls: Element[] = $$('aside.info a, div.supplements a.item').toArray(); + const categories: string[] = [...new Set(categoryEls.map((el) => $$(el).text()?.trim()).filter(Boolean))]; + const authorEls: Element[] = $$('div.content-snippet aside:not(.like)').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $$authorEl: Cheerio<Element> = $$(authorEl); + const $$authorAEl: Cheerio<Element> = $$authorEl.find('a').last(); + + return { + name: $$authorAEl.text(), + url: new URL($$authorAEl.attr('href') as string, baseUrl).href, + avatar: $$authorEl.find('img').attr('src'), + }; + }); + const image: string | undefined = $$('meta[property="og:image"]').attr('content'); + const upDatedStr: string | undefined = $$('meta[property="article:modified_time"]').attr('content'); + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[property="og:description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('meta[property="og:image"]').attr('content'), + author: $('meta[property="og:site_name"]').attr('content'), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/news/:filter{.+}?', + name: 'News', + url: 'kpopping.com', + maintainers: ['nczitzk'], + handler, + example: '/kpopping/news/gender-all/category-all/idol-any/group-any/order', + parameters: { + filter: 'Filter', + }, + description: `:::tip +If you subscribe to [All male articles](https://kpopping.com/news/gender-male/category-all/idol-any/group-any/order),where the URL is \`https://kpopping.com/news/gender-male/category-all/idol-any/group-any/order\`, extract the part \`https://kpopping.com/news\` to the end, which is \`gender-male/category-all/idol-any/group-any/order\`, and use it as the parameter to fill in. Therefore, the route will be [\`/kpopping/news/gender-male/category-all/idol-any/group-any/order\`](https://rsshub.app/kpopping/news/gender-male/category-all/idol-any/group-any/order). +::: +`, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['kpopping.com/news/:filter'], + target: (params) => { + const filter: string = params.filter; + + return `/kpopping/news${filter ? `/${filter}` : ''}`; + }, + }, + ], + view: ViewType.Articles, + + zh: { + path: '/news/:filter{.+}?', + name: 'News', + url: 'kpopping.com', + maintainers: ['nczitzk'], + handler, + example: '/kpopping/news/gender-all/category-all/idol-any/group-any/order', + parameters: { + filter: '筛选,可在对应分类页 URL 中找到', + }, + description: `:::tip +若订阅 [All male articles](https://kpopping.com/news/gender-male/category-all/idol-any/group-any/order),网址为 \`https://kpopping.com/news/gender-male/category-all/idol-any/group-any/order\`,请截取 \`https://kpopping.com/news/\` 到末尾的部分 \`gender-male/category-all/idol-any/group-any/order\` 作为 \`filter\` 参数填入,此时目标路由为 [\`/kpopping/news/gender-male/category-all/idol-any/group-any/order\`](https://rsshub.app/kpopping/news/gender-male/category-all/idol-any/group-any/order)。 +::: +`, + }, +}; diff --git a/lib/routes/kpopping/templates/description.art b/lib/routes/kpopping/templates/description.art new file mode 100644 index 00000000000000..dfab19230c1108 --- /dev/null +++ b/lib/routes/kpopping/templates/description.art @@ -0,0 +1,17 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} + <figure> + <img + {{ if image.alt }} + alt="{{ image.alt }}" + {{ /if }} + src="{{ image.src }}"> + </figure> + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/ktown4u/artist-brandlist.ts b/lib/routes/ktown4u/artist-brandlist.ts new file mode 100644 index 00000000000000..f488becbe078ed --- /dev/null +++ b/lib/routes/ktown4u/artist-brandlist.ts @@ -0,0 +1,61 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/artistBrandlist/:grpNo/:grpNo2?', + categories: ['shopping'], + example: '/ktown4u/artistBrandlist/234590/1723449', + parameters: { grpNo: 'artist id (Get in url)', grpNo2: 'product category id (Get in url), empty for all categories' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: [], + target: '/artistBrandlist/:grpNo/:grpNo2', + }, + ], + name: 'Get the products on sale', + maintainers: ['JamesWDGu'], + handler: async (ctx) => { + const { grpNo, grpNo2 = '' } = ctx.req.param(); + const data = await ofetch(`https://cn.ktown4u.com/selectArtistBrandList?cateGrpNo=${grpNo2}¤tPage=1&goodsSearch=newgoods&grpNo=${grpNo}&searchType=ARTIST`, { + method: 'POST', + headers: { + accept: 'application/json, text/plain, */*', + 'accept-language': 'en,zh-CN;q=0.9,zh;q=0.8', + }, + parseResponse: JSON.parse, + }); + const items = data.map((item) => ({ + title: item.GOODS_NM, + url: item.IMG_PATH, + link: `https://cn.ktown4u.com/iteminfo?goods_no=${item.GOODS_NO}`, + description: desc(item), + pubDate: parseDate(item.RELEASE_DT), + })); + + return { + title: rssTitle(data), + link: `https://cn.ktown4u.com/artistBrandlist?grp_no=${grpNo}&grp_no2=${grpNo2}`, + item: items, + }; + }, +}; + +const rssTitle = (data) => `ktown4u ${data[0].GRP_NM}`; + +const desc = (item) => { + const saleState = item.SALE_YN === 'N' ? '【售罄】' : ''; + let price = `${item.CURR_F_CD}${item.DISP_PRICE}`; + if (item.DISP_PRICE !== item.DISP_DC_PRICE) { + price = `${item.CURR_F_CD}${item.DISP_DC_PRICE} / 原价:${price}`; + } + return `${saleState} ${price} <br> <img src=${item.IMG_PATH}> <br> ${item.GOODS_NM}`; +}; diff --git a/lib/routes/ktown4u/namespace.ts b/lib/routes/ktown4u/namespace.ts new file mode 100644 index 00000000000000..0fc652bf3ac6ae --- /dev/null +++ b/lib/routes/ktown4u/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Ktown4u', + url: 'ktown4u.com', + lang: 'en', +}; diff --git a/lib/routes/kuaidi100/index.ts b/lib/routes/kuaidi100/index.ts index 4e2badd2605d94..434c1e6af01d1f 100644 --- a/lib/routes/kuaidi100/index.ts +++ b/lib/routes/kuaidi100/index.ts @@ -19,11 +19,11 @@ export const route: Route = { handler, description: `快递公司代号如果不能确定,可通过下方快递列表获得。 - :::warning +::: warning 1. 构造链接前请确认所有参数正确:错误\`快递公司 - 订单号\`组合将会缓存信息一小段时间防止产生无用查询 2. 正常查询的订单在未签收状态下不会被缓存:请控制查询频率 3. 订单完成后请尽快取消订阅,避免资源浪费 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/kuaidi100/namespace.ts b/lib/routes/kuaidi100/namespace.ts index 42423fd51ad825..8a6dd64b002760 100644 --- a/lib/routes/kuaidi100/namespace.ts +++ b/lib/routes/kuaidi100/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '快递 100', url: 'kuaidi100.com', + lang: 'zh-CN', }; diff --git a/lib/routes/kuaishou/namespace.ts b/lib/routes/kuaishou/namespace.ts new file mode 100644 index 00000000000000..2a784ede648c1e --- /dev/null +++ b/lib/routes/kuaishou/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '快手', + url: 'kuaishou.com', + categories: ['social-media'], + lang: 'zh-CN', +}; diff --git a/lib/routes/kuaishou/profile.ts b/lib/routes/kuaishou/profile.ts new file mode 100644 index 00000000000000..fac8dfeedef4a7 --- /dev/null +++ b/lib/routes/kuaishou/profile.ts @@ -0,0 +1,92 @@ +import { Route, Data } from '@/types'; +import puppeteer from '@/utils/puppeteer'; +import { config } from '@/config'; +export const route: Route = { + name: 'Profile', + path: '/profile/:principalId', + radar: [ + { + source: ['kuaishou.com/profile/:principalId'], + target: '/profile/:principalId', + }, + ], + parameters: { + principalId: '用户 id, 可在主页中找到', + }, + example: '/kuaishou/profile/3xk46q9cdnvgife', + maintainers: ['GuoChen-thlg'], + url: 'kuaishou.com/profile/:principalId', + description: `::: tip +The profile page of the user, which contains the user's information, videos, and other information. +:::`, + handler, +}; + +async function handler(ctx) { + const { principalId } = ctx.req.param(); + const browser = await puppeteer(); + const page = await browser.newPage(); + + let retryCount = 0; + let resolve; + let userInfo; + const promise = new Promise((res) => { + resolve = res; + }); + await page.setRequestInterception(true); + page.on('request', (req) => { + const resourceType = req.resourceType(); + if (resourceType === 'image' || resourceType === 'media' || resourceType === 'font' || resourceType === 'stylesheet' || resourceType === 'ping') { + req.abort(); + } else { + req.continue(); + } + }); + page.on('response', async (res) => { + if (res.ok() && res.url().includes('/live_api/profile/public')) { + const resData = await res.json(); + if (resData.data.list.length > 0) { + resolve(resData.data); + } else { + if (retryCount > config.requestRetry) { + resolve({}); + } + setTimeout(() => { + page.reload().then(); + retryCount++; + }, 3000); + } + } else if (res.ok() && res.url().includes('/live_api/baseuser/userinfo/byid')) { + // principalId + const resData = await res.json(); + userInfo = resData.data.userInfo; + } + }); + await page.goto('https://www.kuaishou.com', { + waitUntil: 'domcontentloaded', + }); + await page.goto(`https://live.kuaishou.com/profile/${principalId}`); + const resData = (await promise.catch((error) => error)) as Array<any>; + + await browser.close(); + const data: Data = { + title: userInfo?.name ?? `${principalId}的作品 - 快手`, + // description: JSON.stringify(resData), + item: + resData?.list?.map((item) => ({ + // title: '', + author: item.author.name, + description: `<video controls preload="metadata" poster="${item.poster}"> + <source src="${item.playUrl}" type="video/mp4"> + </video>`, + // link: '', + id: item.id, + guid: item.id, + banner: item.poster, + media: { + content: { url: item.playUrl }, + }, + })) || [], + }; + return data; +} diff --git a/lib/routes/kunchengblog/namespace.ts b/lib/routes/kunchengblog/namespace.ts index 00bebd3cbf9388..c45be5951e1359 100644 --- a/lib/routes/kunchengblog/namespace.ts +++ b/lib/routes/kunchengblog/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Kun Cheng', url: 'kunchengblog.com', + lang: 'zh-CN', }; diff --git a/lib/routes/kurogames/namespace.ts b/lib/routes/kurogames/namespace.ts new file mode 100644 index 00000000000000..e51551a1d27844 --- /dev/null +++ b/lib/routes/kurogames/namespace.ts @@ -0,0 +1,8 @@ +import { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '库洛游戏', + url: 'www.kurogames.com', + categories: ['game'], + lang: 'zh-CN', +}; diff --git a/lib/routes/kurogames/wutheringwaves/news.ts b/lib/routes/kurogames/wutheringwaves/news.ts new file mode 100644 index 00000000000000..95e161378669b0 --- /dev/null +++ b/lib/routes/kurogames/wutheringwaves/news.ts @@ -0,0 +1,59 @@ +import { DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import * as cheerio from 'cheerio'; +import ofetch from '@/utils/ofetch'; + +interface NewsItem { + articleContent: string; + articleDesc: string; + articleId: number; + articleTitle: string; + articleType: number; + createTime: string; + sortingMark: number; + startTime: string; + suggestCover: string; + top: number; +} + +export const route: Route = { + path: '/wutheringwaves/news', + categories: ['game'], + example: '/kurogames/wutheringwaves/news', + name: '鸣潮 — 游戏公告、新闻与活动', + radar: [ + { + source: ['mc.kurogames.com/m/main/news', 'mc.kurogames.com/main'], + }, + ], + maintainers: ['enpitsulin'], + description: '', + async handler() { + const res = await ofetch<NewsItem[]>('https://media-cdn-mingchao.kurogame.com/akiwebsite/website2.0/json/G152/zh/ArticleMenu.json', { query: { t: Date.now() } }); + const item = await Promise.all( + res.map((i) => { + const contentUrl = `https://media-cdn-mingchao.kurogame.com/akiwebsite/website2.0/json/G152/zh/article/${i.articleId}.json`; + const item = { + title: i.articleTitle, + pubDate: timezone(parseDate(i.createTime), +8), + link: `https://mc.kurogames.com/main/news/detail/${i.articleId}`, + } as DataItem; + return cache.tryGet(contentUrl, async () => { + const data = await ofetch<NewsItem>(contentUrl, { query: { t: Date.now() } }); + const $ = cheerio.load(data.articleContent); + + item.description = $.html() ?? i.articleDesc ?? ''; + return item; + }) as Promise<DataItem>; + }) + ); + return { + title: '《鸣潮》— 游戏公告、新闻和活动', + link: 'https://mc.kurogames.com/main#news', + item, + language: 'zh-cn', + }; + }, +}; diff --git a/lib/routes/kuwaitlocal/namespace.ts b/lib/routes/kuwaitlocal/namespace.ts index 69dde9d8383397..2a0aa18f5cee89 100644 --- a/lib/routes/kuwaitlocal/namespace.ts +++ b/lib/routes/kuwaitlocal/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Kuwait Local', url: 'kuwaitlocal.com', + lang: 'en', }; diff --git a/lib/routes/kyodonews/index.ts b/lib/routes/kyodonews/index.ts index a3f7cd84cc9da8..8277b4b88a4a29 100644 --- a/lib/routes/kyodonews/index.ts +++ b/lib/routes/kyodonews/index.ts @@ -35,7 +35,7 @@ export const route: Route = { async function handler(ctx) { const language = ctx.req.param('language') ?? 'china'; - const keyword = ctx.req.param('keyword') === 'RSS' ? 'rss' : ctx.req.param('keyword') ?? ''; + const keyword = ctx.req.param('keyword') === 'RSS' ? 'rss' : (ctx.req.param('keyword') ?? ''); // raise error for invalid languages if (!['china', 'tchina'].includes(language)) { diff --git a/lib/routes/kyodonews/namespace.ts b/lib/routes/kyodonews/namespace.ts index 9bec39d239a76b..f21ac864c16f15 100644 --- a/lib/routes/kyodonews/namespace.ts +++ b/lib/routes/kyodonews/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '共同网', url: 'china.kyodonews.net', + lang: 'zh-CN', }; diff --git a/lib/routes/laimanhua/namespace.ts b/lib/routes/laimanhua/namespace.ts index eb3d9bb86fd99e..091f7b0f09e0fe 100644 --- a/lib/routes/laimanhua/namespace.ts +++ b/lib/routes/laimanhua/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '来漫画', url: 'www.laimanhua8.com', + lang: 'zh-CN', }; diff --git a/lib/routes/lala/namespace.ts b/lib/routes/lala/namespace.ts index c0e76e0fc6417c..11ee20d38c514c 100644 --- a/lib/routes/lala/namespace.ts +++ b/lib/routes/lala/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '荒岛', url: 'lala.im', + lang: 'zh-CN', }; diff --git a/lib/routes/landiannews/category.ts b/lib/routes/landiannews/category.ts new file mode 100644 index 00000000000000..ab1a2cd4b08758 --- /dev/null +++ b/lib/routes/landiannews/category.ts @@ -0,0 +1,48 @@ +import { Data, DataItem, Route, ViewType } from '@/types'; +import { fetchNewsItems, fetchCategory } from './utils'; + +export const handler = async (ctx): Promise<Data> => { + const slug = ctx.req.param('slug'); + + const { id, name } = await fetchCategory(slug); + + const rootUrl = 'https://www.landiannews.com/'; + const postApiUrl = `${rootUrl}wp-json/wp/v2/posts?_embed&categories=${id}`; + + const items: DataItem[] = await fetchNewsItems(postApiUrl); + + return { + title: `${name} - 蓝点网`, + description: '给你感兴趣的内容!', + link: `${rootUrl}${slug}`, + item: items, + }; +}; + +export const route: Route = { + path: '/category/:slug', + name: '分类', + url: 'www.landiannews.com', + maintainers: ['cscnk52'], + handler, + example: '/landiannews/category/sells', + parameters: { slug: '分类名称' }, + description: undefined, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.landiannews.com/:slug'], + target: '/category/:slug', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/landiannews/index.ts b/lib/routes/landiannews/index.ts new file mode 100644 index 00000000000000..013850d513606d --- /dev/null +++ b/lib/routes/landiannews/index.ts @@ -0,0 +1,44 @@ +import { Data, DataItem, Route, ViewType } from '@/types'; +import { fetchNewsItems } from './utils'; + +export const handler = async (): Promise<Data> => { + const rootUrl = 'https://www.landiannews.com/'; + const postApiUrl = `${rootUrl}wp-json/wp/v2/posts?_embed`; + + const items: DataItem[] = await fetchNewsItems(postApiUrl); + + return { + title: '蓝点网', + description: '给你感兴趣的内容!', + link: rootUrl, + item: items, + }; +}; + +export const route: Route = { + path: '/', + name: '首页', + url: 'www.landiannews.com', + maintainers: ['nczitzk', 'cscnk52'], + handler, + example: '/landiannews', + parameters: undefined, + description: undefined, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.landiannews.com'], + target: '/', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/landiannews/namespace.ts b/lib/routes/landiannews/namespace.ts new file mode 100644 index 00000000000000..057cad1f7eccc4 --- /dev/null +++ b/lib/routes/landiannews/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '蓝点网', + url: 'landiannews.com', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/landiannews/tag.ts b/lib/routes/landiannews/tag.ts new file mode 100644 index 00000000000000..187bb8a3a2dcf4 --- /dev/null +++ b/lib/routes/landiannews/tag.ts @@ -0,0 +1,48 @@ +import { Data, DataItem, Route, ViewType } from '@/types'; +import { fetchNewsItems, fetchTag } from './utils'; + +export const handler = async (ctx): Promise<Data> => { + const slug = ctx.req.param('slug'); + + const { id, name } = await fetchTag(slug); + + const rootUrl = 'https://www.landiannews.com/'; + const postApiUrl = `${rootUrl}wp-json/wp/v2/posts?_embed&tags=${id}`; + + const items: DataItem[] = await fetchNewsItems(postApiUrl); + + return { + title: `${name} - 蓝点网`, + description: '给你感兴趣的内容!', + link: `${rootUrl}archives/tag/${slug}`, + item: items, + }; +}; + +export const route: Route = { + path: '/tag/:slug', + name: '标签', + url: 'www.landiannews.com', + maintainers: ['cscnk52'], + handler, + example: '/landiannews/tag/linux-kernel', + parameters: { slug: '标签名称' }, + description: undefined, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.landiannews.com/archives/tag/:slug'], + target: '/tag/:slug', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/landiannews/utils.ts b/lib/routes/landiannews/utils.ts new file mode 100644 index 00000000000000..99f2137147ed23 --- /dev/null +++ b/lib/routes/landiannews/utils.ts @@ -0,0 +1,34 @@ +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; + +const rootUrl = 'https://www.landiannews.com/'; + +const fetchTaxonomy = async (slug: string, type: 'categories' | 'tags') => { + const taxonomyUrl = `${rootUrl}wp-json/wp/v2/${type}?slug=${slug}`; + const cachedTaxonomy = await cache.tryGet(taxonomyUrl, async () => { + const taxonomyData = await ofetch(taxonomyUrl); + if (!taxonomyData[0] || !taxonomyData[0].id || !taxonomyData[0].name) { + throw new Error(`${type} ${slug} not found`); + } + return { id: taxonomyData[0].id, name: taxonomyData[0].name }; + }); + return cachedTaxonomy; +}; + +const fetchCategory = async (categorySlug: string) => await fetchTaxonomy(categorySlug, 'categories'); +const fetchTag = async (tagSlug: string) => await fetchTaxonomy(tagSlug, 'tags'); + +async function fetchNewsItems(apiUrl: string) { + const data = await ofetch(apiUrl); + + return data.map((item) => ({ + title: item.title.rendered, + description: item.content.rendered, + link: item.link, + pubDate: new Date(item.date).toUTCString(), + author: item._embedded.author[0].name, + category: item._embedded['wp:term'].flat().map((term) => term.name), + })); +} + +export { fetchCategory, fetchTag, fetchNewsItems }; diff --git a/lib/routes/lang/namespace.ts b/lib/routes/lang/namespace.ts index 65f7dc284ac400..3baa64d930875d 100644 --- a/lib/routes/lang/namespace.ts +++ b/lib/routes/lang/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '浪 Play 直播', url: 'lang.live', + lang: 'zh-CN', }; diff --git a/lib/routes/langchain/index.ts b/lib/routes/langchain/index.ts new file mode 100644 index 00000000000000..7dc1bf1ac96cb1 --- /dev/null +++ b/lib/routes/langchain/index.ts @@ -0,0 +1,74 @@ +import { Route, DataItem } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/blog', + categories: ['blog'], + example: '/langchain/blog', + radar: [ + { + source: ['blog.langchain.dev/'], + }, + ], + url: 'blog.langchain.dev/', + name: 'Blog', + maintainers: ['liyaozhong'], + handler, + description: 'LangChain Blog Posts', +}; + +async function handler() { + const rootUrl = 'https://blog.langchain.dev'; + const currentUrl = rootUrl; + + const response = await got(currentUrl); + const $ = load(response.data); + + const items = await Promise.all( + $('.posts-feed .post-card') + .toArray() + .map((item) => { + const $item = $(item); + const $link = $item.find('.post-card__content-link').first(); + + const href = $link.attr('href'); + const title = $item.find('.post-card__title').text().trim(); + const excerpt = $item.find('.post-card__excerpt').text().trim(); + + if (!href || !title) { + return null; + } + + const link = new URL(href, rootUrl).href; + + return { + title, + description: excerpt, + link, + } as DataItem; + }) + .filter((item): item is DataItem => item !== null) + .map((item) => + cache.tryGet(item.link as string, async () => { + try { + const detailResponse = await got(item.link); + const $detail = load(detailResponse.data); + + item.description = $detail('.article-content').html() || item.description; + + return item as DataItem; + } catch { + return item; + } + }) + ) + ); + + return { + title: 'LangChain Blog', + link: rootUrl, + item: items.filter((item): item is DataItem => item !== null), + }; +} diff --git a/lib/routes/langchain/namespace.ts b/lib/routes/langchain/namespace.ts new file mode 100644 index 00000000000000..04b5fcbddf1266 --- /dev/null +++ b/lib/routes/langchain/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'LangChain Blog', + url: 'blog.langchain.dev', + lang: 'en', +}; diff --git a/lib/routes/lanqiao/namespace.ts b/lib/routes/lanqiao/namespace.ts index 694b2e7a54237c..5c47818c77079c 100644 --- a/lib/routes/lanqiao/namespace.ts +++ b/lib/routes/lanqiao/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '蓝桥云课', url: 'lanqiao.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/laohu8/namespace.ts b/lib/routes/laohu8/namespace.ts index 59b4b57070b229..f8db35ddfec4c6 100644 --- a/lib/routes/laohu8/namespace.ts +++ b/lib/routes/laohu8/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '老虎社区', url: 'laohu8.com', + lang: 'zh-CN', }; diff --git a/lib/routes/laohu8/personal.ts b/lib/routes/laohu8/personal.ts index 697a8a0c3ef8f8..ad00e46efba7f7 100644 --- a/lib/routes/laohu8/personal.ts +++ b/lib/routes/laohu8/personal.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'; @@ -8,7 +8,8 @@ const rootUrl = 'https://www.laohu8.com'; export const route: Route = { path: '/personal/:id', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/laohu8/personal/3527667596890271', parameters: { id: '用户 ID,见网址链接' }, features: { diff --git a/lib/routes/last-origin/namespace.ts b/lib/routes/last-origin/namespace.ts new file mode 100644 index 00000000000000..d977feadc0689c --- /dev/null +++ b/lib/routes/last-origin/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'LastOrigin', + url: 'www.last-origin.com', + lang: 'ja', +}; diff --git a/lib/routes/last-origin/news.ts b/lib/routes/last-origin/news.ts new file mode 100644 index 00000000000000..6dacb38c94d0b1 --- /dev/null +++ b/lib/routes/last-origin/news.ts @@ -0,0 +1,65 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +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: '/news', + name: 'News', + url: 'www.last-origin.com', + maintainers: ['gudezhi'], + example: '/last-origin/news', + parameters: {}, + categories: ['game'], + features: { + supportRadar: true, + }, + radar: [ + { + source: ['www.last-origin.com/news.html', 'www.last-origin.com'], + target: '/news', + }, + ], + handler, + description: '', +}; + +async function handler() { + const baseUrl = 'https://www.last-origin.com/news.html'; + const response = await ofetch(baseUrl); + const $ = load(response); + + const list = $('.contents .news_wrap') + .toArray() + .map((item) => { + const title = $(item).find('.news_title').text().trim(); + const link = new URL($(item).find('a').attr('href')!, baseUrl).href; + const date = $(item).find('time').text().trim(); + const pubDate = timezone(parseDate(date), +9); + return { + title, + link, + pubDate, + description: '', + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + item.description = $('.news_contents_editor').html() ?? ''; + return item; + }) + ) + ); + + return { + title: 'LastOrigin官网公告', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/latepost/index.ts b/lib/routes/latepost/index.ts index 39e5b262c581c7..8d01440409ea91 100644 --- a/lib/routes/latepost/index.ts +++ b/lib/routes/latepost/index.ts @@ -39,8 +39,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 最新报道 | 晚点独家 | 人物访谈 | 晚点早知道 | 长报道 | - | -------- | -------- | -------- | ---------- | ------ | - | | 1 | 2 | 3 | 4 |`, +| -------- | -------- | -------- | ---------- | ------ | +| | 1 | 2 | 3 | 4 |`, }; async function handler(ctx) { @@ -72,7 +72,7 @@ async function handler(ctx) { let items = response.data.slice(0, limit).map((item) => ({ title: item.title, link: new URL(item.detail_url, rootUrl).href, - category: [item.is_dj ? exclusiveCategory : undefined, item.programa ? columns[item.programa].title : undefined, ...item.label.map((c) => c.label)], + category: [item.is_dj ? exclusiveCategory : undefined, item.programa ? columns[item.programa]?.title : undefined, ...item.label.map((c) => c.label)], guid: item.id, pubDate: parseDate(item.release_time, ['MM月DD日', 'YYYY年MM月DD日']), })); @@ -101,7 +101,7 @@ async function handler(ctx) { const pubDate = content('div.article-header-date').text(); if (pubDate) { - item.pubDate = /\d+月\d+日/.test(pubDate) ? parseDate(pubDate, ['MM月DD日 HH:mm', 'YYYY年MM月DD日 HH:mm']) : parseRelativeDate(pubDate); + item.pubDate = /\d+月\d+日/.test(pubDate) ? parseDate(pubDate, ['YYYY年MM月DD日 HH:mm', 'MM月DD日 HH:mm']) : parseRelativeDate(pubDate); } item.pubDate = timezone(item.pubDate, +8); diff --git a/lib/routes/latepost/namespace.ts b/lib/routes/latepost/namespace.ts index ee070520a5b3de..7c65989cabfa45 100644 --- a/lib/routes/latepost/namespace.ts +++ b/lib/routes/latepost/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '晚点 LatePost', url: 'latepost.com', + lang: 'zh-CN', }; diff --git a/lib/routes/layoffs/index.ts b/lib/routes/layoffs/index.ts index 8c46c8d06bad1b..b3abfa689ad4b4 100644 --- a/lib/routes/layoffs/index.ts +++ b/lib/routes/layoffs/index.ts @@ -95,7 +95,7 @@ async function handler() { $('script') .text() .match(/urlWithParams: "(.*?)"/)[1] - .replaceAll('\\u002F', '/'); + .replaceAll(String.raw`\u002F`, '/'); // Cache it again cache.set(ENTRY_URL, dataSourceUrl); diff --git a/lib/routes/layoffs/namespace.ts b/lib/routes/layoffs/namespace.ts index 0c9b14b6f29994..cead1767c0a16c 100644 --- a/lib/routes/layoffs/namespace.ts +++ b/lib/routes/layoffs/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Layoffs.fyi', url: 'layoffs.fyi', + lang: 'en', }; diff --git a/lib/routes/leagueoflegends/namespace.ts b/lib/routes/leagueoflegends/namespace.ts new file mode 100644 index 00000000000000..68acd3d5725b4a --- /dev/null +++ b/lib/routes/leagueoflegends/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'League of Legends', + url: 'leagueoflegends.com', + categories: ['game'], + lang: 'en', +}; diff --git a/lib/routes/leagueoflegends/patch-notes.ts b/lib/routes/leagueoflegends/patch-notes.ts new file mode 100644 index 00000000000000..4a32a07dd84374 --- /dev/null +++ b/lib/routes/leagueoflegends/patch-notes.ts @@ -0,0 +1,76 @@ +import { DataItem, Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/patch-notes', + categories: ['game'], + example: '/leagueoflegends/patch-notes', + radar: [ + { + source: ['www.leagueoflegends.com/en-us/news/tags/patch-notes/', 'www.leagueoflegends.com/en-us/news/game-updates/:postSlug'], + }, + ], + name: 'Patch Notes', + maintainers: ['noahm'], + async handler() { + const url = 'https://www.leagueoflegends.com/en-us/news/tags/patch-notes/'; + const response = await got({ + method: 'get', + url, + }); + + const data = response.data; + + const $ = load(data); + const nextData = $('script[id="__NEXT_DATA__"]').text(); + if (!nextData) { + throw new Error('missing next data'); + } + const list: PatchNotesItem[] = JSON.parse(nextData).props.pageProps.page.blades[2].items; + + return { + title: 'League of Legends Patch Notes', + link: url, + item: list.map( + (item): DataItem => ({ + title: item.title, + description: item.description.body, + pubDate: parseDate(item.publishedAt), + link: item.action.payload.url, + guid: item.analytics.contentId, + image: item.media.url, + itunes_item_image: item.media.url, + }) + ), + }; + }, +}; + +// partial type definition of JSON data pre-filled on the page +interface PatchNotesItem { + title: string; + publishedAt: string; + description: { + type: 'html'; + body: string; + }; + media: { + dimensions: { + height: number; + width: number; + }; + mimeType: string; + url: string; + }; + action: { + payload: { + url: string; + }; + type: 'weblink'; + }; + analytics: { + contentId: string; + }; +} diff --git a/lib/routes/learnblockchain/namespace.ts b/lib/routes/learnblockchain/namespace.ts index c16857fd9110b8..8973702ef6cf72 100644 --- a/lib/routes/learnblockchain/namespace.ts +++ b/lib/routes/learnblockchain/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '登链社区', url: 'learnblockchain.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/learnblockchain/posts.ts b/lib/routes/learnblockchain/posts.ts index 6c9afb0459482c..7fa084761b99c4 100644 --- a/lib/routes/learnblockchain/posts.ts +++ b/lib/routes/learnblockchain/posts.ts @@ -20,24 +20,24 @@ export const route: Route = { maintainers: ['running-grass'], handler, description: `| id | 分类 | - | -------- | ------------ | - | all | 全部 | - | DApp | 去中心化应用 | - | chains | 公链 | - | 联盟链 | 联盟链 | - | scaling | Layer2 | - | langs | 编程语言 | - | security | 安全 | - | dst | 存储 | - | basic | 理论研究 | - | other | 其他 | +| -------- | ------------ | +| all | 全部 | +| DApp | 去中心化应用 | +| chains | 公链 | +| 联盟链 | 联盟链 | +| scaling | Layer2 | +| langs | 编程语言 | +| security | 安全 | +| dst | 存储 | +| basic | 理论研究 | +| other | 其他 | - | id | 排序方式 | - | -------- | ----------- | - | newest | 最新 | - | featured | 精选 (默认) | - | featured | 最赞 | - | hottest | 最热 |`, +| id | 排序方式 | +| -------- | ----------- | +| newest | 最新 | +| featured | 精选 (默认) | +| featured | 最赞 | +| hottest | 最热 |`, }; async function handler(ctx) { diff --git a/lib/routes/learnku/namespace.ts b/lib/routes/learnku/namespace.ts index 4540616f9227a5..63776e84a1da45 100644 --- a/lib/routes/learnku/namespace.ts +++ b/lib/routes/learnku/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'LearnKu', url: 'learnku.com', + lang: 'zh-CN', }; diff --git a/lib/routes/leetcode/articles.ts b/lib/routes/leetcode/articles.ts index ff44afe65e4483..8bb7e6e49db625 100644 --- a/lib/routes/leetcode/articles.ts +++ b/lib/routes/leetcode/articles.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'; import MarkdownIt from 'markdown-it'; @@ -38,8 +38,8 @@ export const route: Route = { async function handler() { const link = new URL('/articles/', host).href; - const response = await got(link); - const $ = load(response.data); + const response = await ofetch(link, { parseResponse: (txt) => txt }); + const $ = load(response); const list = $('a.list-group-item') .filter((i, e) => $(e).find('h4.media-heading i').length === 0) @@ -59,37 +59,35 @@ async function handler() { cache.tryGet(info.link, async () => { const titleSlug = info.link.split('/')[4]; - const questionContent = await got - .post(gqlEndpoint, { - json: { - operationName: 'questionContent', - variables: { titleSlug }, - query: `query questionContent($titleSlug: String!) { + const questionContent = await ofetch(gqlEndpoint, { + method: 'POST', + body: { + operationName: 'questionContent', + variables: { titleSlug }, + query: `query questionContent($titleSlug: String!) { question(titleSlug: $titleSlug) { content mysqlSchemas dataSchemas } }`, - }, - }) - .json(); + }, + }); - const officialSolution = await got - .post(gqlEndpoint, { - json: { - operationName: 'officialSolution', - variables: { titleSlug }, - query: `query officialSolution($titleSlug: String!) { + const officialSolution = await ofetch(gqlEndpoint, { + method: 'POST', + body: { + operationName: 'officialSolution', + variables: { titleSlug }, + query: `query officialSolution($titleSlug: String!) { question(titleSlug: $titleSlug) { solution { content } } }`, - }, - }) - .json(); + }, + }); const solution = md.render(officialSolution.data.question.solution.content); diff --git a/lib/routes/leetcode/namespace.ts b/lib/routes/leetcode/namespace.ts index 9e142f8e02e500..62e58d7addeb39 100644 --- a/lib/routes/leetcode/namespace.ts +++ b/lib/routes/leetcode/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'LeetCode', url: 'leetcode.com', + lang: 'en', }; diff --git a/lib/routes/leiphone/namespace.ts b/lib/routes/leiphone/namespace.ts index 08f51e0ba38c86..6c29e6325a5076 100644 --- a/lib/routes/leiphone/namespace.ts +++ b/lib/routes/leiphone/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '雷峰网', url: 'leiphone.com', + lang: 'zh-CN', }; diff --git a/lib/routes/lemmy/index.ts b/lib/routes/lemmy/index.ts index a8bc1fd4b642c9..cd2c240db50276 100644 --- a/lib/routes/lemmy/index.ts +++ b/lib/routes/lemmy/index.ts @@ -10,9 +10,36 @@ import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { path: '/:community/:sort?', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/lemmy/technology@lemmy.world/Hot', - parameters: { community: 'Lemmmy community, for example technology@lemmy.world', sort: 'Sort by, defaut to Active' }, + parameters: { + community: 'Lemmmy community, for example technology@lemmy.world', + sort: { + description: 'Sort by', + options: [ + { value: 'Active', label: 'Active' }, + { value: 'Hot', label: 'Hot' }, + { value: 'New', label: 'New' }, + { value: 'Old', label: 'Old' }, + { value: 'TopDay', label: 'TopDay' }, + { value: 'TopWeek', label: 'TopWeek' }, + { value: 'TopMonth', label: 'TopMonth' }, + { value: 'TopYear', label: 'TopYear' }, + { value: 'TopAll', label: 'TopAll' }, + { value: 'MostComments', label: 'MostComments' }, + { value: 'NewComments', label: 'NewComments' }, + { value: 'TopHour', label: 'TopHour' }, + { value: 'TopSixHour', label: 'TopSixHour' }, + { value: 'TopTwelveHour', label: 'TopTwelveHour' }, + { value: 'TopThreeMonths', label: 'TopThreeMonths' }, + { value: 'TopSixMonths', label: 'TopSixMonths' }, + { value: 'TopNineMonths', label: 'TopNineMonths' }, + { value: 'Controversial', label: 'Controversial' }, + { value: 'Scaled', label: 'Scaled' }, + ], + default: 'Active', + }, + }, features: { requireConfig: [ { @@ -27,7 +54,7 @@ export const route: Route = { supportScihub: false, }, name: 'Community', - maintainers: ['wb14123'], + maintainers: ['wb14123', 'pseudoyu'], handler, }; diff --git a/lib/routes/lemmy/namespace.ts b/lib/routes/lemmy/namespace.ts index 7bf2dc3dbf4287..8aa1d050a11a0f 100644 --- a/lib/routes/lemmy/namespace.ts +++ b/lib/routes/lemmy/namespace.ts @@ -2,4 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Lemmy', + url: 'join-lemmy.org', + lang: 'en', }; diff --git a/lib/routes/lens/namespace.ts b/lib/routes/lens/namespace.ts new file mode 100644 index 00000000000000..8b7d5ff608b50b --- /dev/null +++ b/lib/routes/lens/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Lens', + url: 'www.lens.xyz', + lang: 'en', +}; diff --git a/lib/routes/lens/profile.ts b/lib/routes/lens/profile.ts new file mode 100644 index 00000000000000..ec7cb42f47357a --- /dev/null +++ b/lib/routes/lens/profile.ts @@ -0,0 +1,80 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; + +export const route: Route = { + path: '/profile/:handle', + categories: ['social-media'], + example: '/lens/profile/stani', + parameters: { handle: 'Lens handle' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['hey.xyz/u/:handle'], + target: '/profile/:handle', + }, + ], + name: 'Lens Profile', + maintainers: ['DIYgod'], + handler, +}; + +async function handler(ctx) { + const handle = ctx.req.param('handle'); + + const profile = ( + await got(`https://api-v2.lens.dev/`, { + method: 'POST', + json: { + operationName: 'Profile', + variables: { + request: { + forHandle: `lens/${handle}`, + }, + }, + query: 'query Profile($request: ProfileRequest!) {\n profile(request: $request) {\n ...ProfileFields\n __typename\n }\n}\n\nfragment AmountFields on Amount {\n asFiat(request: {for: USD}) {\n value\n __typename\n }\n asset {\n ...Erc20Fields\n __typename\n }\n value\n __typename\n}\n\nfragment Erc20Fields on Asset {\n ... on Erc20 {\n name\n symbol\n decimals\n contract {\n ...NetworkAddressFields\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment FollowModuleFields on FollowModule {\n ... on FeeFollowModuleSettings {\n type\n amount {\n ...AmountFields\n __typename\n }\n recipient\n __typename\n }\n ... on RevertFollowModuleSettings {\n type\n __typename\n }\n ... on UnknownFollowModuleSettings {\n type\n __typename\n }\n __typename\n}\n\nfragment HandleInfoFields on HandleInfo {\n fullHandle\n localName\n linkedTo {\n nftTokenId\n __typename\n }\n __typename\n}\n\nfragment ImageSetFields on ImageSet {\n optimized {\n uri\n __typename\n }\n raw {\n uri\n __typename\n }\n __typename\n}\n\nfragment MetadataAttributeFields on MetadataAttribute {\n type\n key\n value\n __typename\n}\n\nfragment NetworkAddressFields on NetworkAddress {\n address\n chainId\n __typename\n}\n\nfragment ProfileFields on Profile {\n id\n handle {\n ...HandleInfoFields\n __typename\n }\n ownedBy {\n ...NetworkAddressFields\n __typename\n }\n signless\n sponsor\n createdAt\n stats {\n ...ProfileStatsFields\n __typename\n }\n operations {\n ...ProfileOperationsFields\n __typename\n }\n interests\n followNftAddress {\n ...NetworkAddressFields\n __typename\n }\n followModule {\n ...FollowModuleFields\n __typename\n }\n metadata {\n ...ProfileMetadataFields\n __typename\n }\n __typename\n}\n\nfragment ProfileMetadataFields on ProfileMetadata {\n displayName\n bio\n picture {\n ... on ImageSet {\n ...ImageSetFields\n __typename\n }\n __typename\n }\n coverPicture {\n ...ImageSetFields\n __typename\n }\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n __typename\n}\n\nfragment ProfileOperationsFields on ProfileOperations {\n id\n isBlockedByMe {\n value\n __typename\n }\n isFollowedByMe {\n value\n __typename\n }\n isFollowingMe {\n value\n __typename\n }\n __typename\n}\n\nfragment ProfileStatsFields on ProfileStats {\n id\n followers\n following\n publications\n comments\n posts\n mirrors\n quotes\n lensClassifierScore\n __typename\n}', + }, + }) + ).data.data.profile; + + const publications = ( + await got(`https://api-v2.lens.dev/`, { + method: 'POST', + json: { + operationName: 'Publications', + variables: { + request: { + limit: 'TwentyFive', + where: { + metadata: null, + publicationTypes: ['POST', 'MIRROR', 'QUOTE'], + from: [profile.id], + }, + }, + }, + query: 'query Publications($request: PublicationsRequest!) {\n publications(request: $request) {\n items {\n ... on Post {\n ...PostFields\n __typename\n }\n ... on Comment {\n ...CommentFields\n __typename\n }\n ... on Mirror {\n ...MirrorFields\n __typename\n }\n ... on Quote {\n ...QuoteFields\n __typename\n }\n __typename\n }\n pageInfo {\n next\n __typename\n }\n __typename\n }\n}\n\nfragment AmountFields on Amount {\n asFiat(request: {for: USD}) {\n value\n __typename\n }\n asset {\n ...Erc20Fields\n __typename\n }\n value\n __typename\n}\n\nfragment AnyPublicationMetadataFields on PublicationMetadata {\n ... on VideoMetadataV3 {\n ...VideoMetadataV3Fields\n __typename\n }\n ... on ArticleMetadataV3 {\n ...ArticleMetadataV3Fields\n __typename\n }\n ... on AudioMetadataV3 {\n ...AudioMetadataV3Fields\n __typename\n }\n ... on ImageMetadataV3 {\n ...ImageMetadataV3Fields\n __typename\n }\n ... on LinkMetadataV3 {\n ...LinkMetadataV3Fields\n __typename\n }\n ... on LiveStreamMetadataV3 {\n ...LiveStreamMetadataV3Fields\n __typename\n }\n ... on MintMetadataV3 {\n ...MintMetadataV3Fields\n __typename\n }\n ... on TextOnlyMetadataV3 {\n ...TextOnlyMetadataV3Fields\n __typename\n }\n ... on CheckingInMetadataV3 {\n ...CheckingInMetadataV3Fields\n __typename\n }\n __typename\n}\n\nfragment CommentBaseFields on Comment {\n id\n publishedOn {\n id\n __typename\n }\n isHidden\n isEncrypted\n momoka {\n proof\n __typename\n }\n createdAt\n by {\n ...PublicationProfileFields\n __typename\n }\n stats {\n ...PublicationStatsFields\n __typename\n }\n operations {\n ...PublicationOperationFields\n __typename\n }\n metadata {\n ...AnyPublicationMetadataFields\n __typename\n }\n openActionModules {\n ...OpenActionModulesFields\n __typename\n }\n root {\n ... on Post {\n ...PostFields\n __typename\n }\n ... on Quote {\n ...QuoteBaseFields\n __typename\n }\n __typename\n }\n profilesMentioned {\n snapshotHandleMentioned {\n ...HandleInfoFields\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment CommentFields on Comment {\n ...CommentBaseFields\n commentOn {\n ...CommentOnFields\n ... on Comment {\n ...CommentBaseFields\n commentOn {\n ...CommentOnFields\n ... on Comment {\n ...CommentBaseFields\n commentOn {\n ...CommentOnFields\n ... on Comment {\n ...CommentBaseFields\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment CommentOnFields on PrimaryPublication {\n ... on Post {\n ...PostFields\n __typename\n }\n ... on Quote {\n ...QuoteBaseFields\n __typename\n }\n __typename\n}\n\nfragment EncryptableImageSetFields on EncryptableImageSet {\n optimized {\n uri\n __typename\n }\n __typename\n}\n\nfragment Erc20Fields on Asset {\n ... on Erc20 {\n name\n symbol\n decimals\n contract {\n ...NetworkAddressFields\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment HandleInfoFields on HandleInfo {\n fullHandle\n localName\n linkedTo {\n nftTokenId\n __typename\n }\n __typename\n}\n\nfragment ImageSetFields on ImageSet {\n optimized {\n uri\n __typename\n }\n raw {\n uri\n __typename\n }\n __typename\n}\n\nfragment MetadataAttributeFields on MetadataAttribute {\n type\n key\n value\n __typename\n}\n\nfragment MirrorFields on Mirror {\n id\n publishedOn {\n id\n __typename\n }\n isHidden\n momoka {\n proof\n __typename\n }\n createdAt\n by {\n ...PublicationProfileFields\n __typename\n }\n mirrorOn {\n ... on Post {\n ...PostFields\n __typename\n }\n ... on Comment {\n ...CommentFields\n __typename\n }\n ... on Quote {\n ...QuoteFields\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment NetworkAddressFields on NetworkAddress {\n address\n chainId\n __typename\n}\n\nfragment OpenActionModulesFields on OpenActionModule {\n ... on SimpleCollectOpenActionSettings {\n type\n contract {\n ...NetworkAddressFields\n __typename\n }\n amount {\n ...AmountFields\n __typename\n }\n collectNft\n collectLimit\n followerOnly\n recipient\n referralFee\n endsAt\n __typename\n }\n ... on MultirecipientFeeCollectOpenActionSettings {\n type\n contract {\n ...NetworkAddressFields\n __typename\n }\n amount {\n ...AmountFields\n __typename\n }\n collectNft\n collectLimit\n referralFee\n followerOnly\n endsAt\n recipients {\n recipient\n split\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PostFields on Post {\n id\n publishedOn {\n id\n __typename\n }\n isHidden\n isEncrypted\n momoka {\n proof\n __typename\n }\n createdAt\n by {\n ...PublicationProfileFields\n __typename\n }\n stats {\n ...PublicationStatsFields\n __typename\n }\n operations {\n ...PublicationOperationFields\n __typename\n }\n metadata {\n ...AnyPublicationMetadataFields\n __typename\n }\n openActionModules {\n ...OpenActionModulesFields\n __typename\n }\n profilesMentioned {\n snapshotHandleMentioned {\n ...HandleInfoFields\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment ProfileMetadataFields on ProfileMetadata {\n displayName\n bio\n picture {\n ... on ImageSet {\n ...ImageSetFields\n __typename\n }\n __typename\n }\n coverPicture {\n ...ImageSetFields\n __typename\n }\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n __typename\n}\n\nfragment ProfileOperationsFields on ProfileOperations {\n id\n isBlockedByMe {\n value\n __typename\n }\n isFollowedByMe {\n value\n __typename\n }\n isFollowingMe {\n value\n __typename\n }\n __typename\n}\n\nfragment PublicationOperationFields on PublicationOperations {\n isNotInterested\n hasBookmarked\n hasActed {\n value\n __typename\n }\n hasReacted(request: {type: UPVOTE})\n canMirror\n hasMirrored\n hasQuoted\n __typename\n}\n\nfragment PublicationProfileFields on Profile {\n id\n handle {\n ...HandleInfoFields\n __typename\n }\n operations {\n ...ProfileOperationsFields\n __typename\n }\n ownedBy {\n ...NetworkAddressFields\n __typename\n }\n metadata {\n ...ProfileMetadataFields\n __typename\n }\n __typename\n}\n\nfragment PublicationStatsFields on PublicationStats {\n id\n comments\n mirrors\n quotes\n reactions(request: {type: UPVOTE})\n countOpenActions(request: {anyOf: [{category: COLLECT}]})\n bookmarks\n __typename\n}\n\nfragment QuoteBaseFields on Quote {\n id\n publishedOn {\n id\n __typename\n }\n isHidden\n isEncrypted\n momoka {\n proof\n __typename\n }\n createdAt\n by {\n ...PublicationProfileFields\n __typename\n }\n stats {\n ...PublicationStatsFields\n __typename\n }\n operations {\n ...PublicationOperationFields\n __typename\n }\n metadata {\n ...AnyPublicationMetadataFields\n __typename\n }\n openActionModules {\n ...OpenActionModulesFields\n __typename\n }\n profilesMentioned {\n snapshotHandleMentioned {\n ...HandleInfoFields\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment QuoteFields on Quote {\n ...QuoteBaseFields\n quoteOn {\n ... on Post {\n ...PostFields\n __typename\n }\n ... on Comment {\n ...CommentBaseFields\n __typename\n }\n ... on Quote {\n ...QuoteBaseFields\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment ArticleMetadataV3Fields on ArticleMetadataV3 {\n id\n content\n tags\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n attachments {\n ...PublicationMetadataMediaFields\n __typename\n }\n __typename\n}\n\nfragment AudioMetadataV3Fields on AudioMetadataV3 {\n id\n title\n content\n tags\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n asset {\n ...PublicationMetadataMediaAudioFields\n __typename\n }\n attachments {\n ...PublicationMetadataMediaFields\n __typename\n }\n __typename\n}\n\nfragment CheckingInMetadataV3Fields on CheckingInMetadataV3 {\n id\n content\n tags\n location\n geographic {\n latitude\n longitude\n __typename\n }\n address {\n country\n locality\n postalCode\n __typename\n }\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n attachments {\n ...PublicationMetadataMediaFields\n __typename\n }\n __typename\n}\n\nfragment ImageMetadataV3Fields on ImageMetadataV3 {\n id\n content\n tags\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n attachments {\n ...PublicationMetadataMediaFields\n __typename\n }\n asset {\n ...PublicationMetadataMediaImageFields\n __typename\n }\n __typename\n}\n\nfragment LinkMetadataV3Fields on LinkMetadataV3 {\n id\n content\n sharingLink\n tags\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n attachments {\n ...PublicationMetadataMediaFields\n __typename\n }\n __typename\n}\n\nfragment LiveStreamMetadataV3Fields on LiveStreamMetadataV3 {\n id\n playbackURL\n liveURL\n content\n tags\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n attachments {\n ...PublicationMetadataMediaFields\n __typename\n }\n __typename\n}\n\nfragment MintMetadataV3Fields on MintMetadataV3 {\n id\n content\n tags\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n attachments {\n ...PublicationMetadataMediaFields\n __typename\n }\n __typename\n}\n\nfragment TextOnlyMetadataV3Fields on TextOnlyMetadataV3 {\n id\n content\n tags\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n __typename\n}\n\nfragment VideoMetadataV3Fields on VideoMetadataV3 {\n id\n content\n tags\n attributes {\n ...MetadataAttributeFields\n __typename\n }\n asset {\n ...PublicationMetadataMediaVideoFields\n __typename\n }\n attachments {\n ...PublicationMetadataMediaFields\n __typename\n }\n __typename\n}\n\nfragment PublicationMetadataMediaAudioFields on PublicationMetadataMediaAudio {\n artist\n audio {\n optimized {\n uri\n __typename\n }\n __typename\n }\n cover {\n ...EncryptableImageSetFields\n __typename\n }\n license\n __typename\n}\n\nfragment PublicationMetadataMediaFields on PublicationMetadataMedia {\n ... on PublicationMetadataMediaVideo {\n ...PublicationMetadataMediaVideoFields\n __typename\n }\n ... on PublicationMetadataMediaImage {\n ...PublicationMetadataMediaImageFields\n __typename\n }\n ... on PublicationMetadataMediaAudio {\n ...PublicationMetadataMediaAudioFields\n __typename\n }\n __typename\n}\n\nfragment PublicationMetadataMediaImageFields on PublicationMetadataMediaImage {\n image {\n ...EncryptableImageSetFields\n __typename\n }\n __typename\n}\n\nfragment PublicationMetadataMediaVideoFields on PublicationMetadataMediaVideo {\n video {\n optimized {\n uri\n __typename\n }\n __typename\n }\n cover {\n ...EncryptableImageSetFields\n __typename\n }\n license\n __typename\n}', + }, + }) + ).data.data.publications.items; + + return { + title: `${profile.metadata.displayName} on Lens`, + link: `https://hey.xyz/u/${handle}`, + item: publications.map((item) => { + const itemWithMirror = item.mirrorOn || item; + return { + title: itemWithMirror.metadata?.content || 'No content', + description: `${item.mirrorOn ? 'Mirrored: ' : ''}${itemWithMirror.metadata?.content || ''} ${itemWithMirror.metadata?.asset?.image?.optimized?.uri ? `<img src="${itemWithMirror.metadata?.asset?.image?.optimized?.uri}" />` : ''}`, + link: `https://hey.xyz/posts/${item.id}`, + pubDate: new Date(item.createdAt).toUTCString(), + guid: item.id, + }; + }), + }; +} diff --git a/lib/routes/lfsyd/namespace.ts b/lib/routes/lfsyd/namespace.ts index 4a8080d49119ca..18c8482cb9e907 100644 --- a/lib/routes/lfsyd/namespace.ts +++ b/lib/routes/lfsyd/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '旅法师营地', url: 'www.iyingdi.com', + lang: 'zh-CN', }; diff --git a/lib/routes/lhratings/namespace.ts b/lib/routes/lhratings/namespace.ts new file mode 100644 index 00000000000000..305f4601849bb5 --- /dev/null +++ b/lib/routes/lhratings/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '联合资信评估股份有限公司', + url: 'lhratings.com', + categories: ['finance'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/lhratings/research.ts b/lib/routes/lhratings/research.ts new file mode 100644 index 00000000000000..c24b2887db4ce3 --- /dev/null +++ b/lib/routes/lhratings/research.ts @@ -0,0 +1,145 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +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'; + +export const handler = async (ctx: Context): Promise<Data> => { + const { type = '1' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '20', 10); + + const baseUrl: string = 'https://www.lhratings.com'; + const targetUrl: string = new URL(`research.html?type=${type}`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'zh-CN'; + + const items: DataItem[] = $('table.list-table tbody tr') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio<Element> = $(el); + const $aEl: Cheerio<Element> = $el.find('a').first(); + + const title: string = $aEl.text(); + const pubDateStr: string | undefined = $aEl.parent().next().next().text(); + const linkUrl: string | undefined = $aEl.attr('href'); + const categoryEls: Element[] = [$aEl.parent().next()].filter(Boolean); + const categories: string[] = [...new Set(categoryEls.map((el) => $(el).text()).filter(Boolean))]; + const image: string | undefined = $el.find('img').attr('src'); + const upDatedStr: string | undefined = pubDateStr; + + let processedItem: DataItem = { + title, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl, + category: categories, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + const enclosureUrl: string | undefined = linkUrl; + + if (enclosureUrl) { + processedItem = { + ...processedItem, + enclosure_url: enclosureUrl, + enclosure_type: `application/${enclosureUrl.split(/\./).pop()}`, + enclosure_title: title, + }; + } + + return processedItem; + }); + + const author: string = $('title').text(); + + return { + title: `${author} - ${$('li.active').text()}`, + description: $('li.active').text(), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('a#logo img').attr('src'), + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/research/:type?', + name: '研究报告', + url: 'www.lhratings.com', + maintainers: ['nczitzk'], + handler, + example: '/lhratings/research/1', + parameters: { + type: '分类,默认为 `1`,即宏观经济,可在对应分类页 URL 中找到', + }, + description: `:::tip +若订阅 [宏观经济](https://www.lhratings.com/research.html?type=1),网址为 \`https://www.lhratings.com/research.html?type=1\`,请截取 \`https://www.lhratings.com/research.html?type=\` 到末尾的部分 \`1\` 作为 \`type\` 参数填入,此时目标路由为 [\`/lhratings/research/1\`](https://rsshub.app/lhratings/research/1)。 +::: + +| 宏观经济 | 债券市场 | 行业研究 | 评级理论与方法 | 国际债券市场与评级 | 评级表现 | +| -------- | -------- | -------- | -------------- | ------------------ | -------- | +| 1 | 2 | 3 | 4 | 5 | 6 | +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.lhratings.com/research.html'], + target: (_, url) => { + const urlObj: URL = new URL(url); + const type: string | undefined = urlObj.searchParams.get('type') ?? undefined; + + return `/lhratings/research/${type ? `/${type}` : ''}`; + }, + }, + { + title: '宏观经济', + source: ['www.lhratings.com/research.html?type=1'], + target: '/research/1', + }, + { + title: '债券市场', + source: ['www.lhratings.com/research.html?type=2'], + target: '/research/2', + }, + { + title: '行业研究', + source: ['www.lhratings.com/research.html?type=3'], + target: '/research/3', + }, + { + title: '评级理论与方法', + source: ['www.lhratings.com/research.html?type=4'], + target: '/research/4', + }, + { + title: '国际债券市场与评级', + source: ['www.lhratings.com/research.html?type=5'], + target: '/research/5', + }, + { + title: '评级表现', + source: ['www.lhratings.com/research.html?type=6'], + target: '/research/6', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/lianxh/namespace.ts b/lib/routes/lianxh/namespace.ts index d46ddbde43e4c1..af812ac58b9634 100644 --- a/lib/routes/lianxh/namespace.ts +++ b/lib/routes/lianxh/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: '连享会', url: 'www.lianxh.cn', categories: ['programming'], + lang: 'zh-CN', }; diff --git a/lib/routes/lifeweek/namespace.ts b/lib/routes/lifeweek/namespace.ts index f8d4e6ffc91126..9734b4fd9b8775 100644 --- a/lib/routes/lifeweek/namespace.ts +++ b/lib/routes/lifeweek/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '三联生活周刊', url: 'lifeweek.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/lightnovel/namespace.ts b/lib/routes/lightnovel/namespace.ts index 2f924cdd502c63..2df1b1bd30d854 100644 --- a/lib/routes/lightnovel/namespace.ts +++ b/lib/routes/lightnovel/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '轻之国度', url: 'lightNovel.us', + lang: 'zh-CN', }; diff --git a/lib/routes/likeshop/index.ts b/lib/routes/likeshop/index.ts new file mode 100644 index 00000000000000..71e13756b1ee75 --- /dev/null +++ b/lib/routes/likeshop/index.ts @@ -0,0 +1,43 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/:site', + categories: ['social-media'], + example: '/likeshop/bloombergpursuits', + parameters: { site: 'the site attached to likeshop.me/' }, + radar: [ + { + source: ['likeshop.me/'], + }, + ], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Posts', + maintainers: ['nickyfoto'], + handler, + description: 'LikeShop link in bio takes your audience from Instagram and TikTok to your website in one easy step.', +}; + +async function handler(ctx) { + const site = ctx.req.param('site'); + const link = `https://api.likeshop.me/api/accounts/${site}/galleries/likeshop`; + const data = await ofetch(link); + const items = data.data.media.map((item) => ({ + title: item.comment, + link: item.product_url.split('?')[0], + description: `<p><img src="${item.image_url.split('?')[0]}"></p>`, + guid: item.id, + })); + return { + title: `@${site} Likeshop`, + link: `https://likeshop.me/${site}`, + item: items, + }; +} diff --git a/lib/routes/likeshop/namespace.ts b/lib/routes/likeshop/namespace.ts new file mode 100644 index 00000000000000..3826d70fb4ad5b --- /dev/null +++ b/lib/routes/likeshop/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'LikeShop', + url: 'likeshop.me', + lang: 'en', +}; diff --git a/lib/routes/line/namespace.ts b/lib/routes/line/namespace.ts index e19adfea26fdea..4404dde93f821a 100644 --- a/lib/routes/line/namespace.ts +++ b/lib/routes/line/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'LINE', url: 'today.line.me', + lang: 'en', }; diff --git a/lib/routes/line/publisher.ts b/lib/routes/line/publisher.ts index 9307d761d1b0f9..ba673fb6bd045d 100644 --- a/lib/routes/line/publisher.ts +++ b/lib/routes/line/publisher.ts @@ -4,7 +4,7 @@ import { baseUrl, parseList, parseItems } from './utils'; export const route: Route = { path: '/today/:edition/publisher/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/line/today/th/publisher/101048', parameters: { edition: 'Edition, see table above', id: 'Channel ID, can be found in URL' }, radar: [ diff --git a/lib/routes/line/today.ts b/lib/routes/line/today.ts index 0260748e9c68ca..2ac75b31ace990 100644 --- a/lib/routes/line/today.ts +++ b/lib/routes/line/today.ts @@ -4,7 +4,7 @@ import { baseUrl as rootUrl, parseList, parseItems } from './utils'; export const route: Route = { path: '/today/:edition?/:tab?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/line/today', parameters: { edition: 'Edition, see below, Taiwan by default', tab: 'Tag, can be found in URL, `top` by default' }, radar: [ @@ -18,9 +18,9 @@ export const route: Route = { url: 'today.line.me/', description: `Edition - | Taiwan | Thailand | Hong Kong | - | ------ | -------- | --------- | - | tw | th | hk |`, +| Taiwan | Thailand | Hong Kong | +| ------ | -------- | --------- | +| tw | th | hk |`, }; async function handler(ctx) { @@ -40,7 +40,7 @@ async function handler(ctx) { url: tabUrl, }); - const listing = moduleResponse.data.modules.filter((item) => item.source === 'CATEGORY_MOST_VIEW').pop().listings[0]; + const listing = moduleResponse.data.modules.findLast((item) => item.source === 'CATEGORY_MOST_VIEW').listings[0]; title = moduleResponse.data.name; moduleUrl = diff --git a/lib/routes/link3/events.ts b/lib/routes/link3/events.ts new file mode 100644 index 00000000000000..00cfcf1bbf2e31 --- /dev/null +++ b/lib/routes/link3/events.ts @@ -0,0 +1,86 @@ +import { Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/events', + name: 'Link3 Events', + url: 'link3.to', + maintainers: ['cxheng315'], + example: '/link3/events', + categories: ['other'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['link3.to/events'], + target: '/events', + }, + ], + handler, +}; + +async function handler() { + const url = 'https://api.cyberconnect.dev/profile/'; + + const response = await ofetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: { + variables: { + order: 'START_TIME_ASC', + }, + query: ` + query getTrendingEvents($first: Int, $after: String, $order: TrendingEventsRequest_EventOrder, $filter: TrendingEventsRequest_EventFilter) { + trendingEvents(first: $first, after: $after, order: $order, filter: $filter) { + list { + id + info + title + posterUrl + startTimestamp + endTimestamp + organizer { + lightInfo { + displayName + profilePicture + profileHandle + } + } + } + } + } + + `, + }, + }); + + const items = response.data.trendingEvents.list.map((event) => ({ + title: event.title, + link: `https://link3.to/e/${event.id}`, + description: event.info ?? '', + author: event.organizer.lightInfo.displayName, + guid: event.id, + pubDate: parseDate(event.startTimestamp * 1000), + itunes_item_image: event.posterUrl, + itunes_duration: event.endTimestamp - event.startTimestamp, + })); + + return { + title: 'Link3 Events', + link: 'https://link3.to/events', + description: 'Link3 is a Web3 native social platform built on CyberConnect protocol.', + image: 'https://link3.to/logo.svg', + logo: 'https://link3.to/logo.svg', + author: 'Link3', + item: items, + }; +} diff --git a/lib/routes/link3/namespace.ts b/lib/routes/link3/namespace.ts new file mode 100644 index 00000000000000..f8f7a39de30d36 --- /dev/null +++ b/lib/routes/link3/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Link3', + url: 'link3.to', + lang: 'en', +}; diff --git a/lib/routes/link3/profile.ts b/lib/routes/link3/profile.ts new file mode 100644 index 00000000000000..906c810bc7d4d6 --- /dev/null +++ b/lib/routes/link3/profile.ts @@ -0,0 +1,143 @@ +import { Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/profile/:handle', + name: 'Link3 Profile', + url: 'link3.to', + maintainers: ['cxheng315'], + example: '/link3/profile/synfutures_defi', + parameters: { handle: 'Profile handle' }, + categories: ['other'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['link3.to/:handle'], + target: '/:handle', + }, + ], + handler, +}; + +async function handler(ctx) { + const url = 'https://api.cyberconnect.dev/profile/'; + + const handle = ctx.req.param('handle'); + + const response = await ofetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: { + variables: { + handle, + }, + query: ` + query getProfile($id: ID, $handle: String) { + profile(id: $id, handle: $handle) { + status + data { + handle + ... on OrgProfile { + displayName + bio + profilePicture + backgroundPicture + __typename + } + ... on PerProfile { + bio + personalDisplayName: displayName { + displayName + } + personalProfilePicture: profilePicture { + picture + } + personalBackgroundPicture: backgroundPicture { + picture + } + __typename + } + blocks { + ... on Block { + ... on EventBlock { + __typename + events { + id + title + info + posterUrl + startTimestamp + endTimestamp + } + } + } + } + } + } + } + + `, + }, + }); + + const status = response.data.profile.status; + + if (status !== 'SUCCESS') { + return { + title: 'Error', + description: 'Profile not found', + items: [ + { + title: 'Error', + description: 'Profile not found', + link: `https://link3.to/${handle}`, + }, + ], + }; + } + + const profile = response.data.profile.data; + + const items = profile.blocks + .filter((block) => block.__typename === 'EventBlock') + .flatMap((block) => block.events) + .map((event) => ({ + title: event.title, + link: `https://link3.to/e/${event.id}`, + description: event.info ?? '', + author: profile.handle, + guid: event.id, + pubDate: event.startTimestamp ? parseDate(event.startTimestamp * 1000) : null, + itunes_item_image: event.posterUrl, + itunes_duration: event.endTimestamp - event.startTimestamp, + })); + + return { + title: profile.displayName ?? profile.personalDisplayName.displayName, + link: `https://link3.to/${profile.handle}`, + description: profile.bio, + logo: profile.profilePicture ?? profile.personalProfilePicture.picture, + image: profile.profilePicture ?? profile.personalProfilePicture.picture, + author: profile.handle, + item: + items && items.length > 0 + ? items + : [ + { + title: 'No events', + description: 'No events', + link: `https://link3.to/${handle}`, + }, + ], + }; +} diff --git a/lib/routes/linkedin/cn/index.ts b/lib/routes/linkedin/cn/index.ts index 1d5b92d8151493..15916d8c29426d 100644 --- a/lib/routes/linkedin/cn/index.ts +++ b/lib/routes/linkedin/cn/index.ts @@ -21,13 +21,13 @@ export const route: Route = { handler, description: `另外,可以通过添加额外的以下 query 参数来输出满足特定要求的工作职位: - | 参数 | 描述 | 举例 | 默认值 | - | ---------- | ------------------------------------------------- | ------------------------------------------------------- | ------- | - | \`geo\` | geo 编码 | 102890883(中国)、102772228(上海)、103873152(北京) | 空 | - | \`remote\` | 是否只显示远程工作 | \`true/false\` | \`false\` | - | \`location\` | 工作地点 | \`china/shanghai/beijing\` | 空 | - | \`relevant\` | 排序方式 (true: 按相关性排序,false: 按日期排序) | \`true/false\` | \`false\` | - | \`period\` | 发布时间 | \`1/7/30\` | 空 | +| 参数 | 描述 | 举例 | 默认值 | +| ---------- | ------------------------------------------------- | ------------------------------------------------------- | ------- | +| \`geo\` | geo 编码 | 102890883(中国)、102772228(上海)、103873152(北京) | 空 | +| \`remote\` | 是否只显示远程工作 | \`true/false\` | \`false\` | +| \`location\` | 工作地点 | \`china/shanghai/beijing\` | 空 | +| \`relevant\` | 排序方式 (true: 按相关性排序,false: 按日期排序) | \`true/false\` | \`false\` | +| \`period\` | 发布时间 | \`1/7/30\` | 空 | 例如: [\`/linkedin/cn/jobs/Software?location=shanghai&period=1\`](https://rsshub.app/linkedin/cn/jobs/Software?location=shanghai\&period=1): 查找所有在上海的今日发布的所有 Software 工作 diff --git a/lib/routes/linkedin/cn/renderer.ts b/lib/routes/linkedin/cn/renderer.ts index da73c4fec1ab74..11b79942ee52c1 100644 --- a/lib/routes/linkedin/cn/renderer.ts +++ b/lib/routes/linkedin/cn/renderer.ts @@ -169,7 +169,7 @@ const parseAttr = (description) => { q.push(render(e)); } if (p < m.length) { - q.push(m.slice(p, m.length)); + q.push(m.slice(p)); } return q.join(''); }; diff --git a/lib/routes/linkedin/jobs.ts b/lib/routes/linkedin/jobs.ts index 9b87514c37686c..fca537f82e196d 100644 --- a/lib/routes/linkedin/jobs.ts +++ b/lib/routes/linkedin/jobs.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import { EXP_LEVELS, EXP_LEVELS_QUERY_KEY, JOB_TYPES, JOB_TYPES_QUERY_KEY, KEYWORDS_QUERY_KEY, parseJobSearch, parseParamsToSearchParams, parseParamsToString, parseRouteParam } from './utils'; @@ -8,7 +8,8 @@ const JOB_SEARCH_PATH = '/jobs-guest/jobs/api/seeMoreJobPostings/search'; export const route: Route = { path: '/jobs/:job_types/:exp_levels/:keywords?/:routeParams?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.Notifications, example: '/linkedin/jobs/C-P/1/software engineer', parameters: { job_types: "See the following table for details, use '-' as delimiter", @@ -50,32 +51,32 @@ export const route: Route = { handler, description: `#### \`job_types\` list - | Full Time | Part Time | Contractor | All | - | --------- | --------- | ---------- | --- | - | F | P | C | all | +| Full Time | Part Time | Contractor | All | +| --------- | --------- | ---------- | --- | +| F | P | C | all | - #### \`exp_levels\` list +#### \`exp_levels\` list - | Intership | Entry Level | Associate | Mid-Senior Level | Director | All | - | --------- | ----------- | --------- | ---------------- | -------- | --- | - | 1 | 2 | 3 | 4 | 5 | all | +| Intership | Entry Level | Associate | Mid-Senior Level | Director | All | +| --------- | ----------- | --------- | ---------------- | -------- | --- | +| 1 | 2 | 3 | 4 | 5 | all | - #### \`routeParams\` additional query parameters +#### \`routeParams\` additional query parameters - ##### \`f_WT\` list +##### \`f_WT\` list - | Onsite | Remote | Hybrid | - | ------ | ------- | ------ | - | 1 | 2 | 3 | +| Onsite | Remote | Hybrid | +| ------ | ------- | ------ | +| 1 | 2 | 3 | - ##### \`geoId\` +##### \`geoId\` Geographic location ID. You can find this ID in the URL of a LinkedIn job search page that is filtered by location. For example: 91000012 is the ID of East Asia. - ##### \`f_TPR\` +##### \`f_TPR\` Time posted range. Here are some possible values: diff --git a/lib/routes/linkedin/namespace.ts b/lib/routes/linkedin/namespace.ts index 4069ccd7cd50a9..ebf43001afd0b7 100644 --- a/lib/routes/linkedin/namespace.ts +++ b/lib/routes/linkedin/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'LinkedIn 领英', + name: 'LinkedIn', url: 'linkedin.com', + lang: 'en', }; diff --git a/lib/routes/linkedin/utils.ts b/lib/routes/linkedin/utils.ts index 40ef3db80e55f1..a91e390cf63d08 100644 --- a/lib/routes/linkedin/utils.ts +++ b/lib/routes/linkedin/utils.ts @@ -85,7 +85,8 @@ function parseJobSearch(data) { // Parse data const jobs = $('li') - .map((i, elem) => { + .toArray() + .map((elem) => { const elemHtml = $(elem); const link = elemHtml.find('a.base-card__full-link, a.base-card--link')?.attr('href')?.split('?')[0]; const title = elemHtml.find('h3.base-search-card__title')?.text()?.trim(); @@ -94,8 +95,7 @@ function parseJobSearch(data) { const pubDate = elemHtml.find('time')?.attr('datetime'); return new Job(title, link, company, location, pubDate); - }) - .toArray(); + }); return jobs; } diff --git a/lib/routes/linkresearcher/index.ts b/lib/routes/linkresearcher/index.ts index 567144f5eeae45..65da513d2f3c71 100644 --- a/lib/routes/linkresearcher/index.ts +++ b/lib/routes/linkresearcher/index.ts @@ -1,76 +1,138 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import qs from 'query-string'; +import { ViewType, type Data, type DataItem, type Route } from '@/types'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import crypto from 'crypto'; +import type { Context } from 'hono'; +import type { DetailResponse, SearchResultItem } from './types'; +import cache from '@/utils/cache'; +import { getCurrentPath } from '@/utils/helpers'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +const __dirname = getCurrentPath(import.meta.url); +const templatePath = path.join(__dirname, 'templates/bilingual.art'); const baseURL = 'https://www.linkresearcher.com'; +const apiURL = `${baseURL}/api`; export const route: Route = { + name: 'Articles', path: '/:params', - name: 'Unknown', - maintainers: ['yech1990'], + example: '/linkresearcher/category=theses&columns=Nature%20导读&subject=生物', + maintainers: ['y9c', 'KarasuShin'], handler, + view: ViewType.Articles, + categories: ['journal'], + parameters: { + params: { + description: 'search parameters, support `category`, `subject`, `columns`, `query`', + }, + }, + zh: { + name: '文章', + }, + 'zh-TW': { + name: '文章', + }, }; -async function handler(ctx) { - // parse params +async function handler(ctx: Context): Promise<Data> { + const categoryMap = { theses: '论文', information: '新闻', careers: '职业' } as const; const params = ctx.req.param('params'); - const query = qs.parse(params); - - const categoryMap = { theses: '论文', information: '新闻', careers: '职业' }; - const category = query.category; - let title = categoryMap[category]; - - // get XSRF token from main page - const metaURL = `${baseURL}/${category}`; - const metaResponse = await got(metaURL); - const xsrfToken = metaResponse.headers['set-cookie'][0].split(';')[0].split('=')[1]; - - let data = { filters: { status: false } }; - if (query.subject !== undefined && query.columns !== undefined) { - data = { filters: { status: true, subject: query.subject, columns: query.columns } }; - title = `${title}「${query.subject} & ${query.columns}」`; - } else if (query.subject !== undefined && query.columns === undefined) { - data = { filters: { status: true, subject: query.subject } }; - title = `${title}「${query.subject}」`; - } else if (query.subject === undefined && query.columns !== undefined) { - data = { filters: { status: true, columns: query.columns } }; - title = `${title}「${query.columns}」`; + const filters = new URLSearchParams(params); + + const subject = filters.get('subject'); + const columns = filters.get('columns'); + const query = filters.get('query') ?? ''; + const category = filters.get('category') ?? ('theses' as keyof typeof categoryMap); + + if (!(category in categoryMap)) { + throw new InvalidParameterError('Invalid category'); + } + let title = categoryMap[category] as string; + + const token = crypto.randomUUID(); + + const data: { + filters: { + status: boolean; + subject?: string; + columns?: string; + }; + } = { filters: { status: true } }; + + if (subject) { + data.filters.subject = subject; + title = `${title}「${subject}」`; } - data.query = query.query; + + if (columns) { + data.filters.columns = columns; + title = `${title}「${columns}」`; + } + const dataURL = `${baseURL}/api/${category === 'careers' ? 'articles' : category}/search`; - const pageResponse = await got.post(dataURL, { + const pageResponse = await ofetch<{ + hits: SearchResultItem[]; + }>(dataURL, { + method: 'POST', headers: { 'content-type': 'application/json; charset=UTF-8', - 'x-xsrf-token': xsrfToken, - cookie: `XSRF-TOKEN=${xsrfToken}`, + 'x-xsrf-token': token, + cookie: `XSRF-TOKEN=${token}`, }, - searchParams: { + params: { from: 0, size: 20, type: category === 'careers' ? 'CAREER' : 'SEARCH', }, - json: data, + body: { + ...data, + query, + }, }); - const list = pageResponse.data.hits; + const items = await Promise.all( + pageResponse.hits.map((item) => { + const link = `${baseURL}/${category}/${item.id}`; + return cache.tryGet(link, async () => { + const response = await ofetch<DetailResponse>(`${apiURL}/${category === 'theses' ? 'theses' : 'information'}/${item.id}`, { + responseType: 'json', + }); + + const dataItem: DataItem = { + title: response.title, + pubDate: parseDate(response.onlineTime), + link, + image: response.cover, + }; + + dataItem.description = + 'zhTextList' in response && 'enTextList' in response + ? art(templatePath, { + zh: response.zhTextList, + en: response.enTextList, + }) + : response.content; + + if ('paperList' in response) { + const { doi, authors } = response.paperList[0]; + dataItem.doi = doi; + dataItem.author = authors.map((author) => ({ name: author })); + } - const out = list.map((item) => ({ - title: item.title, - description: item.content, - pubDate: parseDate(item.createdAt, 'x'), - link: `${metaURL}/${item.id}`, - guid: `${metaURL}/${item.id}`, - doi: item.identCode === undefined ? '' : item.identCode, - author: item.authors === undefined ? '' : item.authors.join(', '), - })); + return dataItem; + }) as unknown as DataItem; + }) + ); return { title: `领研 | ${title}`, description: '领研是链接华人学者的人才及成果平台。领研为国内外高校、科研机构及科技企业提供科研人才招聘服务,也是青年研究者的职业发展指导及线上培训平台;研究者还可将自己的研究论文上传至领研,与超过五十万华人学者分享工作的最新进展。', - image: 'https://www.linkresearcher.com/assets/images/logo-app.png', + image: `${baseURL}/assets/images/logo-app.png`, link: baseURL, - item: out, + item: items, }; } diff --git a/lib/routes/linkresearcher/namespace.ts b/lib/routes/linkresearcher/namespace.ts index d779329b599bcc..dd99889536c07a 100644 --- a/lib/routes/linkresearcher/namespace.ts +++ b/lib/routes/linkresearcher/namespace.ts @@ -2,5 +2,12 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Link Research', - url: 'linkresearcher', + url: 'www.linkresearcher.com', + lang: 'zh-CN', + zh: { + name: '领研', + }, + 'zh-TW': { + name: '領研', + }, }; diff --git a/lib/routes/linkresearcher/templates/bilingual.art b/lib/routes/linkresearcher/templates/bilingual.art new file mode 100644 index 00000000000000..be7d6c1ba93f73 --- /dev/null +++ b/lib/routes/linkresearcher/templates/bilingual.art @@ -0,0 +1,7 @@ +{{ each en }} +{{ if $index !== 0 }} +<br> +{{ /if }} +<p>{{ $value }}</p> +<p>{{ zh[$index] }}</p> +{{ /each }} diff --git a/lib/routes/linkresearcher/types.ts b/lib/routes/linkresearcher/types.ts new file mode 100644 index 00000000000000..f93aa153a42a40 --- /dev/null +++ b/lib/routes/linkresearcher/types.ts @@ -0,0 +1,103 @@ +interface BaseItem { + id: string; + title: string; + tags: string[]; + onlineTime: number; + cover: string; +} + +export interface InformationItem extends BaseItem { + summary: string; +} + +export interface ThesesItem extends BaseItem { + authors: string[]; + content: string; + journals: string[]; + publishDate: string; + source: { + sourceType: string; + }; + subject: string; + thesisTitle: string; +} + +export interface ArticleItem extends BaseItem { + columns: string[]; + source: { + logo: string; + sourceId: string; + sourceName: string; + sourceType: string; + }; + summary: string; + type: string; +} + +export type SearchResultItem = InformationItem | ThesesItem | ArticleItem; + +export interface ThesesDetailResponse { + columns: string[]; + content: string; + cover: string; + enTextList: string[]; + id: string; + journals: string[]; + link: string; + onlineTime: number; + original: boolean; + paperList: { + authors: string[]; + checkname: string; + doi: string; + id: string; + journal: string; + link: string; + publishDate: string; + subjects: string[]; + summary: string; + title: string; + translateSummary: string; + type: string; + }[]; + relevant: { + timestamp: number; + type: string; + }[]; + source: { + sourceId: string; + sourceName: string; + }; + sourceKey: string; + sourceType: string; + tags: string[]; + template: boolean; + title: string; + userType: number; + zhTextList?: string[]; +} + +export interface InformationDetailResponse { + columns: string[]; + content: string; + cover: string; + id: string; + onlineTime: number; + original: boolean; + relevant: { + timestamp: number; + type: string; + }[]; + source: { + sourceId: string; + sourceName: string; + }; + sourceKey: string; + subject: string; + summary: string; + tags: string[]; + title: string; + type: string; +} + +export type DetailResponse = ThesesDetailResponse | InformationDetailResponse; diff --git a/lib/routes/linovelib/namespace.ts b/lib/routes/linovelib/namespace.ts index 5edd5c38021d51..c8cd3d11e23b09 100644 --- a/lib/routes/linovelib/namespace.ts +++ b/lib/routes/linovelib/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '哔哩轻小说', url: 'linovelib.com', + lang: 'zh-CN', }; diff --git a/lib/routes/liquipedia/namespace.ts b/lib/routes/liquipedia/namespace.ts index efb2f3fada3b15..b5635c0e381767 100644 --- a/lib/routes/liquipedia/namespace.ts +++ b/lib/routes/liquipedia/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Liquipedia', url: 'liquipedia.net', + lang: 'en', }; diff --git a/lib/routes/literotica/namespace.ts b/lib/routes/literotica/namespace.ts index 4d2f30bfc548fe..beb3f74b07cd2c 100644 --- a/lib/routes/literotica/namespace.ts +++ b/lib/routes/literotica/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Literotica', url: 'literotica.com', + lang: 'en', }; diff --git a/lib/routes/liulinblog/itnews.ts b/lib/routes/liulinblog/itnews.ts index bc627efc54c1b9..155ad9060fb773 100644 --- a/lib/routes/liulinblog/itnews.ts +++ b/lib/routes/liulinblog/itnews.ts @@ -9,5 +9,5 @@ export const route: Route = { function handler(ctx) { const { channel } = ctx.req.param(); const redirectTo = `/liulinblog/${channel}`; - ctx.redirect(redirectTo); + ctx.set('redirect', redirectTo); } diff --git a/lib/routes/liulinblog/namespace.ts b/lib/routes/liulinblog/namespace.ts index 7a707e8d45b119..0361ef89e34375 100644 --- a/lib/routes/liulinblog/namespace.ts +++ b/lib/routes/liulinblog/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '木木博客', url: 'liulinblog.com', + lang: 'zh-CN', }; diff --git a/lib/routes/liveuamap/index.ts b/lib/routes/liveuamap/index.ts index cfa1ea48bdcd7b..d79b3d75220462 100644 --- a/lib/routes/liveuamap/index.ts +++ b/lib/routes/liveuamap/index.ts @@ -6,7 +6,7 @@ import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { path: '/:region?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/liveuamap', parameters: { region: 'region 热点地区,默认为`ukraine`,其他选项见liveuamap.com的三级域名' }, features: { diff --git a/lib/routes/liveuamap/namespace.ts b/lib/routes/liveuamap/namespace.ts index 7a8c467d89a65c..a145b538acd0cb 100644 --- a/lib/routes/liveuamap/namespace.ts +++ b/lib/routes/liveuamap/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Live Universal Awareness Map', url: 'liveuamap.com', + lang: 'en', }; diff --git a/lib/routes/lkong/namespace.ts b/lib/routes/lkong/namespace.ts index bbcfaa9dd68f49..abbee84992fb75 100644 --- a/lib/routes/lkong/namespace.ts +++ b/lib/routes/lkong/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '龙空', url: 'lkong.com', + lang: 'zh-CN', }; diff --git a/lib/routes/lmu/jobs.ts b/lib/routes/lmu/jobs.ts new file mode 100644 index 00000000000000..a692727c0047e5 --- /dev/null +++ b/lib/routes/lmu/jobs.ts @@ -0,0 +1,96 @@ +import { Route } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +const apiUrl = 'https://jobs.b-ite.com/api/v1/postings/search'; + +// 辅助函数:根据 value 查找对应的 label +function findLabel(value: string, options: Array<{ value: string; label: string }>): string { + const option = options.find((option) => option.value === value); + return option?.label ?? value; // 如果找不到匹配项,返回 value 本身 +} + +async function handler() { + const { data: response } = await got.post(apiUrl, { + json: { + key: '7d4ebad4ecdfd3e99a89596c85c5e4be21cd9c12', + channel: 0, + locale: 'en', + sort: { + by: 'custom.bereich', + order: 'asc', + }, + origin: 'https://www.lmu.de/en/about-lmu/working-at-lmu/job-portal/academic-staff/', + page: { + offset: 0, + num: 1000, + }, + filter: { + locale: { + in: ['en'], + }, + 'custom.beschaeftigtengruppe': { + in: ['02_wiss'], + }, + }, + }, + headers: { + 'content-type': 'application/json', + 'bite-jobsapi-client': 'v5-20230925-9df79de', + }, + }); + + const jobPostings = response.jobPostings; + const bereichOptions = response.fields['custom.bereich'].options; + const verguetungOptions = response.fields['custom.verguetung'].options; + + const items = jobPostings.map((job) => { + const pubDate = parseDate(job.createdOn, 'YYYY-MM-DDTHH:mm:ssZ'); + + // 获取 Institution 的 label + const institutionLabel = findLabel(job.custom.bereich, bereichOptions); + const RemunerationGroupLabel = findLabel(job.custom.verguetung, verguetungOptions); + + // 渲染模板 + const description = art(path.join(__dirname, 'templates/jobPosting.art'), { + institutionLabel, + RemunerationGroupLabel, + job, + }); + + return { + title: job.title, + link: job.url, + description, + pubDate, + }; + }); + + return { + title: 'LMU Academic Staff Job Openings', + link: 'https://www.lmu.de/en/about-lmu/working-at-lmu/job-portal/academic-staff/', + item: items, + }; +} + +export const route: Route = { + path: '/jobs', + name: 'Job Openings', + url: 'lmu.de', + example: '/lmu/jobs', + maintainers: ['StarDxxx'], + categories: ['university', 'study'], + radar: [ + { + source: ['www.lmu.de/en/about-lmu/working-at-lmu/job-portal/academic-staff/'], + target: '/lmu/jobs', + }, + ], + description: 'RSS feed for LMU academic staff job openings.', + handler, +}; diff --git a/lib/routes/lmu/namespace.ts b/lib/routes/lmu/namespace.ts new file mode 100644 index 00000000000000..a821ff81b29ad8 --- /dev/null +++ b/lib/routes/lmu/namespace.ts @@ -0,0 +1,18 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Ludwig Maximilian University of Munich', + url: 'www.lmu.de', + description: ` +This namespace provides RSS feeds for various sections of the Ludwig Maximilian University of Munich (LMU) website, particularly for job openings in the academic staff section. + +::: tip +For more information about LMU and their job offerings, visit their official website. +::: +`, + + zh: { + name: '慕尼黑大学', + }, + lang: 'de', +}; diff --git a/lib/routes/lmu/templates/jobPosting.art b/lib/routes/lmu/templates/jobPosting.art new file mode 100644 index 00000000000000..b89f75f2cc0b46 --- /dev/null +++ b/lib/routes/lmu/templates/jobPosting.art @@ -0,0 +1,11 @@ +<p><strong>Institution:</strong> {{ institutionLabel }}</p> +<p><strong>Remuneration:</strong> {{ RemunerationGroupLabel }}</p> +<p><strong>Application deadline:</strong> {{ job.endsOn }}</p> +<p><strong>Job Details:</strong></p> +<p><strong>About us:</strong></p> +{{ job.custom.das_sind_wir || '' }}<br> +<p><strong>Your qualifications:</strong></p> +{{ job.custom.das_sind_sie || '' }}<br> +<p><strong>Benefits:</strong></p> +{{ job.custom.das_ist_unser_angebot || '' }}<br> +<p><strong>Contact:</strong> {{ job.custom.kontakt || '' }}</p> diff --git a/lib/routes/lofter/collection.ts b/lib/routes/lofter/collection.ts new file mode 100644 index 00000000000000..fb4bb596c7c5b6 --- /dev/null +++ b/lib/routes/lofter/collection.ts @@ -0,0 +1,87 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import cache from '@/utils/cache'; +import { config } from '@/config'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/collection/:collectionID', + categories: ['social-media', 'popular'], + example: '/lofter/collection/552041', + parameters: { collectionID: 'Lofter collection ID, can be found in the share URL' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Collection', + maintainers: ['SrakhiuMeow'], + handler, +}; + +async function fetchCollection(collectionID, limit, offset = 0) { + const response = await got({ + method: 'post', + url: 'https://api.lofter.com/v1.1/postCollection.api?product=lofter-android-7.6.12', + body: new URLSearchParams({ + collectionid: collectionID, + limit: limit.toString(), + method: 'getCollectionDetail', + offset: offset.toString(), + order: '0', + }), + }); + + if (!response.data.response) { + throw new Error('Collection Not Found'); + } + + const data = response.data.response; + + return { + title: data.collection.name || 'Lofter Collection', + link: data.blogInfo.homePageUrl || 'https://www.lofter.com/', + description: data.collection.description || 'No description provided.', + items: data.items, + } as object; +} + +async function handler(ctx) { + const collectionID = ctx.req.param('collectionID'); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : '50'; + + const response = await cache.tryGet(collectionID, () => fetchCollection(collectionID, Number(limit)), config.cache.routeExpire, false); + + const { title, link, description, items } = response; + + const itemsArray = items.map((item) => ({ + title: item.post.title || item.post.noticeLinkTitle, + link: item.post.blogPageUrl, + description: + JSON.parse(item.post.photoLinks || `[]`) + .map((photo) => { + if (photo.raw?.match(/\/\/nos\.netease\.com\//)) { + photo.raw = `https://${photo.raw.match(/(imglf\d)/)[0]}.lf127.net${photo.raw.match(/\/\/nos\.netease\.com\/imglf\d(.*)/)[1]}`; + } + return `<img src='${photo.raw || photo.orign}'>`; + }) + .join('') + + JSON.parse(item.post.embed ? `[${item.post.embed}]` : `[]`) + .map((video) => `<video src='${video.originUrl}' poster='${video.video_img_url}' controls='controls'></video>`) + .join('') + + item.post.content, + pubDate: parseDate(item.post.publishTime), + author: item.post.blogInfo.blogNickName, + category: item.post.tag.split(','), + })); + + return { + title, + link, + item: itemsArray, + description, + }; +} diff --git a/lib/routes/lofter/namespace.ts b/lib/routes/lofter/namespace.ts index a3d67187fad42c..34f3adaab1b4f0 100644 --- a/lib/routes/lofter/namespace.ts +++ b/lib/routes/lofter/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Lofter', url: 'www.lofter.com', + lang: 'zh-CN', }; diff --git a/lib/routes/lofter/tag.ts b/lib/routes/lofter/tag.ts index 4257c5c822e7e7..257452b3b3e8b7 100644 --- a/lib/routes/lofter/tag.ts +++ b/lib/routes/lofter/tag.ts @@ -6,7 +6,7 @@ import { JSDOM } from 'jsdom'; export const route: Route = { path: '/tag/:name?/:type?', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/lofter/tag/cosplay/date', parameters: { name: 'tag name, such as `名侦探柯南`, `摄影` by default', type: 'ranking type, see below, new by default' }, features: { @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['hoilc', 'nczitzk', 'LucunJi'], handler, description: `| new | date | week | month | total | - | ---- | ---- | ---- | ----- | ----- | - | 最新 | 日榜 | 周榜 | 月榜 | 总榜 |`, +| ---- | ---- | ---- | ----- | ----- | +| 最新 | 日榜 | 周榜 | 月榜 | 总榜 |`, }; async function handler(ctx) { diff --git a/lib/routes/lofter/user.ts b/lib/routes/lofter/user.ts index 65cc415f051356..34d2331bb8bdf8 100644 --- a/lib/routes/lofter/user.ts +++ b/lib/routes/lofter/user.ts @@ -1,13 +1,14 @@ import InvalidParameterError from '@/errors/types/invalid-parameter'; -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; import { isValidHost } from '@/utils/valid-host'; export const route: Route = { path: '/user/:name?', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/lofter/user/i', + view: ViewType.Articles, parameters: { name: 'Lofter user name, can be found in the URL' }, features: { requireConfig: false, @@ -74,7 +75,7 @@ async function handler(ctx) { return { title: `${items[0].author} | LOFTER`, - link: rootUrl, + link: `https://${rootUrl}`, item: items, description: response.data.response.posts[0].post.blogInfo.selfIntro, }; diff --git a/lib/routes/logclub/index.ts b/lib/routes/logclub/index.ts index 45b2d3669e2b3d..0fa828af9c984f 100644 --- a/lib/routes/logclub/index.ts +++ b/lib/routes/logclub/index.ts @@ -109,8 +109,7 @@ async function handler(ctx) { content( content('div.video_info_item, div.lc-infos div') .toArray() - .filter((i) => /\d{4}-\d{2}-\d{2}/.test(content(i).text())) - .pop() + .findLast((i) => /\d{4}-\d{2}-\d{2}/.test(content(i).text())) ) .text() .split(/:/) diff --git a/lib/routes/logclub/namespace.ts b/lib/routes/logclub/namespace.ts index 04bc1e9682e739..62fb89f539929a 100644 --- a/lib/routes/logclub/namespace.ts +++ b/lib/routes/logclub/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '罗戈网', url: 'logclub.com', + lang: 'zh-CN', }; diff --git a/lib/routes/logclub/report.ts b/lib/routes/logclub/report.ts index 6a2e708d828002..a93d2a9c88f368 100644 --- a/lib/routes/logclub/report.ts +++ b/lib/routes/logclub/report.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 罗戈研究出品 | 物流报告 | 绿色双碳报告 | - | ------------ | -------------- | --------------------- | - | Report | IndustryReport | GreenDualCarbonReport |`, +| ------------ | -------------- | --------------------- | +| Report | IndustryReport | GreenDualCarbonReport |`, }; async function handler(ctx) { diff --git a/lib/routes/logonews/index.ts b/lib/routes/logonews/index.ts index bc234e7ca8eea7..b18e2fac7b0985 100644 --- a/lib/routes/logonews/index.ts +++ b/lib/routes/logonews/index.ts @@ -21,9 +21,7 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'logonews.cn/', - url: 'logonews.cn/', description: `如 [中国 - 标志情报局](https://www.logonews.cn/tag/china) 的 URL 为 \`https://www.logonews.cn/tag/china\`,可得路由为 [\`/logonews/tag/china\`](https://rsshub.app/logonews/tag/china)。`, - url: 'logonews.cn/work', }; async function handler(ctx) { diff --git a/lib/routes/logonews/namespace.ts b/lib/routes/logonews/namespace.ts index c526370d146fa0..afbc3ea66c3258 100644 --- a/lib/routes/logonews/namespace.ts +++ b/lib/routes/logonews/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'LogoNews 标志情报局', url: 'logonews.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/logrocket/index.ts b/lib/routes/logrocket/index.ts new file mode 100644 index 00000000000000..8a1f605b983ef5 --- /dev/null +++ b/lib/routes/logrocket/index.ts @@ -0,0 +1,71 @@ +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 +import { load } from 'cheerio'; // 类似 jQuery 的 API HTML 解析器 +import { Route } from '@/types'; +import cache from '@/utils/cache'; +// import { getCurrentPath } from '@/utils/helpers'; +// import { art } from '@/utils/render'; +// import path from 'node:path'; +// const __dirname = getCurrentPath(import.meta.url); +export const route: Route = { + path: '/:type', + categories: ['blog'], + example: '/logrocket/dev', + parameters: { type: 'dev | product-management | ux-design' }, + radar: [ + { + source: ['blog.logrocket.com'], + }, + ], + name: 'blog.logrocket', + maintainers: ['findwei'], + handler, + url: 'blog.logrocket.com/', +}; +async function handler(ctx) { + const type = ctx.req.param('type'); + const link = 'https://blog.logrocket.com/'; + let title = 'Dev'; + if (type === 'product-management') { + title = 'Product Management'; + } else if (type === 'ux-design') { + title = 'UX Design'; + } + const response = await ofetch(`${link}${type}`); + const $ = load(response); + const list = $('div.post-list .post-card') + .toArray() + .map((item) => { + item = $(item); + const a = item.find('a').first(); + const title = item.find('.post-card-title').first(); + return { + title: title.text(), + link: a.attr('href'), + pubDate: parseDate(item.find('.post-card-author-name').next().text().split(' ⋅ ')[0], 'MMM D, YYYY'), + author: item.find('.post-card-author-name').text(), + }; + }); + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + // + $('div.content-max-width .sidebar-container div.code-block').remove(); + item.description = $('div.content-max-width .sidebar-container').html(); + // item.description = art(path.join(__dirname, 'templates/description.art'), { + // // header: $('#post-header').html(), + // description: $('div.content-max-width .the-content-container').remove('.lr-content div.code-block.code-block-77').remove('.lr-content .code-block.code-block-57').html(), + // }); + return item; + }) + ) + ); + return { + title: `logrocket-${title}`, + link, + description: `logrocket-${title}`, + item: items, + }; +} diff --git a/lib/routes/logrocket/namespace.ts b/lib/routes/logrocket/namespace.ts new file mode 100644 index 00000000000000..3a091ddc538e6b --- /dev/null +++ b/lib/routes/logrocket/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'logrocket blog', + url: 'blog.logrocket.com', + lang: 'en', +}; diff --git a/lib/routes/loltw/namespace.ts b/lib/routes/loltw/namespace.ts index f4e202a5676eab..6a9b4af92b28c6 100644 --- a/lib/routes/loltw/namespace.ts +++ b/lib/routes/loltw/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '英雄联盟', url: 'lol.garena.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/loltw/news.ts b/lib/routes/loltw/news.ts index 35e405f2786449..45544dd3074072 100644 --- a/lib/routes/loltw/news.ts +++ b/lib/routes/loltw/news.ts @@ -25,8 +25,8 @@ export const route: Route = { maintainers: ['hoilc'], handler, description: `| 活动 | 资讯 | 系统 | 电竞 | 版本资讯 | 战棋资讯 | - | ----- | ---- | ------ | ------ | -------- | -------- | - | event | info | system | esport | patch | TFTpatch |`, +| ----- | ---- | ------ | ------ | -------- | -------- | +| event | info | system | esport | patch | TFTpatch |`, }; async function handler(ctx) { diff --git a/lib/routes/loongarch/namespace.ts b/lib/routes/loongarch/namespace.ts index 016800db50f566..88dd9f18382a2c 100644 --- a/lib/routes/loongarch/namespace.ts +++ b/lib/routes/loongarch/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'LA UOSC社区', url: 'loongarch.org', + lang: 'zh-CN', }; diff --git a/lib/routes/loongarch/post.ts b/lib/routes/loongarch/post.ts index ae24849e2e664c..a0e1ad32abceef 100644 --- a/lib/routes/loongarch/post.ts +++ b/lib/routes/loongarch/post.ts @@ -1,5 +1,6 @@ import { parseDate } from '@/utils/parse-date'; import got from '@/utils/got'; +import { Route } from '@/types'; export const route: Route = { path: '/post/:type?', diff --git a/lib/routes/lorientlejour/index.ts b/lib/routes/lorientlejour/index.ts new file mode 100644 index 00000000000000..8a9bb62422cb97 --- /dev/null +++ b/lib/routes/lorientlejour/index.ts @@ -0,0 +1,186 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { config } from '@/config'; +import { FetchError } from 'ofetch'; +import { load } from 'cheerio'; +import { art } from '@/utils/render'; +import { getCurrentPath } from '@/utils/helpers'; +import path from 'node:path'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; + +const __dirname = getCurrentPath(import.meta.url); +const key = '3d5_f6A(S$G_FD=2S(Dr6%7BW_h37@rE'; + +export const route: Route = { + path: '/:category?', + categories: ['traditional-media'], + example: '/lorientlejour/977-lebanon', + parameters: { + category: 'Category from the last segment of the URL of the corresponding site, see below for more information, /977-Lebanon by default', + }, + features: { + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + requireConfig: [ + { + name: 'LORIENTLEJOUR_USERNAME', + optional: true, + description: `L'Orient-Le Jour/L'Orient Today Email or Username`, + }, + { + name: 'LORIENTLEJOUR_PASSWORD', + optional: true, + description: `L'Orient-Le Jour/L'Orient Today Password`, + }, + { + name: 'LORIENTLEJOUR_TOKEN', + optional: true, + description: `To obtain a token, log into L'Orient-Le Jour/L'Orient Today App and inspect the connection request to find the token parameter from the request URL`, + }, + ], + }, + name: 'Category', + maintainers: ['quiniapiezoelectricity'], + handler, + description: ` ::: tip +For example, the path for the sites https://today.lorientlejour.com/section/977-lebanon and https://www.lorientlejour.com/rubrique/1-liban would be /lorientlejour/977-lebanon and /lorientlejour/1-liban respectively. +Multiple categories seperated by '|' is also supported, e.g. /lorientlejour/977-lebanon|1-liban. +:::`, + radar: [ + { + source: ['www.lorientlejour.com/*/:category'], + target: '/:category', + }, + { + source: ['www.lorientlejour.com'], + target: '/1-Liban', + }, + { + source: ['today.lorientlejour.com/*/:category'], + target: '/:category', + }, + { + source: ['today.lorientlejour.com'], + target: '/977-Lebanon', + }, + ], +}; + +async function viewCategory(category: string) { + const url = `https://www.lorientlejour.com/cmsapi/categories.php?key=${key}&action=view&categoryId=${category}`; + const response = await cache.tryGet( + url, + async () => + await got({ + method: 'get', + url, + }), + config.cache.routeExpire, + false + ); + return response.data.data[0]; +} + +async function handler(ctx) { + const categoryId = (ctx.req.param('category') ?? '977-Lebanon').split('|').map((item) => item.match(/^(\d+)/i)[0] ?? item); + const limit = ctx.req.query('limit') ?? 25; + + let token; + const cacheIn = await cache.get('lorientlejour:token'); + if (cacheIn) { + token = cacheIn; + } else if (config.lorientlejour.token) { + token = config.lorientlejour.token; + cache.set('lorientlejour:token', token); + } else if (config.lorientlejour.username && config.lorientlejour.password) { + const loginUrl = `https://www.lorientlejour.com/cmsapi/visitors.php?key=${key}&action=login&loginName=${config.lorientlejour.username}&password=${config.lorientlejour.password}`; + const loginResponse = await got(loginUrl); + token = loginResponse.data.data.token; + cache.set('lorientlejour:token', token); + } + + if (token) { + try { + await got(`https://www.lorientlejour.com/cmsapi/visitors.php?key=${key}&action=login_token&token=${token}`); + } catch (error) { + if (error instanceof FetchError && error.statusCode === 403) { + await cache.set('lorientlejour:token', ''); + } + throw error; + } + } + + let title = `L'Orient Le Jour/L'Orient Today`; + let description = ''; + let link = 'https://www.lorientlejour.com'; + let language = ''; + + if (categoryId.length === 1) { + const categoryInfo = await viewCategory(categoryId[0]); + if (categoryInfo.typeId.locale) { + language = categoryInfo.typeId.locale; + } else { + language = categoryInfo.typeId.name === 'English' ? 'en-US' : 'fr-FR'; + } + title = language === 'en-US' ? `L'Orient Today - ${categoryInfo.name}` : `L'Orient Le Jour - ${categoryInfo.name}`; + description = categoryInfo.description; + link = categoryInfo.url; + } + + const subcategories = await Promise.all( + categoryId.map(async (id) => { + // get all subcategories of the selected category + const contents = await viewCategory(id); + return contents.children.map((child) => child.id); + }) + ); + const categoriesParam = [...new Set([...categoryId, ...subcategories.flat()])]; + // merge all subcategories with the selected categories to get all contents of the selected category and its subcategories + + let url = `https://www.lorientlejour.com/cmsapi/content.php?text=clean&key=${key}&action=search&category=${encodeURIComponent(JSON.stringify(categoriesParam))}&limit=${limit}&text=false&page=1`; + if (token) { + url = url + `&token=${token}`; + } + const response = await got(url); + const items = response.data.data.map((item) => { + item.link = item.url; + item.author = item.authors.map((author) => author.name).join(', '); + item.pubDate = timezone(parseDate(item.firstPublished), +3); + item.updated = timezone(parseDate(item.lastUpdate), +3); + item.category = item.categories.map((itemCategory) => itemCategory.name); + const contents = item.contents; + const $ = load(contents); + const article = $('html'); + article.find('.inline-embeded-article').remove(); + article.find('.relatedArticles').remove(); + if (item.inline_attachments) { + article.find('.inlineImage').each(function () { + const inlineImageSrc = $(this).attr('src'); + const inlineAttachment = item.inline_attachments.find((inlineAttachment) => inlineAttachment.url === inlineImageSrc); + if (inlineAttachment && inlineAttachment.description) { + $(this).wrap('<figure></figure>'); + $(this).after(`<figcaption>${inlineAttachment.description}</figcaption>`); + } + }); + } + item.description = art(path.join(__dirname, 'templates/description.art'), { + summary: item.summary, + attachments: item.attachments, + article: article.html(), + }); + return item; + }); + + return { + title, + description, + language, + link, + item: items, + }; +} diff --git a/lib/routes/lorientlejour/namespace.ts b/lib/routes/lorientlejour/namespace.ts new file mode 100644 index 00000000000000..64bef17d007203 --- /dev/null +++ b/lib/routes/lorientlejour/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: `L'Orient-Le Jour/L'Orient Today`, + url: 'lorientlejour.com', + description: `RSS feed for the Lebanon-based French-language newspaper L'Orient-Le Jour and its English edition L'Orient Today`, + lang: 'fr', +}; diff --git a/lib/routes/lorientlejour/templates/description.art b/lib/routes/lorientlejour/templates/description.art new file mode 100644 index 00000000000000..3fd2ae9c462747 --- /dev/null +++ b/lib/routes/lorientlejour/templates/description.art @@ -0,0 +1,18 @@ +{{ if summary }} + <blockquote> {{@ summary }} </blockquote> +{{ /if }} +{{ if attachments}} + {{ each attachments }} + {{if $value.url }} + <figure> + <img src="{{ $value.url }}"> + {{ if $value.description }} + <figcaption>{{ $value.description }}</figcaption> + {{ /if }} + </figure> + {{ /if }} + {{ /each }} +{{ /if }} +{{ if article }} + {{@ article }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/lovelive-anime/namespace.ts b/lib/routes/lovelive-anime/namespace.ts index 74afe629c14818..43036eb507b27d 100644 --- a/lib/routes/lovelive-anime/namespace.ts +++ b/lib/routes/lovelive-anime/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'lovelive-anime', + name: 'Love Live! Official Website', url: 'www.lovelive-anime.jp', + lang: 'ja', }; diff --git a/lib/routes/lovelive-anime/news.ts b/lib/routes/lovelive-anime/news.ts index a5fe06a03893e9..45af080bf812db 100644 --- a/lib/routes/lovelive-anime/news.ts +++ b/lib/routes/lovelive-anime/news.ts @@ -7,13 +7,20 @@ import got from '@/utils/got'; import { load } from 'cheerio'; import path from 'node:path'; import { art } from '@/utils/render'; +import ofetch from '@/utils/ofetch'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; const renderDescription = (desc) => art(path.join(__dirname, 'templates/description.art'), desc); export const route: Route = { - path: '/news/:option?', + path: '/news/:abbr?/:category?/:option?', categories: ['anime'], example: '/lovelive-anime/news', - parameters: { option: 'Crawl full text when `option` is `detail`.' }, + parameters: { + abbr: 'The path to the Love Live series of sub-projects on the official website is detailed in the table below, `abbr` is `detail` when crawling the full text', + category: 'The official website lists the Topics category, `category` is `detail` when crawling the full text, other categories see the following table for details', + option: 'Crawl full text when `option` is `detail`.', + }, features: { requireConfig: false, requirePuppeteer: false, @@ -24,45 +31,68 @@ export const route: Route = { }, radar: [ { - source: ['www.lovelive-anime.jp/', 'www.lovelive-anime.jp/news'], + source: ['www.lovelive-anime.jp/', 'www.lovelive-anime.jp/news/'], target: '/news', }, ], - name: 'Love Live! Official Website Latest NEWS', - maintainers: ['axojhf'], + name: 'News', + maintainers: ['axojhf', 'zhaoweizhong'], handler, url: 'www.lovelive-anime.jp/', + description: `| Sub-project Name | All Projects | Lovelive! | Lovelive! Sunshine!! | Lovelive! Nijigasaki High School Idol Club | Lovelive! Superstar!! | 蓮ノ空女学院 | 幻日のヨハネ | ラブライブ!スクールアイドルミュージカル | +| -------------------------------- | -------------- | ----------- | -------------------- | ------------------------------------------ | --------------------- | ------------ | ------------ | ---------------------------------------- | +| \`abbr\`parameter | <u>*No parameter*</u> | lovelive | sunshine | nijigasaki | superstar | hasunosora | yohane | musical | + +| Category Name | 全てのニュース | 音楽商品 | アニメ映像商品 | キャスト映像商品 | 劇場 | アニメ放送 / 配信 | キャスト配信 / ラジオ | ライブ / イベント | ブック | グッズ | ゲーム | メディア | ご当地情報 | キャンペーン | その他 | +| ------------------- | --------------------- | -------- | -------------- | ---------------- | ------- | ----------------- | --------------------- | ----------------- | ------ | ------ | ------ | -------- | ---------- | ------ | ------------ | +| \`category\`parameter | <u>*No parameter*</u> | music | anime_movie | cast_movie | theater | onair | radio | event | books | goods | game | media | local | campaign | other |`, }; async function handler(ctx) { - const rootUrl = 'https://www.lovelive-anime.jp/news/'; + const abbr = ctx.req.param('abbr'); + const category = ctx.req.param('category'); + const option = ctx.req.param('option'); - const response = await got(rootUrl); + const isDetail = abbr === 'detail' || category === 'detail' || option === 'detail'; + let series = ''; + let subcategory = ''; + + if (abbr && abbr !== 'detail') { + series = abbr; + if (category && category !== 'detail') { + subcategory = category; + } + } - const $ = load(response.data); + const limit = 20; + + let url = `https://www.lovelive-anime.jp/common/api/article_list.php?site=jp&ip=lovelive&limit=${limit}&data=`; + const params: { category: string[]; series?: string[]; subcategory?: string[] } = { category: ['NEWS'] }; + if (series) { + params.series = [series]; + } + if (subcategory) { + params.subcategory = [subcategory]; + } + url += encodeURIComponent(JSON.stringify(params)); - const pageFace = $('div.c-card.p-colum__box') - .map((_, item) => { - item = $(item); + const data = await ofetch(url); - return { - link: item.find('a.c-card__head').attr('href'), - pubDate: item.find('span.c-card__date').text(), - title: item.find('div.c-card__title').text(), - // description: `${item.find('div.c-card__title').text()}<br><img src="${item.find('a.c-card__head > div > figure > img').attr('src')}">` - description: renderDescription({ - title: item.find('div.c-card__title').text(), - imglink: item.find('a.c-card__head > div > figure > img').attr('src'), - }), - }; - }) - .get(); + const articles = data.data.article_list.map((item) => ({ + title: item.title, + link: item.url, + description: renderDescription({ + imglink: 'https://www.lovelive-anime.jp' + item.thumbnail, + }), + pubDate: timezone(parseDate(item.dspdate), +9), + category: item.categories.subcategory.map((category) => category.name), + })); - let items = pageFace; + let items = articles; - if (ctx.req.param('option') === 'detail') { + if (isDetail) { items = await Promise.all( - pageFace.map((item) => + articles.map((item) => cache.tryGet(item.link, async () => { const detailResp = await got(item.link); const $ = load(detailResp.data); diff --git a/lib/routes/lovelive-anime/schedules.ts b/lib/routes/lovelive-anime/schedules.ts index 0e8b5fce24a452..046111b9ab03df 100644 --- a/lib/routes/lovelive-anime/schedules.ts +++ b/lib/routes/lovelive-anime/schedules.ts @@ -2,27 +2,39 @@ 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 path from 'node:path'; import { art } from '@/utils/render'; const renderDescription = (desc) => art(path.join(__dirname, 'templates/scheduleDesc.art'), desc); import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; +dayjs.extend(utc); +dayjs.extend(timezone); export const route: Route = { path: '/schedules/:serie?/:category?', - name: 'Unknown', - maintainers: [], + parameters: { + serie: 'Love Live! Series sub-projects abbreviation, see the following table', + category: 'The official website lists the categories, see the following table for details', + }, + name: 'Schedule', + example: '/lovelive-anime/schedules', + categories: ['anime'], + maintainers: ['axojhf'], handler, + description: `| Sub-project Name (not full name) | 全シリーズ | Lovelive! | Lovelive! Sunshine!! | Lovelive! Nijigasaki High School Idol Club | Lovelive! Superstar!! | ラブライブ!スクールアイドルミュージカル | +| -------------------------------- | ----------------------- | ---------- | -------------------- | ------------------------------------------ | --------------------- | ---------------------------------------- | +| \`serie\` parameter | *No parameter* or \`all\` | \`lovelive\` | \`sunshine\` | \`nijigasaki\` | \`superstar\` | \`musical\` | + +| Category Name | 全て | ライブ | イベント | 生配信 | +| -------------------- | ----------------------- | ------ | -------- | --------- | +| \`category\` parameter | *No parameter* or \`all\` | \`live\` | \`event\` | \`haishin\` |`, }; async function handler(ctx) { - dayjs.extend(utc); - dayjs.extend(timezone); - const serie = ctx.req.param('serie'); - const category = ctx.req.param('category'); - const rootUrl = `https://www.lovelive-anime.jp/common/api/calendar_list.php`; + const { serie = 'all', category = 'all' } = ctx.req.param(); + const rootUrl = 'https://www.lovelive-anime.jp/common/api/calendar_list.php'; const nowTime = dayjs(); const dataPara = { year: nowTime.year(), @@ -34,9 +46,9 @@ async function handler(ctx) { if (category && 'all' !== category) { dataPara.category = [category]; } - const response = await got(`${rootUrl}?site=jp&ip=lovelive&data=${JSON.stringify(dataPara)}`); + const response = await ofetch(`${rootUrl}?site=jp&ip=lovelive&data=${encodeURIComponent(JSON.stringify(dataPara))}`); - const items = response.data.data.schedule_list.map((item) => { + const items = response.data.schedule_list.map((item) => { const link = item.url.select_url; const title = item.title; const category = item.categories.name; @@ -49,7 +61,6 @@ async function handler(ctx) { title, category, description: renderDescription({ - title, desc: item.event_dspdate, startTime: eventStartDate, endTime: eventEndDate, diff --git a/lib/routes/lovelive-anime/templates/description.art b/lib/routes/lovelive-anime/templates/description.art index 6f85ee13fa6a36..41ce31a1107cdb 100644 --- a/lib/routes/lovelive-anime/templates/description.art +++ b/lib/routes/lovelive-anime/templates/description.art @@ -1 +1 @@ -{{title}}<br><img src="{{imglink}}"> \ No newline at end of file +<img src="{{imglink}}"> diff --git a/lib/routes/lovelive-anime/templates/scheduleDesc.art b/lib/routes/lovelive-anime/templates/scheduleDesc.art index e5ec0b2b569638..d512a9b37b2b44 100644 --- a/lib/routes/lovelive-anime/templates/scheduleDesc.art +++ b/lib/routes/lovelive-anime/templates/scheduleDesc.art @@ -1,3 +1,2 @@ -<h1>{{title}}</h1><br> <b>{{startTime}}</b>   ~   <b>{{endTime}}</b><br><br> {{desc}} diff --git a/lib/routes/lovelive-anime/topics.ts b/lib/routes/lovelive-anime/topics.ts index 67de791dba732d..1b91f2ce194a1a 100644 --- a/lib/routes/lovelive-anime/topics.ts +++ b/lib/routes/lovelive-anime/topics.ts @@ -3,11 +3,12 @@ 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 path from 'node:path'; import { art } from '@/utils/render'; import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; const renderDescription = (desc) => art(path.join(__dirname, 'templates/description.art'), desc); export const route: Route = { @@ -27,23 +28,38 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, - name: 'Love Live Official Website Categories Topics', + name: 'Categories Topics', maintainers: ['axojhf'], handler, description: `| Sub-project Name (not full name) | Lovelive! | Lovelive! Sunshine!! | Lovelive! Nijigasaki High School Idol Club | Lovelive! Superstar!! | 幻日のヨハネ | ラブライブ!スクールアイドルミュージカル | - | -------------------------------- | ----------- | -------------------- | ------------------------------------------ | --------------------- | ------------ | ---------------------------------------- | - | \`abbr\`parameter | otonokizaka | uranohoshi | nijigasaki | yuigaoka | yohane | musical | +| -------------------------------- | ----------- | -------------------- | ------------------------------------------ | --------------------- | ------------ | ---------------------------------------- | +| \`abbr\`parameter | otonokizaka | uranohoshi | nijigasaki | yuigaoka | yohane | musical | - | Category Name | 全てのニュース | 音楽商品 | アニメ映像商品 | キャスト映像商品 | 劇場 | アニメ放送 / 配信 | キャスト配信 / ラジオ | ライブ / イベント | ブック | グッズ | ゲーム | メディア | ご当地情報 | その他 | キャンペーン | - | ------------------- | --------------------- | -------- | -------------- | ---------------- | ------- | ----------------- | --------------------- | ----------------- | ------ | ------ | ------ | -------- | ---------- | ------ | ------------ | - | \`category\`parameter | <u>*No parameter*</u> | music | anime\_movie | cast\_movie | theater | onair | radio | event | books | goods | game | media | local | other | campaign |`, +| Category Name | 全てのニュース | 音楽商品 | アニメ映像商品 | キャスト映像商品 | 劇場 | アニメ放送 / 配信 | キャスト配信 / ラジオ | ライブ / イベント | ブック | グッズ | ゲーム | メディア | ご当地情報 | その他 | キャンペーン | +| ------------------- | --------------------- | -------- | -------------- | ---------------- | ------- | ----------------- | --------------------- | ----------------- | ------ | ------ | ------ | -------- | ---------- | ------ | ------------ | +| \`category\`parameter | <u>*No parameter*</u> | music | anime\_movie | cast\_movie | theater | onair | radio | event | books | goods | game | media | local | other | campaign |`, }; async function handler(ctx) { - const abbr = ctx.req.param('abbr'); - const rootUrl = `https://www.lovelive-anime.jp/${abbr}`; - const topicsUrlPart = 'yuigaoka' === abbr ? 'topics/' : 'topics.php'; - const baseUrl = `${rootUrl}/${'yuigaoka' === abbr ? 'topics/' : ''}`; + const { abbr, category = '', option } = ctx.req.param(); + let rootUrl: string; + switch (abbr) { + case 'musical': + rootUrl = 'https://www.lovelive-anime.jp/special/musical'; + break; + default: + rootUrl = `https://www.lovelive-anime.jp/${abbr}`; + break; + } + const topicsTable = { + otonokizaka: 'topics.php', + uranohoshi: 'topics.php', + nijigasaki: 'topics.php', + yuigaoka: 'topics/', + hasunosora: 'news/', + musical: 'topics.php', + }; + const baseUrl = `${rootUrl}/${topicsTable[abbr]}`; const abbrDetail = { otonokizaka: 'ラブライブ!', uranohoshi: 'サンシャイン!!', @@ -51,50 +67,85 @@ async function handler(ctx) { yuigaoka: 'スーパースター!!', }; - const url = Object.hasOwn(ctx.params, 'category') && ctx.req.param('category') !== 'detail' ? `${rootUrl}/${topicsUrlPart}?cat=${ctx.req.param('category')}` : `${rootUrl}/${topicsUrlPart}`; + const url = category !== '' && category !== 'detail' ? `${baseUrl}?cat=${category}` : baseUrl; - const response = await got(url); + const response = await ofetch(url); - const $ = load(response.data); + const $ = load(response); const categoryName = 'uranohoshi' === abbr ? $('div.llbox > p').text() : $('div.category_title > h2').text(); - let items = $('ul.listbox > li') - .map((_, item) => { - item = $(item); + const newsList = 'hasunosora' === abbr ? $('.list__content > ul > li').toArray() : $('ul.listbox > li').toArray(); + let items; - const link = `${baseUrl}${item.find('div > a').attr('href')}`; - const pubDate = parseDate(item.find('a > p.date').text(), 'YYYY/MM/DD'); - const title = item.find('a > p.title').text(); - const category = item.find('a > p.category').text(); - const imglink = `${baseUrl}${ - item - .find('a > img') - .attr('style') - .match(/background-image:url\((.*)\)/)[1] - }`; + switch (abbr) { + case 'hasunosora': + items = newsList.map((item) => { + item = $(item); + const link = `${rootUrl}/news/${item.find('a').attr('href')}`; + const pubDate = timezone(parseDate(item.find('.list--date').text(), 'YYYY.MM.DD'), +9); + const title = item.find('.list--text').text(); + const category = item.find('.list--category').text(); - return { - link, - pubDate, - title, - category, - description: renderDescription({ + return { + link, + pubDate, title, - imglink, - }), - }; - }) - .get(); + category, + description: title, + }; + }); + break; + default: + items = newsList.map((item) => { + item = $(item); + let link: string; + switch (abbr) { + case 'yuigaoka': + link = `${baseUrl}${item.find('div > a').attr('href')}`; + break; + default: + link = `${rootUrl}/${item.find('div > a').attr('href')}`; + break; + } + const pubDate = timezone(parseDate(item.find('a > p.date').text(), 'YYYY/MM/DD'), +9); + const title = item.find('a > p.title').text(); + const category = item.find('a > p.category').text(); + const imglink = `${rootUrl}/${ + item + .find('a > img') + .attr('style') + .match(/background-image:url\((.*)\)/)[1] + }`; - if (ctx.req.param('option') === 'detail' || ctx.req.param('category') === 'detail') { + return { + link, + pubDate, + title, + category, + description: renderDescription({ + imglink, + }), + }; + }); + break; + } + + if (option === 'detail' || category === 'detail') { items = await Promise.all( items.map((item) => cache.tryGet(item.link, async () => { - const detailResp = await got(item.link); - const $ = load(detailResp.data); - - const content = $('div.p-page__detail.p-article'); + const detailResp = await ofetch(item.link); + const $ = load(detailResp); + let content; + switch (abbr) { + case 'hasunosora': + content = $('div.detail__content'); + break; + default: + content = $('div.p-page__detail.p-article'); + break; + } for (const v of content.find('img')) { v.attribs.src = `${baseUrl}${v.attribs.src}`; } diff --git a/lib/routes/lrepacks/index.ts b/lib/routes/lrepacks/index.ts new file mode 100644 index 00000000000000..e31a5ddca65b7e --- /dev/null +++ b/lib/routes/lrepacks/index.ts @@ -0,0 +1,201 @@ +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'; + +export const handler = async (ctx) => { + const { category = '' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 12; + + const rootUrl = 'https://lrepacks.net'; + const currentUrl = new URL(category, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('#main article') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const title = item.find('h3.entry-title').text(); + const description = art(path.join(__dirname, 'templates/description.art'), { + intro: item.find('div.entry-content').text(), + }); + + return { + title, + description, + link: item.find('h3.entry-title a').prop('href'), + category: item + .find('span.cat-links') + .toArray() + .map((c) => $(c).text()), + language, + }; + }); + + 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[type="application/ld+json"]').first().text())['@graph']?.[0] ?? undefined; + + $$('div.entry-content a.highslide[href]').each((_, el) => { + el = $$(el); + + el.parent().replaceWith( + art(path.join(__dirname, 'templates/description.art'), { + images: [ + { + src: el.prop('href'), + alt: el.prop('title'), + }, + ], + }) + ); + }); + + const title = $$('h2.entry-title').text(); + const description = + item.description + + art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.entry-content').html(), + }); + const image = $$('meta[property="og:image"]').prop('content'); + + item.title = title; + item.description = description; + item.pubDate = data ? parseDate(data.datePublished) : undefined; + item.author = data?.author?.name ?? undefined; + item.content = { + html: description, + text: $$('div.entry-content').text(), + }; + item.image = image; + item.banner = image; + item.updated = data ? parseDate(data.dateModified) : undefined; + item.language = language; + + return item; + }) + ) + ); + + const image = new URL($('div.logo img').prop('src'), rootUrl).href; + + return { + title: $('title').text(), + description: $('meta[name="description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $('meta[property="og:site_name"]').prop('content'), + language, + }; +}; + +export const route: Route = { + path: '/:category?', + name: 'REPACK скачать', + url: 'lrepacks.net', + maintainers: ['nczitzk'], + handler, + example: '/lrepacks', + parameters: { category: 'Category, Homepage by default' }, + description: `::: tip + If you subscribe to [Системные программы](https://lrepacks.net/repaki-sistemnyh-programm/),where the URL is \`https://lrepacks.net/repaki-sistemnyh-programm/\`, extract the part \`https://lrepacks.net/\` to the end, which is \`repaki-sistemnyh-programm\`, and use it as the parameter to fill in. Therefore, the route will be [\`/lrepacks/repaki-sistemnyh-programm\`](https://rsshub.app/lrepacks/repaki-sistemnyh-programm). + +| Category | ID | +| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| [Новые репаки на сегодня](https://lrepacks.net/novye-repaki-elchupacabra/) | [novye-repaki-elchupacabra](https://rsshub.app/lrepacks/novye-repaki-elchupacabra) | +| [Системные программы](https://lrepacks.net/repaki-sistemnyh-programm/) | [repaki-sistemnyh-programm](https://rsshub.app/lrepacks/repaki-sistemnyh-programm) | +| [Программы для графики](https://lrepacks.net/repaki-programm-dlya-grafiki/) | [repaki-programm-dlya-grafiki](https://rsshub.app/lrepacks/repaki-programm-dlya-grafiki) | +| [Программы для интернета](https://lrepacks.net/repaki-programm-dlya-interneta/) | [repaki-programm-dlya-interneta](https://rsshub.app/lrepacks/repaki-programm-dlya-interneta) | +| [Мультимедиа программы](https://lrepacks.net/repaki-multimedia-programm/) | [repaki-multimedia-programm](https://rsshub.app/lrepacks/repaki-multimedia-programm) | +| [Программы для офиса](https://lrepacks.net/repaki-programm-dlya-ofisa/) | [repaki-programm-dlya-ofisa](https://rsshub.app/lrepacks/repaki-programm-dlya-ofisa) | +| [Разные программы](https://lrepacks.net/repaki-raznyh-programm/) | [repaki-raznyh-programm](https://rsshub.app/lrepacks/repaki-raznyh-programm) | +| [Системные библиотеки](https://lrepacks.net/sistemnye-biblioteki/) | [sistemnye-biblioteki](https://rsshub.app/lrepacks/sistemnye-biblioteki) | +| [Важная информация](https://lrepacks.net/informaciya/) | [informaciya](https://rsshub.app/lrepacks/informaciya) | +:::`, + categories: ['program-update'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['lrepacks.net/:category'], + target: (params) => { + const category = params.category; + + return `/lrepacks${category ? `/${category}` : ''}`; + }, + }, + { + title: 'Новые репаки на сегодня', + source: ['lrepacks.net/novye-repaki-elchupacabra/'], + target: '/novye-repaki-elchupacabra', + }, + { + title: 'Системные программы', + source: ['lrepacks.net/repaki-sistemnyh-programm/'], + target: '/repaki-sistemnyh-programm', + }, + { + title: 'Программы для графики', + source: ['lrepacks.net/repaki-programm-dlya-grafiki/'], + target: '/repaki-programm-dlya-grafiki', + }, + { + title: 'Программы для интернета', + source: ['lrepacks.net/repaki-programm-dlya-interneta/'], + target: '/repaki-programm-dlya-interneta', + }, + { + title: 'Мультимедиа программы', + source: ['lrepacks.net/repaki-multimedia-programm/'], + target: '/repaki-multimedia-programm', + }, + { + title: 'Программы для офиса', + source: ['lrepacks.net/repaki-programm-dlya-ofisa/'], + target: '/repaki-programm-dlya-ofisa', + }, + { + title: 'Разные программы', + source: ['lrepacks.net/repaki-raznyh-programm/'], + target: '/repaki-raznyh-programm', + }, + { + title: 'Системные библиотеки', + source: ['lrepacks.net/sistemnye-biblioteki/'], + target: '/sistemnye-biblioteki', + }, + { + title: 'Важная информация', + source: ['lrepacks.net/informaciya/'], + target: '/informaciya', + }, + ], +}; diff --git a/lib/routes/lrepacks/namespace.ts b/lib/routes/lrepacks/namespace.ts new file mode 100644 index 00000000000000..c2776640629df1 --- /dev/null +++ b/lib/routes/lrepacks/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'REPACK скачать', + url: 'lrepacks.net', + categories: ['program-update'], + description: '', + lang: 'ru', +}; diff --git a/lib/routes/lrepacks/templates/description.art b/lib/routes/lrepacks/templates/description.art new file mode 100644 index 00000000000000..249654e7e618a4 --- /dev/null +++ b/lib/routes/lrepacks/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} + <figure> + <img + {{ if image.alt }} + alt="{{ image.alt }}" + {{ /if }} + src="{{ image.src }}"> + </figure> + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} + <blockquote>{{ intro }}</blockquote> +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/lsnu/jiaowc/tzgg.ts b/lib/routes/lsnu/jiaowc/tzgg.ts index bf618bb142ce42..ed38e6bf6879af 100644 --- a/lib/routes/lsnu/jiaowc/tzgg.ts +++ b/lib/routes/lsnu/jiaowc/tzgg.ts @@ -27,8 +27,8 @@ export const route: Route = { handler, url: 'lsnu.edu.cn/', description: `| 实践教学科 | 教育运行科 | 教研教改科 | 学籍管理科 | 考试科 | 教材建设管理科 | - | ---------- | ---------- | ---------- | ---------- | ------ | -------------- | - | sjjxk | jxyxk | jyjgk | xjglk | ksk | jcjsglk |`, +| ---------- | ---------- | ---------- | ---------- | ------ | -------------- | +| sjjxk | jxyxk | jyjgk | xjglk | ksk | jcjsglk |`, }; async function handler(ctx) { diff --git a/lib/routes/lsnu/namespace.ts b/lib/routes/lsnu/namespace.ts index b5615d228f70bb..9ec8ff41b4db90 100644 --- a/lib/routes/lsnu/namespace.ts +++ b/lib/routes/lsnu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '乐山师范学院', url: 'lsnu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ltaaa/article.ts b/lib/routes/ltaaa/article.ts new file mode 100644 index 00000000000000..510bd4c0d07710 --- /dev/null +++ b/lib/routes/ltaaa/article.ts @@ -0,0 +1,180 @@ +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<Data> => { + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'https://www.ltaaa.cn'; + const targetUrl: string = new URL('article', baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + items = $('ul.wlist li') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio<Element> = $(el); + + const $aEl: Cheerio<Element> = $el.find('div.li-title a'); + + const title: string = $aEl.text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + intro: $el.find('div.dbody p').first().text(), + }); + const pubDateStr: string | undefined = $el.find('i.icon-time').next().text().trim(); + const linkUrl: string | undefined = $aEl.attr('href'); + const authorEls: Element[] = $el.find('i.icon-user').parent().find('a').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $authorEl: Cheerio<Element> = $(authorEl); + + return { + name: $authorEl.text(), + url: new URL($authorEl.attr('href') ?? '', baseUrl).href, + avatar: undefined, + }; + }); + const image: string | undefined = $el.find('div.li-thumb img').attr('src'); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + author: authors, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.post-title').text(); + + const pubDateStr: string | undefined = $$('i.icon-time').next().text().trim(); + const categoryEls: Element[] = $$('span.keywords a').toArray(); + const categories: string[] = [...new Set(categoryEls.map((el) => $$(el).text()).filter(Boolean))]; + const authorEls: Element[] = $$('i.icon-user').first().nextAll('a').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $$authorEl: Cheerio<Element> = $$(authorEl); + + return { + name: $$authorEl.text(), + url: new URL($$authorEl.attr('href') ?? '', baseUrl).href, + avatar: undefined, + }; + }); + const upDatedStr: string | undefined = pubDateStr; + + $$('div.post-tip').each((_, el) => { + const $$el: Cheerio<Element> = $$(el); + const content: string = $$el.html() ?? ''; + + if (content) { + $$el.replaceWith(`<h1>${content}</h1>`); + } + }); + $$('div.post-param, div.post-title, div.post-keywords').remove(); + $$('div.attitude, div.clear').remove(); + + const description: string = art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.post-body').html(), + }); + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const title: string = $('title').text(); + + return { + title, + description: $('meta[name="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: new URL('static/home/images/logo.png', baseUrl).href, + author: title.split(/-/).pop(), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/article', + name: '网站翻译', + url: 'www.ltaaa.cn', + maintainers: ['nczitzk'], + handler, + example: '/ltaaa/article', + parameters: undefined, + description: undefined, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.ltaaa.cn/article'], + target: '/article', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/ltaaa/namespace.ts b/lib/routes/ltaaa/namespace.ts new file mode 100644 index 00000000000000..e019aeab3aa915 --- /dev/null +++ b/lib/routes/ltaaa/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '龙腾网', + url: 'ltaaa.cn', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/ltaaa/templates/description.art b/lib/routes/ltaaa/templates/description.art new file mode 100644 index 00000000000000..57498ab45a9d86 --- /dev/null +++ b/lib/routes/ltaaa/templates/description.art @@ -0,0 +1,7 @@ +{{ if intro }} + <blockquote>{{ intro }}</blockquote> +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/luma/index.ts b/lib/routes/luma/index.ts new file mode 100644 index 00000000000000..795e34c9818ba1 --- /dev/null +++ b/lib/routes/luma/index.ts @@ -0,0 +1,92 @@ +import { Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/:url', + name: 'Events', + url: 'lu.ma', + maintainers: ['cxheng315'], + example: '/luma/yieldnest', + categories: ['other'], + parameters: { + url: 'LuMa URL', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['lu.ma/:url'], + target: '/:url', + }, + ], + handler, +}; + +async function handler(ctx) { + const endpoint = 'https://api.lu.ma/url?url=' + ctx.req.param('url'); + + const response = await ofetch(endpoint); + + let items; + + switch (response.kind) { + case 'calendar': + items = response.data.featured_items.map((item) => ({ + title: item.event.name, + link: 'https://lu.ma/' + item.event.url, + author: item.hosts ? item.hosts.map((host) => host.name).join(', ') : '', + guid: item.event.api_id, + pubDate: parseDate(item.event.start_at), + itunes_item_image: item.event.cover_url, + itunes_duration: (new Date(item.event.end_at).getTime() - new Date(item.event.start_at).getTime()) / 1000, + })); + break; + case 'event': + items = [ + { + title: response.data.event.name, + link: 'https://lu.ma/' + response.data.event.url, + author: response.data.hosts ? response.data.hosts.map((host) => host.name).join(', ') : '', + guid: response.data.event.api_id, + pubDate: parseDate(response.data.event.start_at), + itunes_item_image: response.data.event.cover_url, + itunes_duration: (new Date(response.data.event.end_at).getTime() - new Date(response.data.event.start_at).getTime()) / 1000, + }, + ]; + break; + case 'discover-place': + items = response.data.events.map((item) => ({ + title: item.event.name, + link: 'https://lu.ma/' + item.event.url, + author: item.hosts ? item.hosts.map((host) => host.name).join(', ') : '', + guid: item.event.api_id, + pubDate: parseDate(item.event.start_at), + itunes_item_image: item.event.cover_url, + itunes_duration: (new Date(item.event.end_at).getTime() - new Date(item.event.start_at).getTime()) / 1000, + })); + break; + default: + items = [ + { + title: 'Not Found', + link: 'Not Found', + }, + ]; + break; + } + + return { + title: response.data.calendar ? response.data.calendar.name : response.data.place.name, + description: response.data.place ? response.data.place.description : '', + link: 'https://lu.ma/' + ctx.req.param('url'), + image: response.data.calendar ? response.data.calendar.cover_url : response.data.place.cover_url, + item: items, + }; +} diff --git a/lib/routes/luma/namespace.ts b/lib/routes/luma/namespace.ts new file mode 100644 index 00000000000000..36ce30b89960d3 --- /dev/null +++ b/lib/routes/luma/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'LuMa', + url: 'lu.ma', + lang: 'zh-CN', +}; diff --git a/lib/routes/luogu/daily.ts b/lib/routes/luogu/daily.ts index b48affaab1c139..09be4c19bc3de5 100644 --- a/lib/routes/luogu/daily.ts +++ b/lib/routes/luogu/daily.ts @@ -23,7 +23,7 @@ export const route: Route = { }, ], name: '日报', - maintainers: ['LogicJake ', 'prnake ', 'nczitzk'], + maintainers: ['LogicJake', 'prnake', 'nczitzk'], handler, url: 'luogu.com.cn/discuss/47327', }; diff --git a/lib/routes/luogu/namespace.ts b/lib/routes/luogu/namespace.ts index b5232799bfec88..2c7fbc6e3ae664 100644 --- a/lib/routes/luogu/namespace.ts +++ b/lib/routes/luogu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '洛谷', url: 'luogu.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/luogu/user-article.ts b/lib/routes/luogu/user-article.ts new file mode 100644 index 00000000000000..401d59b03443b7 --- /dev/null +++ b/lib/routes/luogu/user-article.ts @@ -0,0 +1,75 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import { getUserInfoFromUID } from './utils'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/user/article/:uid', + categories: ['programming'], + example: '/luogu/user/article/1', + parameters: { name: '用户 UID' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['luogu.com/user/:uid'], + }, + { + source: ['luogu.com.cn/user/:uid'], + }, + ], + name: '用户文章', + maintainers: ['TonyRL'], + handler, +}; + +const baseUrl = 'https://www.luogu.com'; + +async function handler(ctx) { + const { uid } = ctx.req.param(); + + const userInfo = await getUserInfoFromUID(uid); + const response = await ofetch(`${baseUrl}/api/article/find`, { + query: { + user: uid, + page: 1, + ascending: false, + }, + }); + + const posts = response.articles.result.map((item) => ({ + title: item.title, + link: `${baseUrl}/article/${item.lid}`, + author: item.author.name, + pubDate: parseDate(item.time, 'X'), + description: item.content, + })); + + const item = await Promise.all( + posts.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = cheerio.load(response); + item.description = $('div#rendered-preview').html(); + + return item; + }) + ) + ); + + return { + title: `${userInfo.name} 的个人中心 - 洛谷 | 计算机科学教育新生态`, + description: userInfo.description, + link: `${baseUrl}/user/${uid}#article`, + image: userInfo.avatar, + item, + }; +} diff --git a/lib/routes/luogu/user-blog.ts b/lib/routes/luogu/user-blog.ts index f38d4cc6b0fb2c..51538cb4535410 100644 --- a/lib/routes/luogu/user-blog.ts +++ b/lib/routes/luogu/user-blog.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'; export const route: Route = { @@ -17,12 +17,15 @@ export const route: Route = { supportScihub: false, }, radar: [ + { + source: ['luogu.com/blog/:name'], + }, { source: ['luogu.com.cn/blog/:name'], }, ], name: '用户博客', - maintainers: [], + maintainers: ['ftiasch'], handler, }; @@ -33,8 +36,8 @@ async function handler(ctx) { // Fetch the uid & title const { uid: blogUid, title: blogTitle } = await cache.tryGet(blogBaseUrl, async () => { - const rsp = await got(blogBaseUrl); - const $ = load(rsp.data); + const rsp = await ofetch(blogBaseUrl); + const $ = load(rsp); const uid = $("meta[name='blog-uid']").attr('content'); const name = $("meta[name='blog-name']").attr('content'); return { @@ -43,7 +46,7 @@ async function handler(ctx) { }; }); - const posts = (await got(`https://www.luogu.com.cn/api/blog/lists?uid=${blogUid}`).json()).data.result.map((r) => ({ + const posts = (await ofetch(`https://www.luogu.com.cn/api/blog/lists?uid=${blogUid}`)).data.result.map((r) => ({ title: r.title, link: `${blogBaseUrl}${r.identifier}`, pubDate: new Date(r.postTime * 1000), @@ -53,8 +56,8 @@ async function handler(ctx) { const item = await Promise.all( posts.map((post) => cache.tryGet(post.link, async () => { - const rsp = await got(post.link); - const $ = load(rsp.data); + const rsp = await ofetch(post.link); + const $ = load(rsp); return { title: post.title, link: post.link, diff --git a/lib/routes/luogu/user-feed.ts b/lib/routes/luogu/user-feed.ts index 34c50ce04afbb4..bde1cd462776c5 100644 --- a/lib/routes/luogu/user-feed.ts +++ b/lib/routes/luogu/user-feed.ts @@ -1,8 +1,8 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; import MarkdownIt from 'markdown-it'; +import { getUserInfoFromUID } from './utils'; const md = MarkdownIt(); export const route: Route = { @@ -19,6 +19,9 @@ export const route: Route = { supportScihub: false, }, radar: [ + { + source: ['luogu.com/user/:uid'], + }, { source: ['luogu.com.cn/user/:uid'], }, @@ -29,27 +32,23 @@ export const route: Route = { }; async function handler(ctx) { - const getUsernameFromUID = (uid) => - cache.tryGet('luogu:username:' + uid, async () => { - const { data } = await got(`https://www.luogu.com.cn/user/${uid}?_contentOnly=1`); - return data.currentData.user.name; - }); - const uid = ctx.req.param('uid'); - const name = await getUsernameFromUID(uid); + const userInfo = await getUserInfoFromUID(uid); const { data: response } = await got(`https://www.luogu.com.cn/api/feed/list?user=${uid}`); const data = response.feeds.result; return { - title: `${name} 的洛谷动态`, + title: `${userInfo.name} 的洛谷动态`, + description: userInfo.description, + image: userInfo.avatar, link: `https://www.luogu.com.cn/user/${uid}#activity`, allowEmpty: true, item: data.map((item) => ({ title: item.content, description: md.render(item.content), pubDate: parseDate(item.time, 'X'), - author: name, + author: userInfo.name, link: `https://www.luogu.com.cn/user/${uid}#activity`, guid: item.id, })), diff --git a/lib/routes/luogu/utils.ts b/lib/routes/luogu/utils.ts new file mode 100644 index 00000000000000..bf28c669f4758a --- /dev/null +++ b/lib/routes/luogu/utils.ts @@ -0,0 +1,17 @@ +import cache from '@/utils/cache'; +import ofetch from '@/utils/got'; + +export const getUserInfoFromUID = (uid) => + cache.tryGet('luogu:username:' + uid, async () => { + const data = await ofetch(`https://www.luogu.com/user/${uid}`, { + query: { + _contentOnly: 1, + }, + }); + + return { + name: data.data.currentData.user.name, + description: data.data.currentData.user.slogan, + avatar: data.data.currentData.user.avatar, + }; + }) as Promise<{ name: string; description: string; avatar: string }>; diff --git a/lib/routes/luolei/index.ts b/lib/routes/luolei/index.ts index 2bfaa3e40e39d6..a8fced16a8f9e8 100644 --- a/lib/routes/luolei/index.ts +++ b/lib/routes/luolei/index.ts @@ -39,8 +39,7 @@ export const handler = async (ctx) => { const language = $('html').prop('lang'); const themeEl = $('link[rel="modulepreload"]') .toArray() - .filter((l) => /theme\.\w+\.js$/.test($(l).prop('href'))) - .pop(); + .findLast((l) => /theme\..*\.js$/.test($(l).prop('href'))); const themeUrl = themeEl ? new URL($(themeEl).prop('href'), rootUrl).href : undefined; const { data: themeResponse } = await got(themeUrl); @@ -49,7 +48,12 @@ export const handler = async (ctx) => { .match(/{"title":".*?"string":".*?"}}/g) .slice(0, limit) .map((item) => { - item = JSON.parse(item.replaceAll('\\\\"', '\\"').replaceAll('\\\\n', '').replaceAll('\\`', '`')); + item = JSON.parse( + item + .replaceAll(String.raw`\\"`, String.raw`\"`) + .replaceAll(String.raw`\\n`, '') + .replaceAll('\\`', '`') + ); const $$ = unblurImages(load(item.excerpt)); diff --git a/lib/routes/luolei/namespace.ts b/lib/routes/luolei/namespace.ts index 2707ad28416973..376a772d295b08 100644 --- a/lib/routes/luolei/namespace.ts +++ b/lib/routes/luolei/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: 'luolei.org', categories: ['blog'], description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/luxiangdong/namespace.ts b/lib/routes/luxiangdong/namespace.ts index 3fe40c9415657a..dfd397406be3cd 100644 --- a/lib/routes/luxiangdong/namespace.ts +++ b/lib/routes/luxiangdong/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '土猛的员外', url: 'luxiangdong.com', + lang: 'zh-CN', }; diff --git a/lib/routes/lvv2/namespace.ts b/lib/routes/lvv2/namespace.ts index 03a960193f7605..3f79969384f171 100644 --- a/lib/routes/lvv2/namespace.ts +++ b/lib/routes/lvv2/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'LVV2', url: 'lvv2.com', + lang: 'zh-CN', }; diff --git a/lib/routes/lvv2/news.ts b/lib/routes/lvv2/news.ts index 9328eee16b7f74..ab142ade15bcb2 100644 --- a/lib/routes/lvv2/news.ts +++ b/lib/routes/lvv2/news.ts @@ -46,12 +46,12 @@ export const route: Route = { maintainers: ['Fatpandac'], handler, description: `| 热门 | 最新 | 得分 | 24 小时榜 | - | :------: | :------: | :--------: | :-----------: | - | sort-hot | sort-new | sort-score | sort-realtime | +| :------: | :------: | :--------: | :-----------: | +| sort-hot | sort-new | sort-score | sort-realtime | - | 排序方式 | 一小时内 | 一天内 | 一个周内 | 一个月内 | - | :------: | :------: | :----: | :------: | :------: | - | | t-hour | t-day | t-week | t-month |`, +| 排序方式 | 一小时内 | 一天内 | 一个周内 | 一个月内 | +| :------: | :------: | :----: | :------: | :------: | +| | t-hour | t-day | t-week | t-month |`, }; async function handler(ctx) { diff --git a/lib/routes/lvv2/top.ts b/lib/routes/lvv2/top.ts index fd42254408892c..647c13721f9429 100644 --- a/lib/routes/lvv2/top.ts +++ b/lib/routes/lvv2/top.ts @@ -46,12 +46,12 @@ export const route: Route = { maintainers: ['Fatpandac'], handler, description: `| 热门 | 最新 | 得分 | 24 小时榜 | - | :------: | :------: | :--------: | :-----------: | - | sort-hot | sort-new | sort-score | sort-realtime | +| :------: | :------: | :--------: | :-----------: | +| sort-hot | sort-new | sort-score | sort-realtime | - | 排序方式 | 一小时内 | 一天内 | 一个周内 | 一个月内 | - | :------: | :------: | :----: | :------: | :------: | - | | t-hour | t-day | t-week | t-month |`, +| 排序方式 | 一小时内 | 一天内 | 一个周内 | 一个月内 | +| :------: | :------: | :----: | :------: | :------: | +| | t-hour | t-day | t-week | t-month |`, }; async function handler(ctx) { diff --git a/lib/routes/lxixsxa/jsonp-helper.ts b/lib/routes/lxixsxa/jsonp-helper.ts index d2e8ddfa55c8ef..81d3e1eddc522b 100644 --- a/lib/routes/lxixsxa/jsonp-helper.ts +++ b/lib/routes/lxixsxa/jsonp-helper.ts @@ -5,7 +5,7 @@ function parseJSONP(jsonpData) { let jsonString = jsonpData.substring(startPos + 1, endPos + 1); // remove escaped single quotes since they are not valid json - jsonString = jsonString.replaceAll("\\'", "'"); + jsonString = jsonString.replaceAll(String.raw`\'`, "'"); return JSON.parse(jsonString); } catch (error_) { diff --git a/lib/routes/lxixsxa/namespace.ts b/lib/routes/lxixsxa/namespace.ts index 2739f1854232bb..e02ed8d39c4daf 100644 --- a/lib/routes/lxixsxa/namespace.ts +++ b/lib/routes/lxixsxa/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'LiSA', url: 'www.sonymusic.co.jp', + lang: 'ja', }; diff --git a/lib/routes/m-78/namespace.ts b/lib/routes/m-78/namespace.ts new file mode 100644 index 00000000000000..1b1074074f5fcc --- /dev/null +++ b/lib/routes/m-78/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '円谷ステーション', + url: 'm-78.jp', + lang: 'ja', +}; diff --git a/lib/routes/m-78/news.ts b/lib/routes/m-78/news.ts new file mode 100644 index 00000000000000..5eed7b6b84f1d4 --- /dev/null +++ b/lib/routes/m-78/news.ts @@ -0,0 +1,115 @@ +import { type Data, type Route, ViewType } from '@/types'; +import ofetch from '@/utils/ofetch'; +import type { Context } from 'hono'; +import type { Post } from './types'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +export const route: Route = { + name: 'ニュース', + categories: ['anime'], + path: '/news/:category?', + example: '/m-78/news', + radar: [ + { + source: ['m-78.jp/news'], + target: '/news', + }, + { + source: ['m-78.jp/news/category/:category'], + target: '/news/:category', + }, + ], + parameters: { + category: { + description: 'news category', + default: 'news', + options: [ + { + value: 'news', + label: 'ニュース', + }, + { + value: 'streaming', + label: '動画配信', + }, + { + value: 'event', + label: 'イベント', + }, + { + value: 'onair', + label: '放送', + }, + { + value: 'broadcast', + label: '放送/配信', + }, + { + value: 'goods', + label: 'グッズ', + }, + { + value: 'ultraman-cardgame', + label: 'ウルトラマン カードゲーム', + }, + { + value: 'shop', + label: 'ショップ', + }, + { + value: 'blu-ray_dvd', + label: 'Blu-ray・DVD', + }, + { + value: 'digital', + label: 'デジタル', + }, + ], + }, + }, + handler, + maintainers: ['KarasuShin'], + features: { + supportRadar: true, + }, + view: ViewType.Articles, +}; + +async function handler(ctx: Context): Promise<Data> { + const rootUrl = 'https://m-78.jp'; + const cateAPIUrl = `${rootUrl}/wp-json/wp/v2/categories`; + const postsAPIUrl = `${rootUrl}/wp-json/wp/v2/posts`; + const category = ctx.req.param('category') ?? 'news'; + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')!, 10) : 20; + + const categories = await ofetch(`${cateAPIUrl}?slug=${category}`); + if (categories.length === 0) { + throw new InvalidParameterError('Category not found'); + } + + const { id: categoryId, link: categoryLink, name: categoryName } = categories[0]; + + const posts = await ofetch<Post[]>(`${postsAPIUrl}?categories=${categoryId}&per_page=${limit}`); + return { + title: `${categoryName} | ニュース`, + link: categoryLink, + item: posts.map((post) => { + const $ = load(post.content.rendered, null, false); + $('#ez-toc-container').remove(); + $('img').each((_, img) => { + if (/wp-content\/uploads/.test(img.attribs.src)) { + img.attribs.src = img.attribs.src.replace(/(-\d+x\d+)/, ''); + } + }); + return { + title: post.title.rendered, + link: post.link, + description: $.html(), + pubDate: parseDate(post.date_gmt), + updated: parseDate(post.modified_gmt), + }; + }), + }; +} diff --git a/lib/routes/m-78/types.ts b/lib/routes/m-78/types.ts new file mode 100644 index 00000000000000..ff84da8ae61683 --- /dev/null +++ b/lib/routes/m-78/types.ts @@ -0,0 +1,13 @@ +export interface Post { + id: number; + content: { + rendered: string; + }; + date_gmt: string; + modified_gmt: string; + link: string; + categories: number[]; + title: { + rendered: string; + }; +} diff --git a/lib/routes/m4/namespace.ts b/lib/routes/m4/namespace.ts index b3de9bdf3dfd0a..e5810f3a64ac4e 100644 --- a/lib/routes/m4/namespace.ts +++ b/lib/routes/m4/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '四月网', url: 'news.m4.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/maccms/index.ts b/lib/routes/maccms/index.ts new file mode 100644 index 00000000000000..8dddd1410996c7 --- /dev/null +++ b/lib/routes/maccms/index.ts @@ -0,0 +1,72 @@ +import { Result, Vod } from '@/routes/maccms/type'; +import { DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import path from 'node:path'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import timezone from '@/utils/timezone'; +import { getCurrentPath } from '@/utils/helpers'; + +const render = (vod: Vod, link: string) => art(path.join(getCurrentPath(import.meta.url), 'templates', 'vod.art'), { vod, link }); + +export const route: Route = { + path: '/:domain/:type?/:size?', + categories: ['multimedia'], + example: '/maccms/moduzy.net/2', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + parameters: { + domain: '采集站域名,可选值如下表', + type: '类别ID,不同采集站点有不同的类别规则和ID,默认为 0,代表全部类别', + size: '每次获取的数据条数,上限 100 条,默认 30 条', + }, + name: '最新资源', + maintainers: ['hualiong'], + description: ` +::: tip +每个采集站提供的影视类别ID是不同的,即参数中的 \`type\` 是不同的。**可以先访问一次站点提供的采集接口,然后从返回结果中的 \`class\` 字段中的 \`type_id\`获取相应的类别ID** +::: + +| 站名 | 域名 | 站名 | 域名 | 站名 | 域名 | +| ------------------- | ------------------------------------------------ | ---------------- | -------------------------------------------------- | -------------- | ----------------------------------------------- | +| 魔都资源网 | [moduzy.net](https://moduzy.net) | 华为吧影视资源站 | [hw8.live](https://hw8.live) | 360 资源站 | [360zy.com](https://360zy.com) | +| jkun 爱坤联盟资源网 | [ikunzyapi.com](https://ikunzyapi.com) | 奥斯卡资源站 | [aosikazy.com](https://aosikazy.com) | 飞速资源采集网 | [www.feisuzyapi.com](http://www.feisuzyapi.com) | +| 森林资源网 | [slapibf.com](https://slapibf.com) | 天空资源采集网 | [api.tiankongapi.com](https://api.tiankongapi.com) | 百度云资源 | [api.apibdzy.com](https://api.apibdzy.com) | +| 红牛资源站 | [www.hongniuzy2.com](https://www.hongniuzy2.com) | 乐视资源网 | [leshiapi.com](https://leshiapi.com) | 暴风资源 | [bfzyapi.com](https://bfzyapi.com) |`, + handler: async (ctx) => { + const { domain, type = '0', size = '30' } = ctx.req.param(); + if (!list.has(domain)) { + throw new Error('非法域名!'); + } + + const res = await ofetch<Result>(`https://${domain}/api.php/provide/vod`, { + parseResponse: JSON.parse, + query: { ac: 'detail', t: type, pagesize: Number.parseInt(size) > 100 ? 100 : size }, + }); + + const items: DataItem[] = res.list.map((each) => ({ + title: each.vod_name, + image: each.vod_pic, + link: `https://${domain}/vod/${each.vod_id}/`, + guid: each.vod_play_url?.match(/https:\/\/.+?\.m3u8/g)?.slice(-1)[0], + pubDate: timezone(parseDate(each.vod_time, 'YYYY-MM-DD HH:mm:ss'), +8), + category: [each.type_name, ...each.vod_class!.split(',')], + description: render(each, `https://${domain}/vod/${each.vod_id}/`) + each.vod_content, + })); + + return { + title: `最新${type !== '0' && items.length ? items[0].category![0] : '资源'} - ${domain}`, + link: `https://${domain}`, + allowEmpty: true, + item: items, + }; + }, +}; + +const list = new Set(['moduzy.net', 'hw8.live', '360zy.com', 'ikunzyapi.com', 'aosikazy.com', 'www.feisuzyapi.com', 'slapibf.com', 'api.tiankongapi.com', 'api.apibdzy.com', 'www.hongniuzy2.com', 'leshiapi.com', 'bfzyapi.com']); diff --git a/lib/routes/maccms/namespace.ts b/lib/routes/maccms/namespace.ts new file mode 100644 index 00000000000000..61fc9c85428b3f --- /dev/null +++ b/lib/routes/maccms/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '通用影视采集站视频采集接口路由', + description: ` +::: tip +该路由适用于各大影视采集站对外提供的统一CMS视频采集接口,API 类似于 \`https://网站域名/api.php/provide/vod\` +:::`, + lang: 'zh-CN', +}; diff --git a/lib/routes/maccms/templates/vod.art b/lib/routes/maccms/templates/vod.art new file mode 100644 index 00000000000000..cea9457f9dfc46 --- /dev/null +++ b/lib/routes/maccms/templates/vod.art @@ -0,0 +1,12 @@ +<a href="{{ link }}" title="进入官网查看"><img src="{{ vod.vod_pic }}" alt="{{ vod.vod_name }}" /></a> +<h1>{{ vod.vod_name }} <span style="font-size: 0.6em; color: darkred;">{{ vod.vod_remarks }}</span></h1> +<p><b>别名:</b>{{ vod.vod_sub }}</p> +<p><b>导演:</b>{{ vod.vod_director }}</p> +<p><b>主演:</b>{{ vod.vod_actor }}</p> +<p><b>类型:</b>{{ vod.vod_class }}</p> +<p><b>年份:</b>{{ vod.vod_year }}</p> +<p><b>地区:</b>{{ vod.vod_area }}</p> +<p><b>开播时间:</b>{{ vod.vod_pubdate }}</p> +<p><b>更新时间:</b>{{ vod.vod_time }}</p> +<p><b>资源主页:</b><a href="{{ link }}">{{ link }}</a></p> +<h3>剧情介绍</h3> \ No newline at end of file diff --git a/lib/routes/maccms/type.ts b/lib/routes/maccms/type.ts new file mode 100644 index 00000000000000..092e38ad54472a --- /dev/null +++ b/lib/routes/maccms/type.ts @@ -0,0 +1,102 @@ +export type Result = { + code: number; + msg: string; + page: string; + pagecount: number; + limit: string; + total: number; + list: Array<Vod>; + class?: Array<Class>; +}; + +export type Class = { + type_id: number; + type_pid: number; + type_name: string; +}; + +export type Vod = { + vod_id: number; + vod_name: string; + type_id: number; + type_name: string; + vod_en: string; + vod_time: string; + vod_remarks: string; + vod_play_from: string; + type_id_1?: number; + group_id?: number; + vod_sub?: string; + vod_status?: number; + vod_letter?: string; + vod_color?: string; + vod_tag?: string; + vod_class?: string; + vod_pic?: string; + vod_pic_thumb?: string; + vod_pic_slide?: string; + vod_pic_screenshot?: string; + vod_actor?: string; + vod_director?: string; + vod_writer?: string; + vod_behind?: string; + vod_blurb?: string; + vod_pubdate?: string; + vod_total?: number; + vod_serial?: string; + vod_tv?: string; + vod_weekday?: string; + vod_area?: string; + vod_lang?: string; + vod_year?: string; + vod_version?: string; + vod_state?: string; + vod_author?: string; + vod_jumpurl?: string; + vod_tpl?: string; + vod_tpl_play?: string; + vod_tpl_down?: string; + vod_isend?: number; + vod_lock?: number; + vod_level?: number; + vod_copyright?: number; + vod_points?: number; + vod_points_play?: number; + vod_points_down?: number; + vod_hits?: number; + vod_hits_day?: number; + vod_hits_week?: number; + vod_hits_month?: number; + vod_duration?: string; + vod_up?: number; + vod_down?: number; + vod_score?: string; + vod_score_all?: number; + vod_score_num?: number; + vod_time_add?: number; + vod_time_hits?: number; + vod_time_make?: number; + vod_trysee?: number; + vod_douban_id?: number; + vod_douban_score?: string; + vod_reurl?: string; + vod_rel_vod?: string; + vod_rel_art?: string; + vod_pwd?: string; + vod_pwd_url?: string; + vod_pwd_play?: string; + vod_pwd_play_url?: string; + vod_pwd_down?: string; + vod_pwd_down_url?: string; + vod_content?: string; + vod_play_server?: string; + vod_play_note?: string; + vod_play_url?: string; + vod_down_from?: string; + vod_down_server?: string; + vod_down_note?: string; + vod_down_url?: string; + vod_plot?: number; + vod_plot_name?: string; + vod_plot_detail?: string; +}; diff --git a/lib/routes/macfilos/blog.ts b/lib/routes/macfilos/blog.ts index f79c4e530652d7..f1391f72104387 100644 --- a/lib/routes/macfilos/blog.ts +++ b/lib/routes/macfilos/blog.ts @@ -6,7 +6,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/blog', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/macfilos/blog', parameters: {}, features: { diff --git a/lib/routes/macfilos/namespace.ts b/lib/routes/macfilos/namespace.ts index 1cecc1c53fde27..763180298d471c 100644 --- a/lib/routes/macfilos/namespace.ts +++ b/lib/routes/macfilos/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Macfilos', url: 'macfilos.com', + lang: 'en', }; diff --git a/lib/routes/macmenubar/namespace.ts b/lib/routes/macmenubar/namespace.ts index 4c6a16e0aa643a..ddc5c673e293f0 100644 --- a/lib/routes/macmenubar/namespace.ts +++ b/lib/routes/macmenubar/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MacMenuBar', url: 'macmenubar.com', + lang: 'en', }; diff --git a/lib/routes/macupdate/app.ts b/lib/routes/macupdate/app.ts index 7aa22dd6b28064..c7da81eecb6564 100644 --- a/lib/routes/macupdate/app.ts +++ b/lib/routes/macupdate/app.ts @@ -1,5 +1,5 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; @@ -31,7 +31,7 @@ async function handler(ctx) { const baseUrl = 'https://www.macupdate.com'; const link = `${baseUrl}/app/mac/${appId}${appSlug ? `/${appSlug}` : ''}`; - const { data: response } = await got(link); + const response = await ofetch(link); const $ = load(response); const nextData = JSON.parse($('#__NEXT_DATA__').text()); @@ -39,7 +39,7 @@ async function handler(ctx) { const { asPath, appData: { data: appData }, - } = nextData.props.initialProps.pageProps; + } = nextData.props.pageProps; const item = { title: `${appData.title} ${appData.version}`, @@ -51,10 +51,6 @@ async function handler(ctx) { author: appData.developer.name, }; - ctx.set('json', { - pageProps: nextData.props.initialProps.pageProps, - }); - return { title: appData.title, description: appData.description, diff --git a/lib/routes/macupdate/namespace.ts b/lib/routes/macupdate/namespace.ts index fcaf54d1a87f47..65a18319dcf2c2 100644 --- a/lib/routes/macupdate/namespace.ts +++ b/lib/routes/macupdate/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MacUpdate', url: 'macupdate.com', + lang: 'en', }; diff --git a/lib/routes/magazinelib/namespace.ts b/lib/routes/magazinelib/namespace.ts index 67c7dab0183436..1c0f77115bf4e1 100644 --- a/lib/routes/magazinelib/namespace.ts +++ b/lib/routes/magazinelib/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MagazineLib', url: 'magazinelib.com', + lang: 'en', }; diff --git a/lib/routes/magnumphotos/magazine.ts b/lib/routes/magnumphotos/magazine.ts index 6ca90ef7c8d6fe..3afaee83025080 100644 --- a/lib/routes/magnumphotos/magazine.ts +++ b/lib/routes/magnumphotos/magazine.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import parser from '@/utils/rss-parser'; import ofetch from '@/utils/ofetch'; @@ -6,7 +6,8 @@ import { load } from 'cheerio'; const host = 'https://www.magnumphotos.com'; export const route: Route = { path: '/magazine', - categories: ['picture'], + categories: ['picture', 'popular'], + view: ViewType.Pictures, example: '/magnumphotos/magazine', parameters: {}, features: { diff --git a/lib/routes/magnumphotos/namespace.ts b/lib/routes/magnumphotos/namespace.ts index 36e2f12075e8f9..feb8889e105b05 100644 --- a/lib/routes/magnumphotos/namespace.ts +++ b/lib/routes/magnumphotos/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Magnum Photos', url: 'magnumphotos.com', + lang: 'en', }; diff --git a/lib/routes/mail/namespace.ts b/lib/routes/mail/namespace.ts index ca70bee64dd89b..26ef26c1f9f164 100644 --- a/lib/routes/mail/namespace.ts +++ b/lib/routes/mail/namespace.ts @@ -2,4 +2,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Email', + lang: 'en', }; diff --git a/lib/routes/malaysiakini/index.ts b/lib/routes/malaysiakini/index.ts new file mode 100644 index 00000000000000..666de39f04f114 --- /dev/null +++ b/lib/routes/malaysiakini/index.ts @@ -0,0 +1,179 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import parser from '@/utils/rss-parser'; +import { config } from '@/config'; +import { FetchError } from 'ofetch'; + +export const route: Route = { + path: '/:lang/:category?', + categories: ['new-media'], + example: '/malaysiakini/en', + parameters: { + lang: 'Language, see below', + category: 'Category, see below, news by default', + }, + features: { + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + requireConfig: [ + { + name: 'MALAYSIAKINI_EMAIL', + optional: true, + description: 'Malaysiakini Email or Username', + }, + { + name: 'MALAYSIAKINI_PASSWORD', + optional: true, + description: 'Malaysiakini Password', + }, + { + name: 'MALAYSIAKINI_REFRESHTOKEN', + optional: true, + description: 'To obtain the refresh token, log into Malaysiakini and look for the cookie `nl____refreshToken` within document.cookie in the browser console. The token is the value of the cookie.', + }, + ], + }, + name: 'News', + maintainers: ['quiniapiezoelectricity'], + handler, + description: ` +| Language | English | Bahasa Malaysia | 华文 | +| -------- | ------ | ------- | ------ | +| \`:lang\` | \`en\` | \`my\` | \`zh\` | + +| Category | \`:category\` | +| ---------------------- | ------------- | +| News | \`news\` | +| Columns | \`columns\` | +| From Our Readers | \`letters\` |`, + radar: [ + { + source: ['malaysiakini.com/'], + target: '/en', + }, + { + source: ['malaysiakini.com/:lang'], + target: '/:lang', + }, + { + source: ['www.malaysiakini.com/:lang/latest/:category'], + target: '/:lang/:category', + }, + ], +}; + +async function handler(ctx) { + const lang = ctx.req.param('lang'); + const category = ctx.req.param('category') ?? 'news'; + const apiKey = 'UFXL7F1EL53S8DZ5SGJUMG5IIFVRY4WI'; // Assuming the apiKey is static + + const key = { + email: config.malaysiakini.email, + password: config.malaysiakini.password, + apiKey, + }; + const body = JSON.stringify(key); + + let cookie; + + const cacheIn = await cache.get('malaysiakini:cookie'); + if (cacheIn) { + cookie = cacheIn; + } + + if (cookie === undefined && config.malaysiakini.email && config.malaysiakini.password) { + const login = await got.post('https://membership.malaysiakini.com/api/v1/auth/local', { + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + Connection: 'keep-alive', + }, + body, + }); + if (login.data.accessToken && login.data.refreshToken) { + cookie = `nl____accessToken=${login.data.accessToken}; nl____refreshToken=${login.data.refreshToken};`; + // Refresh token should be sufficient for authenticating for full text, but access token is included for good measure. + cache.set('malaysiakini:cookie', cookie); + } + } + + if (cookie === undefined && config.malaysiakini.refreshToken) { + cookie = `nl____refreshToken=${config.malaysiakini.refreshToken};`; + cache.set('malaysiakini:cookie', cookie); + } + + const link = `https://www.malaysiakini.com/rss/${lang}/${category}.rss`; + const feed = await parser.parseURL(link); + + if (cookie) { + // Testing the cookie with the first item of the feed + try { + await got({ + method: 'get', + url: `https://www.malaysiakini.com/api/full_content/${feed.items[0].guid}`, + headers: { + Cookie: cookie, + }, + }); + } catch (error) { + if (error instanceof FetchError && error.statusCode === 401) { + await cache.set('malaysiakini:cookie', ''); + } + throw error; + } + } + + const items = await Promise.all( + feed.items.map((item) => + cache.tryGet(item.link, async () => { + const response = await got(`https://www.malaysiakini.com/api/content/${item.guid}`); + if (response.data.stories.content) { + item.description = response.data.stories.content; + } else { + item.description = response.data.stories.teaser; + if (cookie) { + let fullResponse; + try { + fullResponse = await got({ + method: 'get', + url: `https://www.malaysiakini.com/api/full_content/${item.guid}`, + headers: { + Cookie: cookie, + }, + }); + } finally { + if (fullResponse) { + item.description = fullResponse.data.content; + } + } + } + } + if (response.data.stories.image_feat) { + const cover = response.data.stories.image_feat; + if (cover.length > 0) { + item.description = `<img src=${cover[0]}>` + item.description; + } + } + if (response.data.stories.author) { + item.author = response.data.stories.author; + } + if (response.data.stories.tags) { + item.category = response.data.stories.tags; + } + return item; + }) + ) + ); + + return { + title: feed.title, + link: feed.link, + description: feed.description, + language: lang, + item: items, + }; +} diff --git a/lib/routes/malaysiakini/namespace.ts b/lib/routes/malaysiakini/namespace.ts new file mode 100644 index 00000000000000..cbfff94d56cbbc --- /dev/null +++ b/lib/routes/malaysiakini/namespace.ts @@ -0,0 +1,12 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Malaysiakini', + url: 'malaysiakini.com', + description: `Provides an easy-to-use RSS feed for Malaysiakini.com with teaser/full-text fetching. +::: warning +A subscription is required for fetching full articles. +Please refer to the deployment config for more information. +:::`, + lang: 'en', +}; diff --git a/lib/routes/mangadex/namespace.ts b/lib/routes/mangadex/namespace.ts index d7d9327f98dcbb..319b70cc815ae4 100644 --- a/lib/routes/mangadex/namespace.ts +++ b/lib/routes/mangadex/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Unknown', + name: 'MangaDex', url: 'mangadex.org', + lang: 'en', }; diff --git a/lib/routes/manhuagui/comic.ts b/lib/routes/manhuagui/comic.ts index 88c638aa107575..32a4dd41a2e4b2 100644 --- a/lib/routes/manhuagui/comic.ts +++ b/lib/routes/manhuagui/comic.ts @@ -126,7 +126,7 @@ async function handler(ctx) { const items = chapters.map((element) => genResult(element)); let itemsLen = items.length; if (chapterCnt > 0) { - itemsLen = chapterCnt < $.newChapterCnt ? $.newChapterCnt : chapterCnt; + itemsLen = Math.max(chapterCnt, $.newChapterCnt); } return { diff --git a/lib/routes/manhuagui/namespace.ts b/lib/routes/manhuagui/namespace.ts index 07438b7349032c..39f25af6ba095d 100644 --- a/lib/routes/manhuagui/namespace.ts +++ b/lib/routes/manhuagui/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '看漫画', url: 'www.manhuagui.com', + lang: 'zh-CN', }; diff --git a/lib/routes/manhuagui/subscribe.ts b/lib/routes/manhuagui/subscribe.ts index 34455022d0d366..be218a614cdd5b 100644 --- a/lib/routes/manhuagui/subscribe.ts +++ b/lib/routes/manhuagui/subscribe.ts @@ -38,10 +38,10 @@ export const route: Route = { maintainers: ['shininome'], handler, url: 'www.mhgui.com/user/book/shelf', - description: `:::tip + description: `::: tip 个人订阅需要自建 环境变量需要添加 MHGUI\_COOKIE - :::`, +:::`, }; async function handler() { diff --git a/lib/routes/manyvids/namespace.ts b/lib/routes/manyvids/namespace.ts new file mode 100644 index 00000000000000..840485abe77795 --- /dev/null +++ b/lib/routes/manyvids/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'ManyVids', + url: 'www.manyvids.com', + categories: ['multimedia'], + lang: 'en', +}; diff --git a/lib/routes/manyvids/templates/video.art b/lib/routes/manyvids/templates/video.art new file mode 100644 index 00000000000000..63d221442ba9f9 --- /dev/null +++ b/lib/routes/manyvids/templates/video.art @@ -0,0 +1,3 @@ +<video controls preload="metadata" poster="{{ poster }}"> + <source src="{{ src }}" type="video/mp4"> +</video> diff --git a/lib/routes/manyvids/types.ts b/lib/routes/manyvids/types.ts new file mode 100644 index 00000000000000..f8529b69bea1cd --- /dev/null +++ b/lib/routes/manyvids/types.ts @@ -0,0 +1,89 @@ +export interface UserProfile { + createdAt: string; + displayName: string; + profileId: string; + urlHandle: string; + userId: string; + legacyUserId: string; + userStatus: string; + userType: string; + avatar: string; + bio: string; + description: string; + dob: string; + identification: string; + location: string; + orientation: string; + currentRank: number; + bodyType: string; + hairColor: string; + ethnicity: string; + poAddress: string; + poCity: string; + poName: string; + poZip: string; + portrait: string; + shortLinkUrl: string; + profession: string; + socLnkFacebook: string; + socLnkInstagram: string; + socLnkReddit: string; + socLnkTwitter: string; + socLnkYoutube: string; + profileType: string; + hasPremiumMembership: boolean; +} + +interface Avatar { + url: string; +} + +interface Creator { + id: string; + slug: string; + stageName: string; + avatar: Avatar; +} + +interface Thumbnail { + url: string; +} + +interface Preview { + url: string; +} + +interface Price { + free: boolean; + onSale: boolean; + regular: string; +} + +interface Video { + id: string; + title: string; + slug: string; + duration: string; + creator: Creator; + thumbnail: Thumbnail; + preview: Preview; + price: Price; + likes: number; + views: number; + type: string; +} + +interface Pagination { + total: number; + totalWithoutFilters: number; + currentPage: number; + totalPages: number; + nextPage: number; +} + +export interface Videos { + statusCode: number; + statusMessage: string; + data: Video[]; + pagination: Pagination; +} diff --git a/lib/routes/manyvids/video.ts b/lib/routes/manyvids/video.ts new file mode 100644 index 00000000000000..51b8e86e630eef --- /dev/null +++ b/lib/routes/manyvids/video.ts @@ -0,0 +1,51 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { UserProfile, Videos } from './types'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; + +const __dirname = getCurrentPath(import.meta.url); + +export const route: Route = { + path: '/profile/vids/:uid', + radar: [ + { + source: ['www.manyvids.com/Profile/:uid/:handle/Store/*', 'www.manyvids.com/Profile/:uid/:handle/Store'], + }, + ], + parameters: { uid: 'User ID, can be found in the URL.' }, + name: 'Creator Videos', + example: '/manyvids/profile/vids/1001213004', + maintainers: ['TonyRL'], + handler, +}; + +const getProfileById = (uid: string) => cache.tryGet(`manyvids:profile:${uid}`, () => ofetch(`https://www.manyvids.com/bff/profile/profiles/${uid}`)) as Promise<UserProfile>; + +const render = (data) => art(path.join(__dirname, 'templates', 'video.art'), data); + +async function handler(ctx) { + const { uid } = ctx.req.param(); + + const profile = await getProfileById(uid); + const videos = await ofetch<Videos>(`https://www.manyvids.com/bff/store/videos/${uid}/`, { + query: { page: 1 }, + }); + + const items = videos.data.map((v) => ({ + title: v.title, + link: `https://www.manyvids.com/Video/${v.id}/${v.slug}`, + author: v.creator.stageName, + description: render({ poster: v.thumbnail.url, src: v.preview.url }), + })); + + return { + title: `${profile.displayName}'s Profile - Porn vids, Pics & More | ManyVids - ManyVids`, + link: `https://www.manyvids.com/Profile/${uid}/${profile.urlHandle}/Store/Videos`, + image: profile.avatar, + description: profile.bio, + item: items, + }; +} diff --git a/lib/routes/mashiro/index.ts b/lib/routes/mashiro/index.ts new file mode 100644 index 00000000000000..0c55ef8326ad12 --- /dev/null +++ b/lib/routes/mashiro/index.ts @@ -0,0 +1,62 @@ +import { Route } from '@/types'; +import { namespace } from './namespace'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +const baseUrl = `https://${namespace.url}`; + +export const route: Route = { + path: '/:lang', + categories: ['blog'], + example: '/mashiro/en', + parameters: { lang: 'the language of the site. Can be either `en` or `zh-cn`. Default: `en`' }, + radar: [ + { + source: ['mashiro.best/', 'mashiro.best/:lang/'], + }, + ], + name: `Blog`, + maintainers: ['MuenYu'], + handler: async (ctx) => { + const { lang = 'en' } = ctx.req.param(); + const targetLink = lang === 'en' ? `${baseUrl}/archives/` : `${baseUrl}/${lang}/archives/`; + const response = await ofetch(targetLink); + const $ = load(response); + const links = $('.archives-group article') + .toArray() + .slice(0, 10) + .map((item) => { + item = $(item); + const a = item.find('a').first(); + + const title = a.find('.article-title').text(); + const link = `${baseUrl}${a.attr('href')}`; + const pubDate = parseDate(a.find('time').attr('datetime')); + + return { + title, + link, + pubDate, + }; + }); + + const items = await Promise.all( + links.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + item.description = $('.article-content').first().html(); + return item; + }) + ) + ); + + return { + title: namespace.name, + link: targetLink, + item: items, + }; + }, +}; diff --git a/lib/routes/mashiro/namespace.ts b/lib/routes/mashiro/namespace.ts new file mode 100644 index 00000000000000..4f0e39fcca94e2 --- /dev/null +++ b/lib/routes/mashiro/namespace.ts @@ -0,0 +1,11 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: `Mashiro's Baumkuchen`, + url: 'mashiro.best', + description: `Muen's blog posts`, + + zh: { + name: '真白的年轮面包', + }, +}; diff --git a/lib/routes/mastodon/account-id.ts b/lib/routes/mastodon/account-id.ts index 65a54cc2aa6197..1d9ddf90a24d1d 100644 --- a/lib/routes/mastodon/account-id.ts +++ b/lib/routes/mastodon/account-id.ts @@ -1,19 +1,42 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import utils from './utils'; import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { path: '/account_id/:site/:account_id/statuses/:only_media?', - name: 'Unknown', - maintainers: ['notofoe'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, + example: '/mastodon/account_id/mas.to/109300507275095341/statuses/false', + parameters: { + site: 'instance address, only domain, no `http://` or `https://` protocol header', + account_id: 'account ID, you can get it from `https://INSTANCE/api/v1/accounts/lookup?acct=USERNAME` api', + only_media: { + description: 'whether only display media content, default to false, any value to true', + options: [ + { value: 'true', label: 'true' }, + { value: 'false', label: 'false' }, + ], + default: 'false', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'User timeline (by account ID)', + maintainers: ['notofoe', 'pseudoyu'], handler, }; async function handler(ctx) { const site = ctx.req.param('site'); const account_id = ctx.req.param('account_id'); - const only_media = ctx.req.param('only_media') ? 'true' : 'false'; + const only_media = ctx.req.param('only_media') === 'true' ? 'true' : 'false'; if (!config.feature.allow_user_supply_unsafe_domain && !utils.allowSiteList.includes(site)) { throw new ConfigNotFoundError(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`); } diff --git a/lib/routes/mastodon/acct.ts b/lib/routes/mastodon/acct.ts index f5acbb3d1d7423..6ee925dcb2bd67 100644 --- a/lib/routes/mastodon/acct.ts +++ b/lib/routes/mastodon/acct.ts @@ -1,11 +1,22 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import utils from './utils'; export const route: Route = { path: '/acct/:acct/statuses/:only_media?', - categories: ['social-media'], - example: '/mastodon/acct/CatWhitney@mastodon.social/statuses', - parameters: { acct: 'Webfinger account URI, like `user@host`', only_media: 'whether only display media content, default to false, any value to true' }, + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, + example: '/mastodon/acct/Mastodon@mastodon.social/statuses', + parameters: { + acct: 'Webfinger account URI, like `user@host`', + only_media: { + description: 'whether only display media content, default to false, any value to true', + options: [ + { value: 'true', label: 'true' }, + { value: 'false', label: 'false' }, + ], + default: 'false', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -24,7 +35,7 @@ However, you can still specify these route-specific configurations if you need t async function handler(ctx) { const acct = ctx.req.param('acct'); - const only_media = ctx.req.param('only_media') ? 'true' : 'false'; + const only_media = ctx.req.param('only_media') === 'true' ? 'true' : 'false'; const { site, account_id } = await utils.getAccountIdByAcct(acct); diff --git a/lib/routes/mastodon/namespace.ts b/lib/routes/mastodon/namespace.ts index 0d8b2473cc1822..4c33dcb9823152 100644 --- a/lib/routes/mastodon/namespace.ts +++ b/lib/routes/mastodon/namespace.ts @@ -3,7 +3,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Mastodon', url: 'mastodon.social', - description: `:::tip + description: `::: tip Official user RSS: - RSS: \`https://**:instance**/users/**:username**.rss\` ([Example](https://pawoo.net/users/pawoo_support.rss)) @@ -11,4 +11,5 @@ Official user RSS: These feed do not include boosts (a.k.a. reblogs). RSSHub provides a feed for user timeline based on the Mastodon API, but to use that, you may need to create application on a Mastodon instance, and configure your RSSHub instance. Check the [Deploy Guide](https://docs.rsshub.app/deploy/config#route-specific-configurations) for route-specific configurations. :::`, + lang: 'en', }; diff --git a/lib/routes/mastodon/timeline-local.ts b/lib/routes/mastodon/timeline-local.ts index c88541cb58f0d8..89aa200bed5cee 100644 --- a/lib/routes/mastodon/timeline-local.ts +++ b/lib/routes/mastodon/timeline-local.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import utils from './utils'; import { config } from '@/config'; @@ -6,9 +6,20 @@ import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { path: '/timeline/:site/:only_media?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/mastodon/timeline/pawoo.net/true', - parameters: { site: 'instance address, only domain, no `http://` or `https://` protocol header', only_media: 'whether only display media content, default to false, any value to true' }, + parameters: { + site: 'instance address, only domain, no `http://` or `https://` protocol header', + only_media: { + description: 'whether only display media content, default to false, any value to true', + options: [ + { value: 'true', label: 'true' }, + { value: 'false', label: 'false' }, + ], + default: 'false', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -25,18 +36,18 @@ export const route: Route = { async function handler(ctx) { const site = ctx.req.param('site'); - const only_media = ctx.req.param('only_media') ? 'true' : 'false'; + const only_media = ctx.req.param('only_media') === 'true' ? 'true' : 'false'; if (!config.feature.allow_user_supply_unsafe_domain && !utils.allowSiteList.includes(site)) { throw new ConfigNotFoundError(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`); } const url = `http://${site}/api/v1/timelines/public?local=true&only_media=${only_media}`; - const response = await got.get(url, { headers: utils.apiHeaders() }); + const response = await got.get(url, { headers: utils.apiHeaders(site) }); const list = response.data; return { - title: `Local Public${ctx.req.param('only_media') ? ' Media' : ''} Timeline on ${site}`, + title: `Local Public${ctx.req.param('only_media') === 'true' ? ' Media' : ''} Timeline on ${site}`, link: `https://${site}`, item: utils.parseStatuses(list), }; diff --git a/lib/routes/mastodon/timeline-remote.ts b/lib/routes/mastodon/timeline-remote.ts index bb85812af7c693..57160af7324b93 100644 --- a/lib/routes/mastodon/timeline-remote.ts +++ b/lib/routes/mastodon/timeline-remote.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import utils from './utils'; import { config } from '@/config'; @@ -6,9 +6,20 @@ import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { path: '/remote/:site/:only_media?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/mastodon/remote/pawoo.net/true', - parameters: { site: 'instance address, only domain, no `http://` or `https://` protocol header', only_media: 'whether only display media content, default to false, any value to true' }, + parameters: { + site: 'instance address, only domain, no `http://` or `https://` protocol header', + only_media: { + description: 'whether only display media content, default to false, any value to true', + options: [ + { value: 'true', label: 'true' }, + { value: 'false', label: 'false' }, + ], + default: 'false', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -25,18 +36,18 @@ export const route: Route = { async function handler(ctx) { const site = ctx.req.param('site'); - const only_media = ctx.req.param('only_media') ? 'true' : 'false'; + const only_media = ctx.req.param('only_media') === 'true' ? 'true' : 'false'; if (!config.feature.allow_user_supply_unsafe_domain && !utils.allowSiteList.includes(site)) { throw new ConfigNotFoundError(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`); } const url = `http://${site}/api/v1/timelines/public?remote=true&only_media=${only_media}`; - const response = await got.get(url, { headers: utils.apiHeaders() }); + const response = await got.get(url, { headers: utils.apiHeaders(site) }); const list = response.data; return { - title: `Federated Public${ctx.req.param('only_media') ? ' Media' : ''} Timeline on ${site}`, + title: `Federated Public${ctx.req.param('only_media') === 'true' ? ' Media' : ''} Timeline on ${site}`, link: `https://${site}`, item: utils.parseStatuses(list), }; diff --git a/lib/routes/mastodon/utils.ts b/lib/routes/mastodon/utils.ts index 40b4401ade10f5..d2beb9e334cd8c 100644 --- a/lib/routes/mastodon/utils.ts +++ b/lib/routes/mastodon/utils.ts @@ -4,7 +4,7 @@ import { parseDate } from '@/utils/parse-date'; import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; -const allowSiteList = ['mastodon.social', 'pawoo.net', config.mastodon.apiHost].filter(Boolean); +const allowSiteList = ['mastodon.social', 'pawoo.net', 'fosstodon.org', config.mastodon.apiHost].filter(Boolean); const apiHeaders = (site) => { const { accessToken, apiHost } = config.mastodon; diff --git a/lib/routes/matters/author.ts b/lib/routes/matters/author.ts new file mode 100644 index 00000000000000..0526f437bbccf7 --- /dev/null +++ b/lib/routes/matters/author.ts @@ -0,0 +1,63 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { baseUrl, gqlEndpoint, parseItem } from './utils'; + +const handler = async (ctx) => { + const { uid } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const response = await ofetch(gqlEndpoint, { + method: 'POST', + body: { + query: `{ + user(input: {userName: "${uid}"}) { + displayName + avatar + info { + description + } + articles(input: {first: ${limit}}) { + edges { + node { + shortHash + title + content + createdAt + author { + displayName + } + tags { + content + } + } + } + } + } + }`, + }, + }); + + const user = response.data.user; + + return { + title: `Matters | ${user.displayName}`, + link: `${baseUrl}/@${uid}`, + description: user.info.description, + image: user.avatar, + item: user.articles.edges.map(({ node }) => parseItem(node)), + }; +}; + +export const route: Route = { + path: '/author/:uid', + name: 'Author', + example: '/matters/author/robertu', + parameters: { uid: "Author id, can be found at author's homepage url" }, + maintainers: ['Cerebrater', 'xosdy'], + handler, + radar: [ + { + source: ['matters.town/:uid'], + target: (params) => `/matters/author/${params.uid.slice(1)}`, + }, + ], +}; diff --git a/lib/routes/matters/latest.ts b/lib/routes/matters/latest.ts new file mode 100644 index 00000000000000..86fb4260f47ef2 --- /dev/null +++ b/lib/routes/matters/latest.ts @@ -0,0 +1,74 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { baseUrl, gqlEndpoint, parseItem } from './utils'; + +const handler = async (ctx) => { + const { type = 'latest' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const options = { + latest: { + title: '最新', + apiType: 'newest', + }, + heat: { + title: '熱議', + apiType: 'hottest', + }, + essence: { + title: '精華', + apiType: 'icymi', + }, + }; + + const response = await ofetch(gqlEndpoint, { + method: 'POST', + body: { + query: `{ + viewer { + recommendation { + feed: ${options[type].apiType}(input: {first: ${limit}}) { + edges { + node { + shortHash + title + content + createdAt + author { + displayName + } + tags { + content + } + } + } + } + } + } + }`, + }, + }); + + const item = response.data.viewer.recommendation.feed.edges.map(({ node }) => parseItem(node)); + + return { + title: `Matters | ${options[type].title}`, + link: baseUrl, + item, + }; +}; +export const route: Route = { + path: '/latest/:type?', + name: 'Latest, heat, essence', + example: '/matters/latest/heat', + parameters: { uid: 'Defaults to latest, see table below' }, + maintainers: ['xyqfer', 'Cerebrater', 'xosdy'], + handler, + radar: [ + { + source: ['matters.town'], + }, + ], + description: `| 最新 | 热门 | 精华 | +| ------ | ---- | ------- | +| latest | heat | essence |`, +}; diff --git a/lib/routes/matters/namespace.ts b/lib/routes/matters/namespace.ts new file mode 100644 index 00000000000000..1fd32ba4d7c613 --- /dev/null +++ b/lib/routes/matters/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Matters', + url: 'matters.town', + categories: ['new-media', 'popular'], + lang: 'en', +}; diff --git a/lib/routes/matters/tags.ts b/lib/routes/matters/tags.ts new file mode 100644 index 00000000000000..73d96f1146a92a --- /dev/null +++ b/lib/routes/matters/tags.ts @@ -0,0 +1,85 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import cache from '@/utils/cache'; +import { baseUrl, gqlEndpoint, parseItem } from './utils'; + +interface Tag { + type: string; + generated: boolean; + id: string; + typename: string; +} + +const getTagId = (tid: string) => + cache.tryGet(`matters:tags:${tid}`, async () => { + const response = await ofetch(`${baseUrl}/tags/${tid}`); + const $ = cheerio.load(response); + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + + const node = Object.entries(nextData.props.apolloState.data.ROOT_QUERY) + .find(([key]) => key.startsWith('node')) + ?.pop() as Tag; + + return node?.id.split(':')[1]; + }); + +const handler = async (ctx) => { + const { tid } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + + const tagId = await getTagId(tid); + + const gqlResponse = await ofetch(gqlEndpoint, { + method: 'POST', + body: { + query: `{ + node(input: {id: "${tagId}"}) { + ... on Tag { + content + description + articles(input: {first: ${limit}}) { + edges { + node { + title + shortHash + content + createdAt + author { + displayName + } + tags { + content + } + } + } + } + } + } + }`, + }, + }); + + const node = gqlResponse.data.node; + + return { + title: `Matters | ${node.content}`, + link: `${baseUrl}/tags/${tid}`, + description: node.description, + item: node.articles.edges.map(({ node }) => parseItem(node)), + }; +}; + +export const route: Route = { + path: '/tags/:tid', + name: 'Tags', + example: '/matters/tags/972-哲學', + parameters: { tid: 'Tag id, can be found in the url of the tag page' }, + maintainers: ['Cerebrater'], + handler, + radar: [ + { + source: ['matters.town/tags/:tid'], + }, + ], +}; diff --git a/lib/routes/matters/utils.ts b/lib/routes/matters/utils.ts new file mode 100644 index 00000000000000..010244dfbe1e69 --- /dev/null +++ b/lib/routes/matters/utils.ts @@ -0,0 +1,30 @@ +import { parseDate } from '@/utils/parse-date'; + +export const baseUrl = 'https://matters.town'; +export const gqlEndpoint = 'https://server.matters.town/graphql'; + +interface Tag { + content: string; +} + +interface Author { + displayName: string; +} + +interface Article { + shortHash: string; + title: string; + content: string; + createdAt: string; + author: Author; + tags: Tag[]; +} + +export const parseItem = (node: Article) => ({ + title: node.title, + description: node.content, + link: `${baseUrl}/a/${node.shortHash}`, + author: node.author.displayName, + pubDate: parseDate(node.createdAt), + category: node.tags.map((tag) => tag.content), +}); diff --git a/lib/routes/mckinsey/cn/index.ts b/lib/routes/mckinsey/cn/index.ts index 2ca56154073dc4..b095fb4715e301 100644 --- a/lib/routes/mckinsey/cn/index.ts +++ b/lib/routes/mckinsey/cn/index.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import { categories } from './category-map'; @@ -8,9 +8,16 @@ const endpoint = `${baseUrl}/wp-json`; export const route: Route = { path: '/cn/:category?', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/mckinsey/cn', - parameters: { category: '分类,见下表,默认为全部' }, + parameters: { + category: { + description: '分类', + options: Object.entries(categories).map(([value, label]) => ({ value, label: label.name })), + default: '25', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -23,25 +30,25 @@ export const route: Route = { maintainers: ['laampui'], handler, description: `| 分类 | 分类名 | - | ---- | ------------------ | - | 25 | 全部洞见 | - | 2 | 汽车 | - | 3 | 金融服务 | - | 4 | 消费者 | - | 5 | 医药 | - | 7 | 数字化 | - | 8 | 制造业 | - | 9 | 私募 | - | 10 | 技术,媒体与通信 | - | 12 | 城市化与可持续发展 | - | 13 | 创新 | - | 16 | 人才与领导力 | - | 18 | 宏观经济 | - | 19 | 麦肯锡全球研究院 | - | 37 | 麦肯锡季刊 | - | 41 | 资本项目和基础设施 | - | 42 | 旅游、运输和物流 | - | 45 | 全球基础材料 |`, +| ---- | ------------------ | +| 25 | 全部洞见 | +| 2 | 汽车 | +| 3 | 金融服务 | +| 4 | 消费者 | +| 5 | 医药 | +| 7 | 数字化 | +| 8 | 制造业 | +| 9 | 私募 | +| 10 | 技术,媒体与通信 | +| 12 | 城市化与可持续发展 | +| 13 | 创新 | +| 16 | 人才与领导力 | +| 18 | 宏观经济 | +| 19 | 麦肯锡全球研究院 | +| 37 | 麦肯锡季刊 | +| 41 | 资本项目和基础设施 | +| 42 | 旅游、运输和物流 | +| 45 | 全球基础材料 |`, }; async function handler(ctx) { diff --git a/lib/routes/mckinsey/namespace.ts b/lib/routes/mckinsey/namespace.ts index 9cf96d5b87e967..473a92371ba246 100644 --- a/lib/routes/mckinsey/namespace.ts +++ b/lib/routes/mckinsey/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '麦肯锡', url: 'mckinsey.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/mcmod/index.ts b/lib/routes/mcmod/index.ts new file mode 100644 index 00000000000000..91fbef4d962d0f --- /dev/null +++ b/lib/routes/mcmod/index.ts @@ -0,0 +1,105 @@ +import { DataItem, Route } from '@/types'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; +import path from 'node:path'; +import cache from '@/utils/cache'; +import timezone from '@/utils/timezone'; +import { art } from '@/utils/render'; +import { getCurrentPath } from '@/utils/helpers'; + +const render = (mod) => art(path.join(getCurrentPath(import.meta.url), 'templates', 'mod.art'), { mod }); + +export const route: Route = { + path: '/:type', + categories: ['game'], + example: '/mcmod/new', + parameters: { type: '查询类型,详见下表' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '最新MOD', + maintainers: ['hualiong'], + description: `\`:type\` 类型可选如下 + +| 随机显示MOD | 最新收录MOD | 最近编辑MOD | +| ------ | --- | ---- | +| random | new | edit |`, + handler: async (ctx) => { + const type = ctx.req.param('type'); + const $get = ofetch.create({ baseURL: 'https://www.mcmod.cn' }); + const response = await $get('/'); + + const $ = load(response); + const typeName = $(`div.left > ul > li[i='${type}']`).attr('title'); + const list = $(`#indexNew_${type} > .block`) + .toArray() + .map((item): DataItem => { + const each = $(item); + const time = each.find('div .time'); + return { + title: each.find('div > .name > a').text(), + image: each.find('img').attr('src')?.split('@')[0], + link: each.children('a').attr('href'), + pubDate: time.attr('title') && timezone(parseDate(time.attr('title')!.substring(6), 'YYYY-MM-DD HH:mm:ss'), +8), + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link!, async () => { + const response = await $get(item.link!); + const $ = load(response); + + item.author = $('.author li') + .toArray() + .map((item) => { + const each = $(item); + const name = each.find('.name a'); + return { + name: name.text(), + url: 'https://www.mcmod.cn' + name.attr('href'), + avatar: each.find('.avatar img').attr('src')?.split('?')[0], + }; + }); + + const html = $('.common-text[data-id="1"]').html()!; + const support = $('.mcver > ul > ul') + .toArray() + .map((e) => { + const ul = $(e); + const label = ul.children('li:first-child').text(); + const versions = ul + .children('li:not(:first-child)') + .toArray() + .map((e) => $(e).text()) + .join(','); + return { label, versions }; + }); + + item.description = + render({ + pic: 'https:' + item.image, + label: $('.class-info li.col-lg-4') + .toArray() + .map((e) => $(e).text()), + support, + }) + html.replaceAll(/\ssrc=".+?"/g, '').replaceAll('data-src', 'src'); + return item; + }) + ) + ); + + return { + title: `${typeName} - MC百科`, + description: $('meta[name="description"]').attr('content'), + link: 'https://www.mcmod.cn', + item: items as DataItem[], + }; + }, +}; diff --git a/lib/routes/mcmod/namespace.ts b/lib/routes/mcmod/namespace.ts new file mode 100644 index 00000000000000..8f04ea9a95d587 --- /dev/null +++ b/lib/routes/mcmod/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'MC百科', + url: 'www.mcmod.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/mcmod/templates/mod.art b/lib/routes/mcmod/templates/mod.art new file mode 100644 index 00000000000000..90042d547da7e9 --- /dev/null +++ b/lib/routes/mcmod/templates/mod.art @@ -0,0 +1,13 @@ +<img src="{{ mod.pic }}" referrerpolicy="no-referrer"> +{{ each mod.label l }} + <p>{{ l }}</p> +{{ /each }} +{{ if mod.support.length > 0 }} + <p>支持的MC版本: </p> + <ul> + {{ each mod.support s }} + <li><b>{{ s.label }}</b>{{ s.versions }}</li> + {{ /each }} + </ul> +{{ /if }} +<hr style="height: 2px; background-color: #e7e7e7; border: 0 none;"> \ No newline at end of file diff --git a/lib/routes/mdpi/namespace.ts b/lib/routes/mdpi/namespace.ts index b4613d20e296cb..6bcda6c9c067bc 100644 --- a/lib/routes/mdpi/namespace.ts +++ b/lib/routes/mdpi/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MDPI', url: 'www.mdpi.com', + lang: 'en', }; diff --git a/lib/routes/medieval-china/namespace.ts b/lib/routes/medieval-china/namespace.ts index 4ce5979f7bc83b..be94d080a0eef7 100644 --- a/lib/routes/medieval-china/namespace.ts +++ b/lib/routes/medieval-china/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国的中古', url: 'medieval-china.club', + lang: 'zh-CN', }; diff --git a/lib/routes/medium/feed.ts b/lib/routes/medium/feed.ts new file mode 100644 index 00000000000000..c1ab790f13f9a7 --- /dev/null +++ b/lib/routes/medium/feed.ts @@ -0,0 +1,54 @@ +import { Route, ViewType } from '@/types'; +import parser from '@/utils/rss-parser'; +import { parseDate } from '@/utils/parse-date'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +export const route: Route = { + path: '/feed/:user', + categories: ['blog'], + view: ViewType.SocialMedia, + example: '/medium/feed/zhgchgli', + parameters: { user: 'Username of the Medium' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['medium.com/@:user'], + target: '/feed/:user', + }, + ], + name: 'Medium Feed', + maintainers: ['pseudoyu'], + handler, +}; + +async function handler(ctx) { + const user = ctx.req.param('user'); + + if (!user) { + throw new InvalidParameterError('Invalid user'); + } + + const feed = await parser.parseURL(`https://medium.com/feed/@${user}`); + + return { + title: feed.title ?? 'Medium', + description: feed.description ?? `${user}'s Medium`, + link: feed.link ?? `https://medium.com/@${user}`, + image: feed.image?.url ?? '', + item: feed.items.map((item) => ({ + title: item.title ?? 'Untitled', + description: item['content:encoded'] ?? item.content ?? '', + link: item.link ?? '', + pubDate: item.pubDate ? parseDate(item.pubDate) : undefined, + guid: item.guid ?? '', + author: item.creator ?? user, + })), + }; +} diff --git a/lib/routes/medium/following.ts b/lib/routes/medium/following.ts index 1f34e24533fc0b..59bade582523b1 100644 --- a/lib/routes/medium/following.ts +++ b/lib/routes/medium/following.ts @@ -26,9 +26,9 @@ export const route: Route = { name: 'Personalized Recommendations - Following', maintainers: ['ImSingee'], handler, - description: `:::warning + description: `::: warning Personalized recommendations require the cookie value after logging in, so only self-hosting is supported. See the configuration module on the deployment page for details. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/medium/for-you.ts b/lib/routes/medium/for-you.ts index 48b7a204f96754..ddb8b4140cd63d 100644 --- a/lib/routes/medium/for-you.ts +++ b/lib/routes/medium/for-you.ts @@ -26,9 +26,9 @@ export const route: Route = { name: 'Personalized Recommendations - For You', maintainers: ['ImSingee'], handler, - description: `:::warning + description: `::: warning Personalized recommendations require the cookie value after logging in, so only self-hosting is supported. See the configuration module on the deployment page for details. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/medium/list.ts b/lib/routes/medium/list.ts index d628e232cf3f9f..3c5b95f590bffb 100644 --- a/lib/routes/medium/list.ts +++ b/lib/routes/medium/list.ts @@ -24,9 +24,9 @@ export const route: Route = { handler, description: `The List ID is the last part of the URL after \`-\`, for example, the username in [https://medium.com/@imsingee/list/collection-7e67004f23f9](https://medium.com/@imsingee/list/collection-7e67004f23f9) is \`imsingee\`, and the ID is \`7e67004f23f9\`. - :::warning +::: warning To access private lists, only self-hosting is supported. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/medium/namespace.ts b/lib/routes/medium/namespace.ts index 605e31be98c4f5..68396902c517d9 100644 --- a/lib/routes/medium/namespace.ts +++ b/lib/routes/medium/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Medium', url: 'medium.com', + lang: 'en', }; diff --git a/lib/routes/medium/tag.ts b/lib/routes/medium/tag.ts index 15ef2e54d00ca6..0f6472a61de91d 100644 --- a/lib/routes/medium/tag.ts +++ b/lib/routes/medium/tag.ts @@ -28,9 +28,9 @@ export const route: Route = { handler, description: `There are many tags, which can be obtained by clicking on a tag from the homepage and looking at the URL. For example, if the URL is \`https://medium.com/?tag=web3\`, then the tag is \`web3\`. - :::warning +::: warning Personalized recommendations require the cookie value after logging in, so only self-hosting is supported. See the configuration module on the deployment page for details. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/medsci/index.ts b/lib/routes/medsci/index.ts index 4695fd9e7344df..5ff9400706e57e 100644 --- a/lib/routes/medsci/index.ts +++ b/lib/routes/medsci/index.ts @@ -6,7 +6,7 @@ import { parseDate, parseRelativeDate } from '@/utils/parse-date'; export const route: Route = { path: '/:sid?/:tid?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/medsci', parameters: { sid: '科室,见下表,默认为推荐', tid: '亚专业,可在对应科室页 URL 中找到,默认为该科室的全部' }, features: { @@ -20,47 +20,47 @@ export const route: Route = { name: '资讯', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 下表为科室对应的 sid,若想获得 tid,可以到对应科室页面 URL 中寻找 \`t_id\` 字段的值,下面是一个例子: 如 [肿瘤 - NSCLC](https://www.medsci.cn/department/details?s_id=5\&t_id=277) 的 URL 为 \`https://www.medsci.cn/department/details?s_id=5&t_id=277\`,可以看到此时 \`s_id\` 对应 \`sid\` 的值为 5, \`t_id\` 对应 \`tid\` 的值为 277,所以可以得到路由 [\`/medsci/5/277\`](https://rsshub.app/medsci/5/277) - ::: +::: - | 心血管 | 内分泌 | 消化 | 呼吸 | 神经科 | - | ------ | ------ | ---- | ---- | ------ | - | 2 | 6 | 4 | 12 | 17 | +| 心血管 | 内分泌 | 消化 | 呼吸 | 神经科 | +| ------ | ------ | ---- | ---- | ------ | +| 2 | 6 | 4 | 12 | 17 | - | 传染科 | 精神心理 | 肾内科 | 风湿免疫 | 血液科 | - | ------ | -------- | ------ | -------- | ------ | - | 9 | 13 | 14 | 15 | 21 | +| 传染科 | 精神心理 | 肾内科 | 风湿免疫 | 血液科 | +| ------ | -------- | ------ | -------- | ------ | +| 9 | 13 | 14 | 15 | 21 | - | 老年医学 | 胃肠外科 | 血管外科 | 肝胆胰外 | 骨科 | - | -------- | -------- | -------- | -------- | ---- | - | 19 | 76 | 92 | 91 | 10 | +| 老年医学 | 胃肠外科 | 血管外科 | 肝胆胰外 | 骨科 | +| -------- | -------- | -------- | -------- | ---- | +| 19 | 76 | 92 | 91 | 10 | - | 普通外科 | 胸心外科 | 神经外科 | 泌尿外科 | 烧伤科 | - | -------- | -------- | -------- | -------- | ------ | - | 23 | 24 | 25 | 26 | 27 | +| 普通外科 | 胸心外科 | 神经外科 | 泌尿外科 | 烧伤科 | +| -------- | -------- | -------- | -------- | ------ | +| 23 | 24 | 25 | 26 | 27 | - | 整形科 | 麻醉疼痛 | 罕见病 | 康复医学 | 药械 | - | ------ | -------- | ------ | -------- | ---- | - | 28 | 29 | 304 | 95 | 11 | +| 整形科 | 麻醉疼痛 | 罕见病 | 康复医学 | 药械 | +| ------ | -------- | ------ | -------- | ---- | +| 28 | 29 | 304 | 95 | 11 | - | 儿科 | 耳鼻咽喉 | 口腔科 | 眼科 | 政策人文 | - | ---- | -------- | ------ | ---- | -------- | - | 18 | 30 | 31 | 32 | 33 | +| 儿科 | 耳鼻咽喉 | 口腔科 | 眼科 | 政策人文 | +| ---- | -------- | ------ | ---- | -------- | +| 18 | 30 | 31 | 32 | 33 | - | 营养全科 | 预防公卫 | 妇产科 | 中医科 | 急重症 | - | -------- | -------- | ------ | ------ | ------ | - | 34 | 35 | 36 | 37 | 38 | +| 营养全科 | 预防公卫 | 妇产科 | 中医科 | 急重症 | +| -------- | -------- | ------ | ------ | ------ | +| 34 | 35 | 36 | 37 | 38 | - | 皮肤性病 | 影像放射 | 转化医学 | 检验病理 | 护理 | - | -------- | -------- | -------- | -------- | ---- | - | 39 | 40 | 42 | 69 | 79 | +| 皮肤性病 | 影像放射 | 转化医学 | 检验病理 | 护理 | +| -------- | -------- | -------- | -------- | ---- | +| 39 | 40 | 42 | 69 | 79 | - | 糖尿病 | 冠心病 | 肝病 | 乳腺癌 | - | ------ | ------ | ---- | ------ | - | 8 | 43 | 22 | 89 |`, +| 糖尿病 | 冠心病 | 肝病 | 乳腺癌 | +| ------ | ------ | ---- | ------ | +| 8 | 43 | 22 | 89 |`, }; async function handler(ctx) { diff --git a/lib/routes/medsci/namespace.ts b/lib/routes/medsci/namespace.ts index 9340378253970f..0ec510c7d23294 100644 --- a/lib/routes/medsci/namespace.ts +++ b/lib/routes/medsci/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '梅斯医学 MedSci', url: 'medsci.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/meishichina/index.ts b/lib/routes/meishichina/index.ts new file mode 100644 index 00000000000000..b4b24b61863cf6 --- /dev/null +++ b/lib/routes/meishichina/index.ts @@ -0,0 +1,1997 @@ +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 isChinese = (text: string): boolean => /^[\u4E00-\u9FA5]+$/.test(text); + +export const handler = async (ctx) => { + const DEFAULT_CATEGORY = '最新推荐'; + const DEFAULT_CLASSID = 0; + const DEFAULT_ORDERBY = 'hot'; + + const { category = DEFAULT_CATEGORY } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + + // If `category` is in Chinese, it should come from the tab titles, + // because each `recipe-type` has an English ID. + // e.g. `recai` is for [热菜](https://home.meishichina.com/recipe/recai/). `mifan` is for [米饭](https://home.meishichina.com/recipe/mifan/). + + const isTab = isChinese(category); + + // Some categories, for example, [做法简单的菜谱](https://home.meishichina.com/recipe-type-do-level-view-1.html). + // The URLs of theirs start with `recipe` and end with `.html`. + + const isHtml = category.startsWith('recipe'); + + const rootUrl = 'https://home.meishichina.com'; + const rootImageUrl = 'https://i3.meishichina.com'; + const currentUrl = new URL(`${isHtml ? '' : 'recipe'}${isTab ? '.html' : `/${category.endsWith('/') ? category : `${category}${isHtml ? '.html' : '/'}`}`}`, rootUrl).href; + const apiUrl = new URL('ajax/ajax.php', rootUrl).href; + + const { data: currentResponse } = await got(currentUrl); + + const $ = load(currentResponse); + + const categoryEl = isTab + ? $('div#recipeindex_info_wrap a') + .toArray() + .find((a) => $(a).text() === category) + : undefined; + + const { data: response } = isTab + ? await got(apiUrl, { + searchParams: { + ac: 'recipe', + op: 'getMoreDiffStateRecipeList', + classid: categoryEl ? $(categoryEl).prop('data') : DEFAULT_CLASSID, + orderby: categoryEl ? $(categoryEl).prop('order') : DEFAULT_ORDERBY, + page: 1, + }, + }) + : { data: undefined }; + + let items = isTab + ? response.data.slice(0, limit).map((item) => { + const title = item.title; + const guid = `meishichina-${item.id}`; + const image = item.fcover.split(/\?/)[0]; + + return { + title, + pubDate: item.datelines ? parseDate(item.datelines, 'X') : parseDate(item.dateline, 'YYYY-M-D'), + link: new URL(`recipe-${item.id}.html`, rootUrl).href, + category: item.mainingredient.replace(/。$/, '').split('、'), + author: item.username, + guid, + id: guid, + image, + banner: image, + }; + }) + : $('div#J_list ul li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const title = item.find('div.detail h2').text(); + const description = item.find('div.detail').html(); + const guid = `meishichina-${item.prop('data-id')}`; + const image = item.find('div.pic img').prop('src').split(/\?/)[0]; + + return { + title, + description, + link: item.find('div.detail h2 a').prop('href'), + category: item.find('p.subcontent').text().replace(/。$/, '').split('、'), + author: item.find('p.subline a').text(), + guid, + id: guid, + content: { + html: description, + text: item.find('div.detail').text(), + }, + 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); + + $$('input[type="hidden"]').remove(); + $$('p.J_photo, p.copyright').remove(); + $$('div.sharebox').nextAll().addBack().remove(); + + const title = $$('a#recipe_title').text(); + const description = $$('div.recipDetail').html(); + const image = $$('div#recipe_De_imgBox img').prop('src')?.split(/\?/)[0] ?? undefined; + + const pubDate = detailResponse.match(/"pubDate":\s"(.*?)",/)?.[1] ?? undefined; + const updated = detailResponse.match(/"upDate":\s"(.*?)",/)?.[1] ?? undefined; + + item.title = title; + item.description = description; + item.pubDate = pubDate ? parseDate(pubDate) : item.pubDate; + item.category = [ + ...new Set([ + ...$$('div.recipeTip a') + .toArray() + .map((c) => $$(c).prop('title')), + ...$$('fieldset.particulars span.category_s1') + .toArray() + .map((c) => $$(c).text().trim()), + ]), + ].filter(Boolean); + item.author = $$('span#recipe_username').text(); + item.content = { + html: description, + text: $$('div.recipDetail').text(), + }; + item.image = image; + item.banner = image; + item.updated = updated ? parseDate(updated) : item.updated; + + return item; + }) + ) + ); + + const image = new URL('static/lib/logo.png', rootImageUrl).href; + + return { + title: `${isTab ? (categoryEl ? category : DEFAULT_CATEGORY) : `${$('h1.on').text()}${$('a.right.on').text()}`}${( + $('title') + .text() + .match(/(?:.*_)?([^_]+)_(.*)$/) || [] + ) + .slice(1, 3) + .join('_')}`, + description: $('meta[name="description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $('div.logo_inner a').prop('title'), + }; +}; + +export const route: Route = { + path: '/recipe/:category{.+}?', + name: '菜谱', + url: 'home.meishichina.com', + maintainers: ['nczitzk'], + handler, + example: '/meishichina/recipe', + parameters: { category: '分类,默认为最新推荐,见下表' }, + description: `::: tip + 若订阅 [菜谱大全](https://home.meishichina.com/recipe.html) 中的 \`最新推荐\` 分类,将 \`最新推荐\` 作为参数填入,此时路由为 [\`/meishichina/recipe/最新推荐/\`](https://rsshub.app/meishichina/recipe/最新推荐)。 + + 若订阅 [菜谱大全](https://home.meishichina.com/recipe.html) 中的 \`自制食材\` 分类,将 \`自制食材\` 作为参数填入,此时路由为 [\`/meishichina/recipe/自制食材/\`](https://rsshub.app/meishichina/recipe/自制食材)。 +::: + +| [最新推荐](https://home.meishichina.com/recipe.html) | [最新发布](https://home.meishichina.com/recipe.html) | [热菜](https://home.meishichina.com/recipe.html) | [凉菜](https://home.meishichina.com/recipe.html) | [汤羹](https://home.meishichina.com/recipe.html) | [主食](https://home.meishichina.com/recipe.html) | [小吃](https://home.meishichina.com/recipe.html) | [西餐](https://home.meishichina.com/recipe.html) | [烘焙](https://home.meishichina.com/recipe.html) | [自制食材](https://home.meishichina.com/recipe.html) | +| ---------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ | ---------------------------------------------------- | + +::: tip + 若订阅 [全部分类](https://home.meishichina.com/recipe-type.html) 中的对应分类页,见下方说明。 + + 若订阅 [热菜最新菜谱](https://home.meishichina.com/recipe/recai/),网址为 \`https://home.meishichina.com/recipe/recai/\`。截取 \`https://home.meishichina.com/recipe/\` 到末尾 \`/\` 的部分 \`recai\` 作为参数填入,此时路由为 [\`/meishichina/recipe/recai/\`](https://rsshub.app/meishichina/recipe/recai)。 + + 若订阅 [米饭最热菜谱](https://home.meishichina.com/recipe/mifan/hot/),网址为 \`https://home.meishichina.com/recipe/mifan/hot/\`。截取 \`https://home.meishichina.com/recipe/\` 到末尾 \`/\` 的部分 \`mifan/hot\` 作为参数填入,此时路由为 [\`/meishichina/recipe/mifan/hot/\`](https://rsshub.app/meishichina/recipe/mifan/hot)。 + + 若订阅 [制作难度简单菜谱](https://home.meishichina.com/recipe-type-do-level-view-1.html),网址为 \`https://home.meishichina.com/recipe-type-do-level-view-1.html\`。截取 \`https://home.meishichina.com/\` 到末尾 \`.html\` 的部分 \`recipe-type-do-level-view-1\` 作为参数填入,此时路由为 [\`/meishichina/recipe/recipe-type-do-level-view-1/\`](https://rsshub.app/meishichina/recipe/recipe-type-do-level-view-1)。 +::: + +<details> +<summary>更多分类</summary> + +#### 常见菜式 + +| [热菜](https://home.meishichina.com/recipe/recai/) | [凉菜](https://home.meishichina.com/recipe/liangcai/) | [汤羹](https://home.meishichina.com/recipe/tanggeng/) | [主食](https://home.meishichina.com/recipe/zhushi/) | [小吃](https://home.meishichina.com/recipe/xiaochi/) | [家常菜](https://home.meishichina.com/recipe/jiachang/) | +| ---------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------- | ---------------------------------------------------------- | +| [recai](https://rsshub.app/meishichina/recipe/recai) | [liangcai](https://rsshub.app/meishichina/recipe/liangcai) | [tanggeng](https://rsshub.app/meishichina/recipe/tanggeng) | [zhushi](https://rsshub.app/meishichina/recipe/zhushi) | [xiaochi](https://rsshub.app/meishichina/recipe/xiaochi) | [jiachang](https://rsshub.app/meishichina/recipe/jiachang) | + +| [泡酱腌菜](https://home.meishichina.com/recipe/jiangpaoyancai/) | [西餐](https://home.meishichina.com/recipe/xican/) | [烘焙](https://home.meishichina.com/recipe/hongbei/) | [烤箱菜](https://home.meishichina.com/recipe/kaoxiangcai/) | [饮品](https://home.meishichina.com/recipe/yinpin/) | [零食](https://home.meishichina.com/recipe/lingshi/) | +| ---------------------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------- | +| [jiangpaoyancai](https://rsshub.app/meishichina/recipe/jiangpaoyancai) | [xican](https://rsshub.app/meishichina/recipe/xican) | [hongbei](https://rsshub.app/meishichina/recipe/hongbei) | [kaoxiangcai](https://rsshub.app/meishichina/recipe/kaoxiangcai) | [yinpin](https://rsshub.app/meishichina/recipe/yinpin) | [lingshi](https://rsshub.app/meishichina/recipe/lingshi) | + +| [火锅](https://home.meishichina.com/recipe/huoguo/) | [自制食材](https://home.meishichina.com/recipe/zizhishicai/) | [海鲜](https://home.meishichina.com/recipe/haixian/) | [宴客菜](https://home.meishichina.com/recipe/yankecai/) | +| ------------------------------------------------------ | ---------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------- | +| [huoguo](https://rsshub.app/meishichina/recipe/huoguo) | [zizhishicai](https://rsshub.app/meishichina/recipe/zizhishicai) | [haixian](https://rsshub.app/meishichina/recipe/haixian) | [yankecai](https://rsshub.app/meishichina/recipe/yankecai) | + +#### 主食/小吃 + +| [米饭](https://home.meishichina.com/recipe/mifan/) | [炒饭](https://home.meishichina.com/recipe/chaofan/) | [面食](https://home.meishichina.com/recipe/mianshi/) | [包子](https://home.meishichina.com/recipe/baozi/) | [饺子](https://home.meishichina.com/recipe/jiaozi/) | [馒头花卷](https://home.meishichina.com/recipe/mantou/) | +| ---------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------- | +| [mifan](https://rsshub.app/meishichina/recipe/mifan) | [chaofan](https://rsshub.app/meishichina/recipe/chaofan) | [mianshi](https://rsshub.app/meishichina/recipe/mianshi) | [baozi](https://rsshub.app/meishichina/recipe/baozi) | [jiaozi](https://rsshub.app/meishichina/recipe/jiaozi) | [mantou](https://rsshub.app/meishichina/recipe/mantou) | + +| [面条](https://home.meishichina.com/recipe/miantiao/) | [饼](https://home.meishichina.com/recipe/bing/) | [粥](https://home.meishichina.com/recipe/zhou/) | [馄饨](https://home.meishichina.com/recipe/hundun/) | [五谷杂粮](https://home.meishichina.com/recipe/wuguzaliang/) | [北京小吃](https://home.meishichina.com/recipe/beijingxiaochi/) | +| ---------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------- | ---------------------------------------------------------------------- | +| [miantiao](https://rsshub.app/meishichina/recipe/miantiao) | [bing](https://rsshub.app/meishichina/recipe/bing) | [zhou](https://rsshub.app/meishichina/recipe/zhou) | [hundun](https://rsshub.app/meishichina/recipe/hundun) | [wuguzaliang](https://rsshub.app/meishichina/recipe/wuguzaliang) | [beijingxiaochi](https://rsshub.app/meishichina/recipe/beijingxiaochi) | + +| [陕西小吃](https://home.meishichina.com/recipe/shanxixiaochi/) | [广东小吃](https://home.meishichina.com/recipe/guangdongxiaochi/) | [四川小吃](https://home.meishichina.com/recipe/sichuanxiaochi/) | [重庆小吃](https://home.meishichina.com/recipe/chongqingxiaochi/) | [天津小吃](https://home.meishichina.com/recipe/tianjinxiaochi/) | [上海小吃](https://home.meishichina.com/recipe/shanghaixiochi/) | +| -------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| [shanxixiaochi](https://rsshub.app/meishichina/recipe/shanxixiaochi) | [guangdongxiaochi](https://rsshub.app/meishichina/recipe/guangdongxiaochi) | [sichuanxiaochi](https://rsshub.app/meishichina/recipe/sichuanxiaochi) | [chongqingxiaochi](https://rsshub.app/meishichina/recipe/chongqingxiaochi) | [tianjinxiaochi](https://rsshub.app/meishichina/recipe/tianjinxiaochi) | [shanghaixiochi](https://rsshub.app/meishichina/recipe/shanghaixiochi) | + +| [福建小吃](https://home.meishichina.com/recipe/fujianxiaochi/) | [湖南小吃](https://home.meishichina.com/recipe/hunanxiaochi/) | [湖北小吃](https://home.meishichina.com/recipe/hubeixiaochi/) | [江西小吃](https://home.meishichina.com/recipe/jiangxixiaochi/) | [山东小吃](https://home.meishichina.com/recipe/shandongxiaochi/) | [山西小吃](https://home.meishichina.com/recipe/jinxiaochi/) | +| -------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------- | +| [fujianxiaochi](https://rsshub.app/meishichina/recipe/fujianxiaochi) | [hunanxiaochi](https://rsshub.app/meishichina/recipe/hunanxiaochi) | [hubeixiaochi](https://rsshub.app/meishichina/recipe/hubeixiaochi) | [jiangxixiaochi](https://rsshub.app/meishichina/recipe/jiangxixiaochi) | [shandongxiaochi](https://rsshub.app/meishichina/recipe/shandongxiaochi) | [jinxiaochi](https://rsshub.app/meishichina/recipe/jinxiaochi) | + +| [河南小吃](https://home.meishichina.com/recipe/henanxiaochi/) | [台湾小吃](https://home.meishichina.com/recipe/taiwanxiaochi/) | [江浙小吃](https://home.meishichina.com/recipe/jiangzhexiaochi/) | [云贵小吃](https://home.meishichina.com/recipe/yunguixiaochi/) | [东北小吃](https://home.meishichina.com/recipe/dongbeixiaochi/) | [西北小吃](https://home.meishichina.com/recipe/xibeixiaochi/) | +| ------------------------------------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [henanxiaochi](https://rsshub.app/meishichina/recipe/henanxiaochi) | [taiwanxiaochi](https://rsshub.app/meishichina/recipe/taiwanxiaochi) | [jiangzhexiaochi](https://rsshub.app/meishichina/recipe/jiangzhexiaochi) | [yunguixiaochi](https://rsshub.app/meishichina/recipe/yunguixiaochi) | [dongbeixiaochi](https://rsshub.app/meishichina/recipe/dongbeixiaochi) | [xibeixiaochi](https://rsshub.app/meishichina/recipe/xibeixiaochi) | + +#### 甜品/饮品 + +| [甜品](https://home.meishichina.com/recipe/tianpin/) | [冰品](https://home.meishichina.com/recipe/bingpin/) | [果汁](https://home.meishichina.com/recipe/guozhi/) | [糖水](https://home.meishichina.com/recipe/tangshui/) | [布丁](https://home.meishichina.com/recipe/buding/) | [果酱](https://home.meishichina.com/recipe/guojiang/) | +| -------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------- | +| [tianpin](https://rsshub.app/meishichina/recipe/tianpin) | [bingpin](https://rsshub.app/meishichina/recipe/bingpin) | [guozhi](https://rsshub.app/meishichina/recipe/guozhi) | [tangshui](https://rsshub.app/meishichina/recipe/tangshui) | [buding](https://rsshub.app/meishichina/recipe/buding) | [guojiang](https://rsshub.app/meishichina/recipe/guojiang) | + +| [果冻](https://home.meishichina.com/recipe/guodong/) | [酸奶](https://home.meishichina.com/recipe/suannai/) | [鸡尾酒](https://home.meishichina.com/recipe/jiweijiu/) | [咖啡](https://home.meishichina.com/recipe/kafei/) | [豆浆](https://home.meishichina.com/recipe/doujiang/) | [奶昔](https://home.meishichina.com/recipe/naixi/) | +| -------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------- | +| [guodong](https://rsshub.app/meishichina/recipe/guodong) | [suannai](https://rsshub.app/meishichina/recipe/suannai) | [jiweijiu](https://rsshub.app/meishichina/recipe/jiweijiu) | [kafei](https://rsshub.app/meishichina/recipe/kafei) | [doujiang](https://rsshub.app/meishichina/recipe/doujiang) | [naixi](https://rsshub.app/meishichina/recipe/naixi) | + +| [冰淇淋](https://home.meishichina.com/recipe/bingqilin/) | +| ------------------------------------------------------------ | +| [bingqilin](https://rsshub.app/meishichina/recipe/bingqilin) | + +#### 适宜人群 + +| [孕妇](https://home.meishichina.com/recipe/yunfu/) | [产妇](https://home.meishichina.com/recipe/chanfu/) | [婴儿](https://home.meishichina.com/recipe/yinger/) | [儿童](https://home.meishichina.com/recipe/ertong/) | [老人](https://home.meishichina.com/recipe/laoren/) | [幼儿](https://home.meishichina.com/recipe/youer/) | +| ---------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | ---------------------------------------------------- | +| [yunfu](https://rsshub.app/meishichina/recipe/yunfu) | [chanfu](https://rsshub.app/meishichina/recipe/chanfu) | [yinger](https://rsshub.app/meishichina/recipe/yinger) | [ertong](https://rsshub.app/meishichina/recipe/ertong) | [laoren](https://rsshub.app/meishichina/recipe/laoren) | [youer](https://rsshub.app/meishichina/recipe/youer) | + +| [哺乳期](https://home.meishichina.com/recipe/buruqi/) | [青少年](https://home.meishichina.com/recipe/qingshaonian/) | +| ------------------------------------------------------ | ------------------------------------------------------------------ | +| [buruqi](https://rsshub.app/meishichina/recipe/buruqi) | [qingshaonian](https://rsshub.app/meishichina/recipe/qingshaonian) | + +#### 食疗食补 + +| [健康食谱](https://home.meishichina.com/recipe/jiankangshipu/) | [减肥瘦身](https://home.meishichina.com/recipe/shoushen/) | [贫血](https://home.meishichina.com/recipe/pinxue/) | [痛经](https://home.meishichina.com/recipe/tongjing/) | [清热祛火](https://home.meishichina.com/recipe/qingrequhuo/) | [滋阴](https://home.meishichina.com/recipe/ziyin/) | +| -------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------- | +| [jiankangshipu](https://rsshub.app/meishichina/recipe/jiankangshipu) | [shoushen](https://rsshub.app/meishichina/recipe/shoushen) | [pinxue](https://rsshub.app/meishichina/recipe/pinxue) | [tongjing](https://rsshub.app/meishichina/recipe/tongjing) | [qingrequhuo](https://rsshub.app/meishichina/recipe/qingrequhuo) | [ziyin](https://rsshub.app/meishichina/recipe/ziyin) | + +| [壮阳](https://home.meishichina.com/recipe/zhuangyang/) | [便秘](https://home.meishichina.com/recipe/bianmi/) | [排毒养颜](https://home.meishichina.com/recipe/paiduyangyan/) | [滋润补水](https://home.meishichina.com/recipe/ziyinbushuui/) | [健脾养胃](https://home.meishichina.com/recipe/jianbiyangwei/) | [护肝明目](https://home.meishichina.com/recipe/huganmingmu/) | +| -------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------- | +| [zhuangyang](https://rsshub.app/meishichina/recipe/zhuangyang) | [bianmi](https://rsshub.app/meishichina/recipe/bianmi) | [paiduyangyan](https://rsshub.app/meishichina/recipe/paiduyangyan) | [ziyinbushuui](https://rsshub.app/meishichina/recipe/ziyinbushuui) | [jianbiyangwei](https://rsshub.app/meishichina/recipe/jianbiyangwei) | [huganmingmu](https://rsshub.app/meishichina/recipe/huganmingmu) | + +| [清肺止咳](https://home.meishichina.com/recipe/qingfeizhike/) | [下奶](https://home.meishichina.com/recipe/xianai/) | [补钙](https://home.meishichina.com/recipe/bugai/) | [醒酒](https://home.meishichina.com/recipe/xingjiu/) | [抗过敏](https://home.meishichina.com/recipe/kangguomin/) | [防辐射](https://home.meishichina.com/recipe/fangfushe/) | +| ------------------------------------------------------------------ | ------------------------------------------------------ | ---------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------ | +| [qingfeizhike](https://rsshub.app/meishichina/recipe/qingfeizhike) | [xianai](https://rsshub.app/meishichina/recipe/xianai) | [bugai](https://rsshub.app/meishichina/recipe/bugai) | [xingjiu](https://rsshub.app/meishichina/recipe/xingjiu) | [kangguomin](https://rsshub.app/meishichina/recipe/kangguomin) | [fangfushe](https://rsshub.app/meishichina/recipe/fangfushe) | + +| [提高免疫力](https://home.meishichina.com/recipe/tigaomianyili/) | [流感](https://home.meishichina.com/recipe/liugan/) | [驱寒暖身](https://home.meishichina.com/recipe/quhannuanshen/) | [秋冬进补](https://home.meishichina.com/recipe/qiudongjinbu/) | [消暑解渴](https://home.meishichina.com/recipe/xiaoshujieke/) | +| -------------------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| [tigaomianyili](https://rsshub.app/meishichina/recipe/tigaomianyili) | [liugan](https://rsshub.app/meishichina/recipe/liugan) | [quhannuanshen](https://rsshub.app/meishichina/recipe/quhannuanshen) | [qiudongjinbu](https://rsshub.app/meishichina/recipe/qiudongjinbu) | [xiaoshujieke](https://rsshub.app/meishichina/recipe/xiaoshujieke) | + +#### 场景 + +| [早餐](https://home.meishichina.com/recipe/zaocan/) | [下午茶](https://home.meishichina.com/recipe/xiawucha/) | [二人世界](https://home.meishichina.com/recipe/erren/) | [野餐](https://home.meishichina.com/recipe/yecan/) | [开胃菜](https://home.meishichina.com/recipe/kaiweicai/) | [私房菜](https://home.meishichina.com/recipe/sifangcai/) | +| ------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| [zaocan](https://rsshub.app/meishichina/recipe/zaocan) | [xiawucha](https://rsshub.app/meishichina/recipe/xiawucha) | [erren](https://rsshub.app/meishichina/recipe/erren) | [yecan](https://rsshub.app/meishichina/recipe/yecan) | [kaiweicai](https://rsshub.app/meishichina/recipe/kaiweicai) | [sifangcai](https://rsshub.app/meishichina/recipe/sifangcai) | + +| [快餐](https://home.meishichina.com/recipe/kuaican/) | [快手菜](https://home.meishichina.com/recipe/kuaishoucai/) | [宿舍时代](https://home.meishichina.com/recipe/susheshidai/) | [中式宴请](https://home.meishichina.com/recipe/zhongshiyanqing/) | [西式宴请](https://home.meishichina.com/recipe/xishiyanqing/) | +| -------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------ | +| [kuaican](https://rsshub.app/meishichina/recipe/kuaican) | [kuaishoucai](https://rsshub.app/meishichina/recipe/kuaishoucai) | [susheshidai](https://rsshub.app/meishichina/recipe/susheshidai) | [zhongshiyanqing](https://rsshub.app/meishichina/recipe/zhongshiyanqing) | [xishiyanqing](https://rsshub.app/meishichina/recipe/xishiyanqing) | + +#### 饮食方式 + +| [素食](https://home.meishichina.com/recipe/sushi/) | [素菜](https://home.meishichina.com/recipe/sucai2/) | [清真菜](https://home.meishichina.com/recipe/qingzhencai/) | [春季食谱](https://home.meishichina.com/recipe/chunji/) | [夏季食谱](https://home.meishichina.com/recipe/xiaji/) | [秋季食谱](https://home.meishichina.com/recipe/qiuji/) | +| ---------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | +| [sushi](https://rsshub.app/meishichina/recipe/sushi) | [sucai2](https://rsshub.app/meishichina/recipe/sucai2) | [qingzhencai](https://rsshub.app/meishichina/recipe/qingzhencai) | [chunji](https://rsshub.app/meishichina/recipe/chunji) | [xiaji](https://rsshub.app/meishichina/recipe/xiaji) | [qiuji](https://rsshub.app/meishichina/recipe/qiuji) | + +| [冬季食谱](https://home.meishichina.com/recipe/dongji/) | [小清新](https://home.meishichina.com/recipe/xiaoqingxin/) | [高颜值](https://home.meishichina.com/recipe/gaoyanzhi/) | +| ------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------ | +| [dongji](https://rsshub.app/meishichina/recipe/dongji) | [xiaoqingxin](https://rsshub.app/meishichina/recipe/xiaoqingxin) | [gaoyanzhi](https://rsshub.app/meishichina/recipe/gaoyanzhi) | + +#### 中式菜系 + +| [川菜](https://home.meishichina.com/recipe/chuancai/) | [鲁菜](https://home.meishichina.com/recipe/lucai/) | [闽菜](https://home.meishichina.com/recipe/mincai/) | [粤菜](https://home.meishichina.com/recipe/yuecai/) | [苏菜](https://home.meishichina.com/recipe/sucai/) | [浙菜](https://home.meishichina.com/recipe/zhecai/) | +| ---------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | ---------------------------------------------------- | ------------------------------------------------------ | +| [chuancai](https://rsshub.app/meishichina/recipe/chuancai) | [lucai](https://rsshub.app/meishichina/recipe/lucai) | [mincai](https://rsshub.app/meishichina/recipe/mincai) | [yuecai](https://rsshub.app/meishichina/recipe/yuecai) | [sucai](https://rsshub.app/meishichina/recipe/sucai) | [zhecai](https://rsshub.app/meishichina/recipe/zhecai) | + +| [湘菜](https://home.meishichina.com/recipe/xiangcai/) | [徽菜](https://home.meishichina.com/recipe/huicai/) | [淮扬菜](https://home.meishichina.com/recipe/huaiyangcai/) | [豫菜](https://home.meishichina.com/recipe/yucai/) | [晋菜](https://home.meishichina.com/recipe/jincai/) | [鄂菜](https://home.meishichina.com/recipe/ecai/) | +| ---------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------- | +| [xiangcai](https://rsshub.app/meishichina/recipe/xiangcai) | [huicai](https://rsshub.app/meishichina/recipe/huicai) | [huaiyangcai](https://rsshub.app/meishichina/recipe/huaiyangcai) | [yucai](https://rsshub.app/meishichina/recipe/yucai) | [jincai](https://rsshub.app/meishichina/recipe/jincai) | [ecai](https://rsshub.app/meishichina/recipe/ecai) | + +| [云南菜](https://home.meishichina.com/recipe/yunnancai/) | [北京菜](https://home.meishichina.com/recipe/beijingcai/) | [东北菜](https://home.meishichina.com/recipe/dongbeicai/) | [西北菜](https://home.meishichina.com/recipe/xibeicai/) | [贵州菜](https://home.meishichina.com/recipe/guizhoucai/) | [上海菜](https://home.meishichina.com/recipe/shanghaicai/) | +| ------------------------------------------------------------ | -------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------- | +| [yunnancai](https://rsshub.app/meishichina/recipe/yunnancai) | [beijingcai](https://rsshub.app/meishichina/recipe/beijingcai) | [dongbeicai](https://rsshub.app/meishichina/recipe/dongbeicai) | [xibeicai](https://rsshub.app/meishichina/recipe/xibeicai) | [guizhoucai](https://rsshub.app/meishichina/recipe/guizhoucai) | [shanghaicai](https://rsshub.app/meishichina/recipe/shanghaicai) | + +| [新疆菜](https://home.meishichina.com/recipe/xinjiangcai/) | [客家菜](https://home.meishichina.com/recipe/kejiacai/) | [台湾美食](https://home.meishichina.com/recipe/taiwancai/) | [香港美食](https://home.meishichina.com/recipe/xianggangcai/) | [澳门美食](https://home.meishichina.com/recipe/aomeicai/) | [赣菜](https://home.meishichina.com/recipe/gancai/) | +| ---------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------ | +| [xinjiangcai](https://rsshub.app/meishichina/recipe/xinjiangcai) | [kejiacai](https://rsshub.app/meishichina/recipe/kejiacai) | [taiwancai](https://rsshub.app/meishichina/recipe/taiwancai) | [xianggangcai](https://rsshub.app/meishichina/recipe/xianggangcai) | [aomeicai](https://rsshub.app/meishichina/recipe/aomeicai) | [gancai](https://rsshub.app/meishichina/recipe/gancai) | + +| [中式菜系](https://home.meishichina.com/recipe/zhongshicaixi/) | +| -------------------------------------------------------------------- | +| [zhongshicaixi](https://rsshub.app/meishichina/recipe/zhongshicaixi) | + +#### 外国美食 + +| [日本料理](https://home.meishichina.com/recipe/ribencai/) | [韩国料理](https://home.meishichina.com/recipe/hanguocai/) | [泰国菜](https://home.meishichina.com/recipe/taiguocai/) | [印度菜](https://home.meishichina.com/recipe/yiducai/) | [法国菜](https://home.meishichina.com/recipe/faguocai/) | [意大利菜](https://home.meishichina.com/recipe/yidalicai/) | +| ---------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------ | +| [ribencai](https://rsshub.app/meishichina/recipe/ribencai) | [hanguocai](https://rsshub.app/meishichina/recipe/hanguocai) | [taiguocai](https://rsshub.app/meishichina/recipe/taiguocai) | [yiducai](https://rsshub.app/meishichina/recipe/yiducai) | [faguocai](https://rsshub.app/meishichina/recipe/faguocai) | [yidalicai](https://rsshub.app/meishichina/recipe/yidalicai) | + +| [西班牙菜](https://home.meishichina.com/recipe/xibanya/) | [英国菜](https://home.meishichina.com/recipe/yingguocai/) | [越南菜](https://home.meishichina.com/recipe/yuenancai/) | [墨西哥菜](https://home.meishichina.com/recipe/moxigecai/) | [外国美食](https://home.meishichina.com/recipe/waiguomeishi/) | +| -------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------ | +| [xibanya](https://rsshub.app/meishichina/recipe/xibanya) | [yingguocai](https://rsshub.app/meishichina/recipe/yingguocai) | [yuenancai](https://rsshub.app/meishichina/recipe/yuenancai) | [moxigecai](https://rsshub.app/meishichina/recipe/moxigecai) | [waiguomeishi](https://rsshub.app/meishichina/recipe/waiguomeishi) | + +#### 烘焙 + +| [蛋糕](https://home.meishichina.com/recipe/dangao/) | [面包](https://home.meishichina.com/recipe/mianbao/) | [饼干](https://home.meishichina.com/recipe/binggan/) | [派塔](https://home.meishichina.com/recipe/paita/) | [吐司](https://home.meishichina.com/recipe/tusi/) | [戚风蛋糕](https://home.meishichina.com/recipe/qifeng/) | +| ------------------------------------------------------ | -------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------- | +| [dangao](https://rsshub.app/meishichina/recipe/dangao) | [mianbao](https://rsshub.app/meishichina/recipe/mianbao) | [binggan](https://rsshub.app/meishichina/recipe/binggan) | [paita](https://rsshub.app/meishichina/recipe/paita) | [tusi](https://rsshub.app/meishichina/recipe/tusi) | [qifeng](https://rsshub.app/meishichina/recipe/qifeng) | + +| [纸杯蛋糕](https://home.meishichina.com/recipe/zhibei/) | [蛋糕卷](https://home.meishichina.com/recipe/dangaojuan/) | [玛芬蛋糕](https://home.meishichina.com/recipe/mafen/) | [乳酪蛋糕](https://home.meishichina.com/recipe/rulao/) | [芝士蛋糕](https://home.meishichina.com/recipe/zhishi/) | [奶油蛋糕](https://home.meishichina.com/recipe/naiyou/) | +| ------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------- | +| [zhibei](https://rsshub.app/meishichina/recipe/zhibei) | [dangaojuan](https://rsshub.app/meishichina/recipe/dangaojuan) | [mafen](https://rsshub.app/meishichina/recipe/mafen) | [rulao](https://rsshub.app/meishichina/recipe/rulao) | [zhishi](https://rsshub.app/meishichina/recipe/zhishi) | [naiyou](https://rsshub.app/meishichina/recipe/naiyou) | + +| [批萨](https://home.meishichina.com/recipe/pisa/) | [慕斯](https://home.meishichina.com/recipe/musi/) | [曲奇](https://home.meishichina.com/recipe/quqi/) | [翻糖](https://home.meishichina.com/recipe/fantang/) | +| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------- | +| [pisa](https://rsshub.app/meishichina/recipe/pisa) | [musi](https://rsshub.app/meishichina/recipe/musi) | [quqi](https://rsshub.app/meishichina/recipe/quqi) | [fantang](https://rsshub.app/meishichina/recipe/fantang) | + +#### 传统美食 + +| [粽子](https://home.meishichina.com/recipe/zongzi/) | [月饼](https://home.meishichina.com/recipe/yuebing/) | [春饼](https://home.meishichina.com/recipe/chunbing/) | [元宵](https://home.meishichina.com/recipe/yuanxiao/) | [汤圆](https://home.meishichina.com/recipe/tangyuan/) | [青团](https://home.meishichina.com/recipe/qingtuan/) | +| ------------------------------------------------------ | -------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | +| [zongzi](https://rsshub.app/meishichina/recipe/zongzi) | [yuebing](https://rsshub.app/meishichina/recipe/yuebing) | [chunbing](https://rsshub.app/meishichina/recipe/chunbing) | [yuanxiao](https://rsshub.app/meishichina/recipe/yuanxiao) | [tangyuan](https://rsshub.app/meishichina/recipe/tangyuan) | [qingtuan](https://rsshub.app/meishichina/recipe/qingtuan) | + +| [腊八粥](https://home.meishichina.com/recipe/labazhou/) | [春卷](https://home.meishichina.com/recipe/chunjuan/) | [传统美食](https://home.meishichina.com/recipe/chuantongmeishi/) | +| ---------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------ | +| [labazhou](https://rsshub.app/meishichina/recipe/labazhou) | [chunjuan](https://rsshub.app/meishichina/recipe/chunjuan) | [chuantongmeishi](https://rsshub.app/meishichina/recipe/chuantongmeishi) | + +#### 节日食俗 + +| [立冬](https://home.meishichina.com/recipe/lidong/) | [冬至](https://home.meishichina.com/recipe/dongzhi/) | [腊八](https://home.meishichina.com/recipe/laba/) | [端午节](https://home.meishichina.com/recipe/duanwu/) | [中秋](https://home.meishichina.com/recipe/zhongqiu/) | [立春](https://home.meishichina.com/recipe/lichun/) | +| ------------------------------------------------------ | -------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------ | +| [lidong](https://rsshub.app/meishichina/recipe/lidong) | [dongzhi](https://rsshub.app/meishichina/recipe/dongzhi) | [laba](https://rsshub.app/meishichina/recipe/laba) | [duanwu](https://rsshub.app/meishichina/recipe/duanwu) | [zhongqiu](https://rsshub.app/meishichina/recipe/zhongqiu) | [lichun](https://rsshub.app/meishichina/recipe/lichun) | + +| [元宵节](https://home.meishichina.com/recipe/yuanxiaojie/) | [贴秋膘](https://home.meishichina.com/recipe/tieqiubiao/) | [清明](https://home.meishichina.com/recipe/qingming/) | [年夜饭](https://home.meishichina.com/recipe/nianyefan/) | [圣诞节](https://home.meishichina.com/recipe/shengdanjie/) | [感恩节](https://home.meishichina.com/recipe/ganenjie/) | +| ---------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- | ---------------------------------------------------------- | +| [yuanxiaojie](https://rsshub.app/meishichina/recipe/yuanxiaojie) | [tieqiubiao](https://rsshub.app/meishichina/recipe/tieqiubiao) | [qingming](https://rsshub.app/meishichina/recipe/qingming) | [nianyefan](https://rsshub.app/meishichina/recipe/nianyefan) | [shengdanjie](https://rsshub.app/meishichina/recipe/shengdanjie) | [ganenjie](https://rsshub.app/meishichina/recipe/ganenjie) | + +| [万圣节](https://home.meishichina.com/recipe/wanshengjie/) | [情人节](https://home.meishichina.com/recipe/qingrenjie/) | [复活节](https://home.meishichina.com/recipe/fuhuojie/) | [雨水](https://home.meishichina.com/recipe/yushui/) | [惊蛰](https://home.meishichina.com/recipe/jingzhi/) | [春分](https://home.meishichina.com/recipe/chunfen/) | +| ---------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------- | -------------------------------------------------------- | +| [wanshengjie](https://rsshub.app/meishichina/recipe/wanshengjie) | [qingrenjie](https://rsshub.app/meishichina/recipe/qingrenjie) | [fuhuojie](https://rsshub.app/meishichina/recipe/fuhuojie) | [yushui](https://rsshub.app/meishichina/recipe/yushui) | [jingzhi](https://rsshub.app/meishichina/recipe/jingzhi) | [chunfen](https://rsshub.app/meishichina/recipe/chunfen) | + +| [谷雨](https://home.meishichina.com/recipe/guyu/) | [立夏](https://home.meishichina.com/recipe/lixia/) | [小满](https://home.meishichina.com/recipe/xiaoman/) | [芒种](https://home.meishichina.com/recipe/mangzhong/) | [夏至](https://home.meishichina.com/recipe/xiazhi/) | [小暑](https://home.meishichina.com/recipe/xiaoshu/) | +| -------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ | -------------------------------------------------------- | +| [guyu](https://rsshub.app/meishichina/recipe/guyu) | [lixia](https://rsshub.app/meishichina/recipe/lixia) | [xiaoman](https://rsshub.app/meishichina/recipe/xiaoman) | [mangzhong](https://rsshub.app/meishichina/recipe/mangzhong) | [xiazhi](https://rsshub.app/meishichina/recipe/xiazhi) | [xiaoshu](https://rsshub.app/meishichina/recipe/xiaoshu) | + +| [大暑](https://home.meishichina.com/recipe/dashu/) | [立秋](https://home.meishichina.com/recipe/xiqiu/) | [处暑](https://home.meishichina.com/recipe/chushu/) | [白露](https://home.meishichina.com/recipe/bailu/) | [秋分](https://home.meishichina.com/recipe/qiufen/) | [寒露](https://home.meishichina.com/recipe/hanlu/) | +| ---------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------- | +| [dashu](https://rsshub.app/meishichina/recipe/dashu) | [xiqiu](https://rsshub.app/meishichina/recipe/xiqiu) | [chushu](https://rsshub.app/meishichina/recipe/chushu) | [bailu](https://rsshub.app/meishichina/recipe/bailu) | [qiufen](https://rsshub.app/meishichina/recipe/qiufen) | [hanlu](https://rsshub.app/meishichina/recipe/hanlu) | + +| [霜降](https://home.meishichina.com/recipe/shuangjiang/) | [小雪](https://home.meishichina.com/recipe/xiaoxue/) | [大雪](https://home.meishichina.com/recipe/daxue/) | [小寒](https://home.meishichina.com/recipe/xiaohan/) | [大寒](https://home.meishichina.com/recipe/dahan/) | [二月二](https://home.meishichina.com/recipe/eryueer/) | +| ---------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------- | +| [shuangjiang](https://rsshub.app/meishichina/recipe/shuangjiang) | [xiaoxue](https://rsshub.app/meishichina/recipe/xiaoxue) | [daxue](https://rsshub.app/meishichina/recipe/daxue) | [xiaohan](https://rsshub.app/meishichina/recipe/xiaohan) | [dahan](https://rsshub.app/meishichina/recipe/dahan) | [eryueer](https://rsshub.app/meishichina/recipe/eryueer) | + +| [母亲节](https://home.meishichina.com/recipe/muqinjie/) | [父亲节](https://home.meishichina.com/recipe/fuqinjie/) | [儿童节](https://home.meishichina.com/recipe/ertongjie/) | [七夕](https://home.meishichina.com/recipe/qixi/) | [重阳节](https://home.meishichina.com/recipe/chongyangjie/) | [节日习俗](https://home.meishichina.com/recipe/jierixisu/) | +| ---------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------ | +| [muqinjie](https://rsshub.app/meishichina/recipe/muqinjie) | [fuqinjie](https://rsshub.app/meishichina/recipe/fuqinjie) | [ertongjie](https://rsshub.app/meishichina/recipe/ertongjie) | [qixi](https://rsshub.app/meishichina/recipe/qixi) | [chongyangjie](https://rsshub.app/meishichina/recipe/chongyangjie) | [jierixisu](https://rsshub.app/meishichina/recipe/jierixisu) | + +#### 按制作难度 + +| [简单](https://home.meishichina.com/recipe-type-do-level-view-1.html) | [普通](https://home.meishichina.com/recipe-type-do-level-view-2.html) | [高级](https://home.meishichina.com/recipe-type-do-level-view-3.html) | [神级](https://home.meishichina.com/recipe-type-do-level-view-4.html) | +| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| [recipe-type-do-level-view-1](https://rsshub.app/meishichina/recipe/recipe-type-do-level-view-1) | [recipe-type-do-level-view-2](https://rsshub.app/meishichina/recipe/recipe-type-do-level-view-2) | [recipe-type-do-level-view-3](https://rsshub.app/meishichina/recipe/recipe-type-do-level-view-3) | [recipe-type-do-level-view-4](https://rsshub.app/meishichina/recipe/recipe-type-do-level-view-4) | + +#### 按所需时间 + +| [十分钟](https://home.meishichina.com/recipe-type-do-during-view-1.html) | [廿分钟](https://home.meishichina.com/recipe-type-do-during-view-2.html) | [半小时](https://home.meishichina.com/recipe-type-do-during-view-3.html) | [三刻钟](https://home.meishichina.com/recipe-type-do-during-view-4.html) | [一小时](https://home.meishichina.com/recipe-type-do-during-view-5.html) | [数小时](https://home.meishichina.com/recipe-type-do-during-view-6.html) | +| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| [recipe-type-do-during-view-1](https://rsshub.app/meishichina/recipe/recipe-type-do-during-view-1) | [recipe-type-do-during-view-2](https://rsshub.app/meishichina/recipe/recipe-type-do-during-view-2) | [recipe-type-do-during-view-3](https://rsshub.app/meishichina/recipe/recipe-type-do-during-view-3) | [recipe-type-do-during-view-4](https://rsshub.app/meishichina/recipe/recipe-type-do-during-view-4) | [recipe-type-do-during-view-5](https://rsshub.app/meishichina/recipe/recipe-type-do-during-view-5) | [recipe-type-do-during-view-6](https://rsshub.app/meishichina/recipe/recipe-type-do-during-view-6) | + +| [一天](https://home.meishichina.com/recipe-type-do-during-view-7.html) | [数天](https://home.meishichina.com/recipe-type-do-during-view-8.html) | +| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| [recipe-type-do-during-view-7](https://rsshub.app/meishichina/recipe/recipe-type-do-during-view-7) | [recipe-type-do-during-view-8](https://rsshub.app/meishichina/recipe/recipe-type-do-during-view-8) | + +#### 按菜品口味 + +| [微辣](https://home.meishichina.com/recipe-type-do-cuisine-view-1.html) | [中辣](https://home.meishichina.com/recipe-type-do-cuisine-view-2.html) | [超辣](https://home.meishichina.com/recipe-type-do-cuisine-view-3.html) | [麻辣](https://home.meishichina.com/recipe-type-do-cuisine-view-4.html) | [酸辣](https://home.meishichina.com/recipe-type-do-cuisine-view-5.html) | [甜辣](https://home.meishichina.com/recipe-type-do-cuisine-view-29.html) | +| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| [recipe-type-do-cuisine-view-1](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-1) | [recipe-type-do-cuisine-view-2](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-2) | [recipe-type-do-cuisine-view-3](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-3) | [recipe-type-do-cuisine-view-4](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-4) | [recipe-type-do-cuisine-view-5](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-5) | [recipe-type-do-cuisine-view-29](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-29) | + +| [香辣](https://home.meishichina.com/recipe-type-do-cuisine-view-31.html) | [酸甜](https://home.meishichina.com/recipe-type-do-cuisine-view-6.html) | [酸咸](https://home.meishichina.com/recipe-type-do-cuisine-view-7.html) | [咸鲜](https://home.meishichina.com/recipe-type-do-cuisine-view-8.html) | [咸甜](https://home.meishichina.com/recipe-type-do-cuisine-view-9.html) | [甜味](https://home.meishichina.com/recipe-type-do-cuisine-view-10.html) | +| ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| [recipe-type-do-cuisine-view-31](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-31) | [recipe-type-do-cuisine-view-6](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-6) | [recipe-type-do-cuisine-view-7](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-7) | [recipe-type-do-cuisine-view-8](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-8) | [recipe-type-do-cuisine-view-9](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-9) | [recipe-type-do-cuisine-view-10](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-10) | + +| [苦味](https://home.meishichina.com/recipe-type-do-cuisine-view-11.html) | [原味](https://home.meishichina.com/recipe-type-do-cuisine-view-12.html) | [清淡](https://home.meishichina.com/recipe-type-do-cuisine-view-13.html) | [五香](https://home.meishichina.com/recipe-type-do-cuisine-view-14.html) | [鱼香](https://home.meishichina.com/recipe-type-do-cuisine-view-15.html) | [葱香](https://home.meishichina.com/recipe-type-do-cuisine-view-16.html) | +| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| [recipe-type-do-cuisine-view-11](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-11) | [recipe-type-do-cuisine-view-12](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-12) | [recipe-type-do-cuisine-view-13](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-13) | [recipe-type-do-cuisine-view-14](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-14) | [recipe-type-do-cuisine-view-15](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-15) | [recipe-type-do-cuisine-view-16](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-16) | + +| [蒜香](https://home.meishichina.com/recipe-type-do-cuisine-view-17.html) | [奶香](https://home.meishichina.com/recipe-type-do-cuisine-view-18.html) | [酱香](https://home.meishichina.com/recipe-type-do-cuisine-view-19.html) | [糟香](https://home.meishichina.com/recipe-type-do-cuisine-view-20.html) | [咖喱](https://home.meishichina.com/recipe-type-do-cuisine-view-21.html) | [孜然](https://home.meishichina.com/recipe-type-do-cuisine-view-22.html) | +| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| [recipe-type-do-cuisine-view-17](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-17) | [recipe-type-do-cuisine-view-18](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-18) | [recipe-type-do-cuisine-view-19](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-19) | [recipe-type-do-cuisine-view-20](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-20) | [recipe-type-do-cuisine-view-21](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-21) | [recipe-type-do-cuisine-view-22](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-22) | + +| [果味](https://home.meishichina.com/recipe-type-do-cuisine-view-23.html) | [香草](https://home.meishichina.com/recipe-type-do-cuisine-view-24.html) | [怪味](https://home.meishichina.com/recipe-type-do-cuisine-view-25.html) | [咸香](https://home.meishichina.com/recipe-type-do-cuisine-view-26.html) | [甜香](https://home.meishichina.com/recipe-type-do-cuisine-view-27.html) | [麻香](https://home.meishichina.com/recipe-type-do-cuisine-view-28.html) | +| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| [recipe-type-do-cuisine-view-23](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-23) | [recipe-type-do-cuisine-view-24](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-24) | [recipe-type-do-cuisine-view-25](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-25) | [recipe-type-do-cuisine-view-26](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-26) | [recipe-type-do-cuisine-view-27](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-27) | [recipe-type-do-cuisine-view-28](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-28) | + +| [其他](https://home.meishichina.com/recipe-type-do-cuisine-view-50.html) | +| ------------------------------------------------------------------------------------------------------ | +| [recipe-type-do-cuisine-view-50](https://rsshub.app/meishichina/recipe/recipe-type-do-cuisine-view-50) | + +#### 按主要工艺 + +| [烧](https://home.meishichina.com/recipe-type-do-technics-view-1.html) | [炒](https://home.meishichina.com/recipe-type-do-technics-view-2.html) | [爆](https://home.meishichina.com/recipe-type-do-technics-view-3.html) | [焖](https://home.meishichina.com/recipe-type-do-technics-view-4.html) | [炖](https://home.meishichina.com/recipe-type-do-technics-view-5.html) | [蒸](https://home.meishichina.com/recipe-type-do-technics-view-6.html) | +| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| [recipe-type-do-technics-view-1](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-1) | [recipe-type-do-technics-view-2](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-2) | [recipe-type-do-technics-view-3](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-3) | [recipe-type-do-technics-view-4](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-4) | [recipe-type-do-technics-view-5](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-5) | [recipe-type-do-technics-view-6](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-6) | + +| [煮](https://home.meishichina.com/recipe-type-do-technics-view-7.html) | [拌](https://home.meishichina.com/recipe-type-do-technics-view-8.html) | [烤](https://home.meishichina.com/recipe-type-do-technics-view-9.html) | [炸](https://home.meishichina.com/recipe-type-do-technics-view-10.html) | [烩](https://home.meishichina.com/recipe-type-do-technics-view-11.html) | [溜](https://home.meishichina.com/recipe-type-do-technics-view-12.html) | +| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [recipe-type-do-technics-view-7](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-7) | [recipe-type-do-technics-view-8](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-8) | [recipe-type-do-technics-view-9](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-9) | [recipe-type-do-technics-view-10](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-10) | [recipe-type-do-technics-view-11](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-11) | [recipe-type-do-technics-view-12](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-12) | + +| [氽](https://home.meishichina.com/recipe-type-do-technics-view-13.html) | [腌](https://home.meishichina.com/recipe-type-do-technics-view-14.html) | [卤](https://home.meishichina.com/recipe-type-do-technics-view-15.html) | [炝](https://home.meishichina.com/recipe-type-do-technics-view-16.html) | [煎](https://home.meishichina.com/recipe-type-do-technics-view-17.html) | [酥](https://home.meishichina.com/recipe-type-do-technics-view-18.html) | +| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [recipe-type-do-technics-view-13](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-13) | [recipe-type-do-technics-view-14](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-14) | [recipe-type-do-technics-view-15](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-15) | [recipe-type-do-technics-view-16](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-16) | [recipe-type-do-technics-view-17](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-17) | [recipe-type-do-technics-view-18](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-18) | + +| [扒](https://home.meishichina.com/recipe-type-do-technics-view-19.html) | [熏](https://home.meishichina.com/recipe-type-do-technics-view-20.html) | [煨](https://home.meishichina.com/recipe-type-do-technics-view-21.html) | [酱](https://home.meishichina.com/recipe-type-do-technics-view-22.html) | [煲](https://home.meishichina.com/recipe-type-do-technics-view-30.html) | [烘焙](https://home.meishichina.com/recipe-type-do-technics-view-23.html) | +| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [recipe-type-do-technics-view-19](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-19) | [recipe-type-do-technics-view-20](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-20) | [recipe-type-do-technics-view-21](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-21) | [recipe-type-do-technics-view-22](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-22) | [recipe-type-do-technics-view-30](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-30) | [recipe-type-do-technics-view-23](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-23) | + +| [火锅](https://home.meishichina.com/recipe-type-do-technics-view-24.html) | [砂锅](https://home.meishichina.com/recipe-type-do-technics-view-25.html) | [拔丝](https://home.meishichina.com/recipe-type-do-technics-view-26.html) | [生鲜](https://home.meishichina.com/recipe-type-do-technics-view-27.html) | [调味](https://home.meishichina.com/recipe-type-do-technics-view-28.html) | [技巧](https://home.meishichina.com/recipe-type-do-technics-view-29.html) | +| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [recipe-type-do-technics-view-24](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-24) | [recipe-type-do-technics-view-25](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-25) | [recipe-type-do-technics-view-26](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-26) | [recipe-type-do-technics-view-27](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-27) | [recipe-type-do-technics-view-28](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-28) | [recipe-type-do-technics-view-29](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-29) | + +| [烙](https://home.meishichina.com/recipe-type-do-technics-view-31.html) | [榨汁](https://home.meishichina.com/recipe-type-do-technics-view-32.html) | [冷冻](https://home.meishichina.com/recipe-type-do-technics-view-33.html) | [焗](https://home.meishichina.com/recipe-type-do-technics-view-34.html) | [焯](https://home.meishichina.com/recipe-type-do-technics-view-35.html) | [干煸](https://home.meishichina.com/recipe-type-do-technics-view-36.html) | +| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [recipe-type-do-technics-view-31](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-31) | [recipe-type-do-technics-view-32](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-32) | [recipe-type-do-technics-view-33](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-33) | [recipe-type-do-technics-view-34](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-34) | [recipe-type-do-technics-view-35](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-35) | [recipe-type-do-technics-view-36](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-36) | + +| [干锅](https://home.meishichina.com/recipe-type-do-technics-view-37.html) | [铁板](https://home.meishichina.com/recipe-type-do-technics-view-38.html) | [微波](https://home.meishichina.com/recipe-type-do-technics-view-39.html) | [其他](https://home.meishichina.com/recipe-type-do-technics-view-50.html) | +| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [recipe-type-do-technics-view-37](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-37) | [recipe-type-do-technics-view-38](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-38) | [recipe-type-do-technics-view-39](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-39) | [recipe-type-do-technics-view-50](https://rsshub.app/meishichina/recipe/recipe-type-do-technics-view-50) | + +</details> + `, + categories: ['new-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + title: '最新推荐', + source: ['home.meishichina.com/recipe.html'], + target: '/recipe/最新推荐', + }, + { + title: '最新发布', + source: ['home.meishichina.com/recipe.html'], + target: '/recipe/最新发布', + }, + { + title: '热菜', + source: ['home.meishichina.com/recipe.html'], + target: '/recipe/热菜', + }, + { + title: '凉菜', + source: ['home.meishichina.com/recipe.html'], + target: '/recipe/凉菜', + }, + { + title: '汤羹', + source: ['home.meishichina.com/recipe.html'], + target: '/recipe/汤羹', + }, + { + title: '主食', + source: ['home.meishichina.com/recipe.html'], + target: '/recipe/主食', + }, + { + title: '小吃', + source: ['home.meishichina.com/recipe.html'], + target: '/recipe/小吃', + }, + { + title: '西餐', + source: ['home.meishichina.com/recipe.html'], + target: '/recipe/西餐', + }, + { + title: '烘焙', + source: ['home.meishichina.com/recipe.html'], + target: '/recipe/烘焙', + }, + { + title: '自制食材', + source: ['home.meishichina.com/recipe.html'], + target: '/recipe/自制食材', + }, + { + title: '常见菜式 - 热菜', + source: ['home.meishichina.com/recipe/recai/'], + target: '/recipe/recai', + }, + { + title: '常见菜式 - 凉菜', + source: ['home.meishichina.com/recipe/liangcai/'], + target: '/recipe/liangcai', + }, + { + title: '常见菜式 - 汤羹', + source: ['home.meishichina.com/recipe/tanggeng/'], + target: '/recipe/tanggeng', + }, + { + title: '常见菜式 - 主食', + source: ['home.meishichina.com/recipe/zhushi/'], + target: '/recipe/zhushi', + }, + { + title: '常见菜式 - 小吃', + source: ['home.meishichina.com/recipe/xiaochi/'], + target: '/recipe/xiaochi', + }, + { + title: '常见菜式 - 家常菜', + source: ['home.meishichina.com/recipe/jiachang/'], + target: '/recipe/jiachang', + }, + { + title: '常见菜式 - 泡酱腌菜', + source: ['home.meishichina.com/recipe/jiangpaoyancai/'], + target: '/recipe/jiangpaoyancai', + }, + { + title: '常见菜式 - 西餐', + source: ['home.meishichina.com/recipe/xican/'], + target: '/recipe/xican', + }, + { + title: '常见菜式 - 烘焙', + source: ['home.meishichina.com/recipe/hongbei/'], + target: '/recipe/hongbei', + }, + { + title: '常见菜式 - 烤箱菜', + source: ['home.meishichina.com/recipe/kaoxiangcai/'], + target: '/recipe/kaoxiangcai', + }, + { + title: '常见菜式 - 饮品', + source: ['home.meishichina.com/recipe/yinpin/'], + target: '/recipe/yinpin', + }, + { + title: '常见菜式 - 零食', + source: ['home.meishichina.com/recipe/lingshi/'], + target: '/recipe/lingshi', + }, + { + title: '常见菜式 - 火锅', + source: ['home.meishichina.com/recipe/huoguo/'], + target: '/recipe/huoguo', + }, + { + title: '常见菜式 - 自制食材', + source: ['home.meishichina.com/recipe/zizhishicai/'], + target: '/recipe/zizhishicai', + }, + { + title: '常见菜式 - 海鲜', + source: ['home.meishichina.com/recipe/haixian/'], + target: '/recipe/haixian', + }, + { + title: '常见菜式 - 宴客菜', + source: ['home.meishichina.com/recipe/yankecai/'], + target: '/recipe/yankecai', + }, + { + title: '主食/小吃 - 米饭', + source: ['home.meishichina.com/recipe/mifan/'], + target: '/recipe/mifan', + }, + { + title: '主食/小吃 - 炒饭', + source: ['home.meishichina.com/recipe/chaofan/'], + target: '/recipe/chaofan', + }, + { + title: '主食/小吃 - 面食', + source: ['home.meishichina.com/recipe/mianshi/'], + target: '/recipe/mianshi', + }, + { + title: '主食/小吃 - 包子', + source: ['home.meishichina.com/recipe/baozi/'], + target: '/recipe/baozi', + }, + { + title: '主食/小吃 - 饺子', + source: ['home.meishichina.com/recipe/jiaozi/'], + target: '/recipe/jiaozi', + }, + { + title: '主食/小吃 - 馒头花卷', + source: ['home.meishichina.com/recipe/mantou/'], + target: '/recipe/mantou', + }, + { + title: '主食/小吃 - 面条', + source: ['home.meishichina.com/recipe/miantiao/'], + target: '/recipe/miantiao', + }, + { + title: '主食/小吃 - 饼', + source: ['home.meishichina.com/recipe/bing/'], + target: '/recipe/bing', + }, + { + title: '主食/小吃 - 粥', + source: ['home.meishichina.com/recipe/zhou/'], + target: '/recipe/zhou', + }, + { + title: '主食/小吃 - 馄饨', + source: ['home.meishichina.com/recipe/hundun/'], + target: '/recipe/hundun', + }, + { + title: '主食/小吃 - 五谷杂粮', + source: ['home.meishichina.com/recipe/wuguzaliang/'], + target: '/recipe/wuguzaliang', + }, + { + title: '主食/小吃 - 北京小吃', + source: ['home.meishichina.com/recipe/beijingxiaochi/'], + target: '/recipe/beijingxiaochi', + }, + { + title: '主食/小吃 - 陕西小吃', + source: ['home.meishichina.com/recipe/shanxixiaochi/'], + target: '/recipe/shanxixiaochi', + }, + { + title: '主食/小吃 - 广东小吃', + source: ['home.meishichina.com/recipe/guangdongxiaochi/'], + target: '/recipe/guangdongxiaochi', + }, + { + title: '主食/小吃 - 四川小吃', + source: ['home.meishichina.com/recipe/sichuanxiaochi/'], + target: '/recipe/sichuanxiaochi', + }, + { + title: '主食/小吃 - 重庆小吃', + source: ['home.meishichina.com/recipe/chongqingxiaochi/'], + target: '/recipe/chongqingxiaochi', + }, + { + title: '主食/小吃 - 天津小吃', + source: ['home.meishichina.com/recipe/tianjinxiaochi/'], + target: '/recipe/tianjinxiaochi', + }, + { + title: '主食/小吃 - 上海小吃', + source: ['home.meishichina.com/recipe/shanghaixiochi/'], + target: '/recipe/shanghaixiochi', + }, + { + title: '主食/小吃 - 福建小吃', + source: ['home.meishichina.com/recipe/fujianxiaochi/'], + target: '/recipe/fujianxiaochi', + }, + { + title: '主食/小吃 - 湖南小吃', + source: ['home.meishichina.com/recipe/hunanxiaochi/'], + target: '/recipe/hunanxiaochi', + }, + { + title: '主食/小吃 - 湖北小吃', + source: ['home.meishichina.com/recipe/hubeixiaochi/'], + target: '/recipe/hubeixiaochi', + }, + { + title: '主食/小吃 - 江西小吃', + source: ['home.meishichina.com/recipe/jiangxixiaochi/'], + target: '/recipe/jiangxixiaochi', + }, + { + title: '主食/小吃 - 山东小吃', + source: ['home.meishichina.com/recipe/shandongxiaochi/'], + target: '/recipe/shandongxiaochi', + }, + { + title: '主食/小吃 - 山西小吃', + source: ['home.meishichina.com/recipe/jinxiaochi/'], + target: '/recipe/jinxiaochi', + }, + { + title: '主食/小吃 - 河南小吃', + source: ['home.meishichina.com/recipe/henanxiaochi/'], + target: '/recipe/henanxiaochi', + }, + { + title: '主食/小吃 - 台湾小吃', + source: ['home.meishichina.com/recipe/taiwanxiaochi/'], + target: '/recipe/taiwanxiaochi', + }, + { + title: '主食/小吃 - 江浙小吃', + source: ['home.meishichina.com/recipe/jiangzhexiaochi/'], + target: '/recipe/jiangzhexiaochi', + }, + { + title: '主食/小吃 - 云贵小吃', + source: ['home.meishichina.com/recipe/yunguixiaochi/'], + target: '/recipe/yunguixiaochi', + }, + { + title: '主食/小吃 - 东北小吃', + source: ['home.meishichina.com/recipe/dongbeixiaochi/'], + target: '/recipe/dongbeixiaochi', + }, + { + title: '主食/小吃 - 西北小吃', + source: ['home.meishichina.com/recipe/xibeixiaochi/'], + target: '/recipe/xibeixiaochi', + }, + { + title: '甜品/饮品 - 甜品', + source: ['home.meishichina.com/recipe/tianpin/'], + target: '/recipe/tianpin', + }, + { + title: '甜品/饮品 - 冰品', + source: ['home.meishichina.com/recipe/bingpin/'], + target: '/recipe/bingpin', + }, + { + title: '甜品/饮品 - 果汁', + source: ['home.meishichina.com/recipe/guozhi/'], + target: '/recipe/guozhi', + }, + { + title: '甜品/饮品 - 糖水', + source: ['home.meishichina.com/recipe/tangshui/'], + target: '/recipe/tangshui', + }, + { + title: '甜品/饮品 - 布丁', + source: ['home.meishichina.com/recipe/buding/'], + target: '/recipe/buding', + }, + { + title: '甜品/饮品 - 果酱', + source: ['home.meishichina.com/recipe/guojiang/'], + target: '/recipe/guojiang', + }, + { + title: '甜品/饮品 - 果冻', + source: ['home.meishichina.com/recipe/guodong/'], + target: '/recipe/guodong', + }, + { + title: '甜品/饮品 - 酸奶', + source: ['home.meishichina.com/recipe/suannai/'], + target: '/recipe/suannai', + }, + { + title: '甜品/饮品 - 鸡尾酒', + source: ['home.meishichina.com/recipe/jiweijiu/'], + target: '/recipe/jiweijiu', + }, + { + title: '甜品/饮品 - 咖啡', + source: ['home.meishichina.com/recipe/kafei/'], + target: '/recipe/kafei', + }, + { + title: '甜品/饮品 - 豆浆', + source: ['home.meishichina.com/recipe/doujiang/'], + target: '/recipe/doujiang', + }, + { + title: '甜品/饮品 - 奶昔', + source: ['home.meishichina.com/recipe/naixi/'], + target: '/recipe/naixi', + }, + { + title: '甜品/饮品 - 冰淇淋', + source: ['home.meishichina.com/recipe/bingqilin/'], + target: '/recipe/bingqilin', + }, + { + title: '适宜人群 - 孕妇', + source: ['home.meishichina.com/recipe/yunfu/'], + target: '/recipe/yunfu', + }, + { + title: '适宜人群 - 产妇', + source: ['home.meishichina.com/recipe/chanfu/'], + target: '/recipe/chanfu', + }, + { + title: '适宜人群 - 婴儿', + source: ['home.meishichina.com/recipe/yinger/'], + target: '/recipe/yinger', + }, + { + title: '适宜人群 - 儿童', + source: ['home.meishichina.com/recipe/ertong/'], + target: '/recipe/ertong', + }, + { + title: '适宜人群 - 老人', + source: ['home.meishichina.com/recipe/laoren/'], + target: '/recipe/laoren', + }, + { + title: '适宜人群 - 幼儿', + source: ['home.meishichina.com/recipe/youer/'], + target: '/recipe/youer', + }, + { + title: '适宜人群 - 哺乳期', + source: ['home.meishichina.com/recipe/buruqi/'], + target: '/recipe/buruqi', + }, + { + title: '适宜人群 - 青少年', + source: ['home.meishichina.com/recipe/qingshaonian/'], + target: '/recipe/qingshaonian', + }, + { + title: '食疗食补 - 健康食谱', + source: ['home.meishichina.com/recipe/jiankangshipu/'], + target: '/recipe/jiankangshipu', + }, + { + title: '食疗食补 - 减肥瘦身', + source: ['home.meishichina.com/recipe/shoushen/'], + target: '/recipe/shoushen', + }, + { + title: '食疗食补 - 贫血', + source: ['home.meishichina.com/recipe/pinxue/'], + target: '/recipe/pinxue', + }, + { + title: '食疗食补 - 痛经', + source: ['home.meishichina.com/recipe/tongjing/'], + target: '/recipe/tongjing', + }, + { + title: '食疗食补 - 清热祛火', + source: ['home.meishichina.com/recipe/qingrequhuo/'], + target: '/recipe/qingrequhuo', + }, + { + title: '食疗食补 - 滋阴', + source: ['home.meishichina.com/recipe/ziyin/'], + target: '/recipe/ziyin', + }, + { + title: '食疗食补 - 壮阳', + source: ['home.meishichina.com/recipe/zhuangyang/'], + target: '/recipe/zhuangyang', + }, + { + title: '食疗食补 - 便秘', + source: ['home.meishichina.com/recipe/bianmi/'], + target: '/recipe/bianmi', + }, + { + title: '食疗食补 - 排毒养颜', + source: ['home.meishichina.com/recipe/paiduyangyan/'], + target: '/recipe/paiduyangyan', + }, + { + title: '食疗食补 - 滋润补水', + source: ['home.meishichina.com/recipe/ziyinbushuui/'], + target: '/recipe/ziyinbushuui', + }, + { + title: '食疗食补 - 健脾养胃', + source: ['home.meishichina.com/recipe/jianbiyangwei/'], + target: '/recipe/jianbiyangwei', + }, + { + title: '食疗食补 - 护肝明目', + source: ['home.meishichina.com/recipe/huganmingmu/'], + target: '/recipe/huganmingmu', + }, + { + title: '食疗食补 - 清肺止咳', + source: ['home.meishichina.com/recipe/qingfeizhike/'], + target: '/recipe/qingfeizhike', + }, + { + title: '食疗食补 - 下奶', + source: ['home.meishichina.com/recipe/xianai/'], + target: '/recipe/xianai', + }, + { + title: '食疗食补 - 补钙', + source: ['home.meishichina.com/recipe/bugai/'], + target: '/recipe/bugai', + }, + { + title: '食疗食补 - 醒酒', + source: ['home.meishichina.com/recipe/xingjiu/'], + target: '/recipe/xingjiu', + }, + { + title: '食疗食补 - 抗过敏', + source: ['home.meishichina.com/recipe/kangguomin/'], + target: '/recipe/kangguomin', + }, + { + title: '食疗食补 - 防辐射', + source: ['home.meishichina.com/recipe/fangfushe/'], + target: '/recipe/fangfushe', + }, + { + title: '食疗食补 - 提高免疫力', + source: ['home.meishichina.com/recipe/tigaomianyili/'], + target: '/recipe/tigaomianyili', + }, + { + title: '食疗食补 - 流感', + source: ['home.meishichina.com/recipe/liugan/'], + target: '/recipe/liugan', + }, + { + title: '食疗食补 - 驱寒暖身', + source: ['home.meishichina.com/recipe/quhannuanshen/'], + target: '/recipe/quhannuanshen', + }, + { + title: '食疗食补 - 秋冬进补', + source: ['home.meishichina.com/recipe/qiudongjinbu/'], + target: '/recipe/qiudongjinbu', + }, + { + title: '食疗食补 - 消暑解渴', + source: ['home.meishichina.com/recipe/xiaoshujieke/'], + target: '/recipe/xiaoshujieke', + }, + { + title: '场景 - 早餐', + source: ['home.meishichina.com/recipe/zaocan/'], + target: '/recipe/zaocan', + }, + { + title: '场景 - 下午茶', + source: ['home.meishichina.com/recipe/xiawucha/'], + target: '/recipe/xiawucha', + }, + { + title: '场景 - 二人世界', + source: ['home.meishichina.com/recipe/erren/'], + target: '/recipe/erren', + }, + { + title: '场景 - 野餐', + source: ['home.meishichina.com/recipe/yecan/'], + target: '/recipe/yecan', + }, + { + title: '场景 - 开胃菜', + source: ['home.meishichina.com/recipe/kaiweicai/'], + target: '/recipe/kaiweicai', + }, + { + title: '场景 - 私房菜', + source: ['home.meishichina.com/recipe/sifangcai/'], + target: '/recipe/sifangcai', + }, + { + title: '场景 - 快餐', + source: ['home.meishichina.com/recipe/kuaican/'], + target: '/recipe/kuaican', + }, + { + title: '场景 - 快手菜', + source: ['home.meishichina.com/recipe/kuaishoucai/'], + target: '/recipe/kuaishoucai', + }, + { + title: '场景 - 宿舍时代', + source: ['home.meishichina.com/recipe/susheshidai/'], + target: '/recipe/susheshidai', + }, + { + title: '场景 - 中式宴请', + source: ['home.meishichina.com/recipe/zhongshiyanqing/'], + target: '/recipe/zhongshiyanqing', + }, + { + title: '场景 - 西式宴请', + source: ['home.meishichina.com/recipe/xishiyanqing/'], + target: '/recipe/xishiyanqing', + }, + { + title: '饮食方式 - 素食', + source: ['home.meishichina.com/recipe/sushi/'], + target: '/recipe/sushi', + }, + { + title: '饮食方式 - 素菜', + source: ['home.meishichina.com/recipe/sucai2/'], + target: '/recipe/sucai2', + }, + { + title: '饮食方式 - 清真菜', + source: ['home.meishichina.com/recipe/qingzhencai/'], + target: '/recipe/qingzhencai', + }, + { + title: '饮食方式 - 春季食谱', + source: ['home.meishichina.com/recipe/chunji/'], + target: '/recipe/chunji', + }, + { + title: '饮食方式 - 夏季食谱', + source: ['home.meishichina.com/recipe/xiaji/'], + target: '/recipe/xiaji', + }, + { + title: '饮食方式 - 秋季食谱', + source: ['home.meishichina.com/recipe/qiuji/'], + target: '/recipe/qiuji', + }, + { + title: '饮食方式 - 冬季食谱', + source: ['home.meishichina.com/recipe/dongji/'], + target: '/recipe/dongji', + }, + { + title: '饮食方式 - 小清新', + source: ['home.meishichina.com/recipe/xiaoqingxin/'], + target: '/recipe/xiaoqingxin', + }, + { + title: '饮食方式 - 高颜值', + source: ['home.meishichina.com/recipe/gaoyanzhi/'], + target: '/recipe/gaoyanzhi', + }, + { + title: '中式菜系 - 川菜', + source: ['home.meishichina.com/recipe/chuancai/'], + target: '/recipe/chuancai', + }, + { + title: '中式菜系 - 鲁菜', + source: ['home.meishichina.com/recipe/lucai/'], + target: '/recipe/lucai', + }, + { + title: '中式菜系 - 闽菜', + source: ['home.meishichina.com/recipe/mincai/'], + target: '/recipe/mincai', + }, + { + title: '中式菜系 - 粤菜', + source: ['home.meishichina.com/recipe/yuecai/'], + target: '/recipe/yuecai', + }, + { + title: '中式菜系 - 苏菜', + source: ['home.meishichina.com/recipe/sucai/'], + target: '/recipe/sucai', + }, + { + title: '中式菜系 - 浙菜', + source: ['home.meishichina.com/recipe/zhecai/'], + target: '/recipe/zhecai', + }, + { + title: '中式菜系 - 湘菜', + source: ['home.meishichina.com/recipe/xiangcai/'], + target: '/recipe/xiangcai', + }, + { + title: '中式菜系 - 徽菜', + source: ['home.meishichina.com/recipe/huicai/'], + target: '/recipe/huicai', + }, + { + title: '中式菜系 - 淮扬菜', + source: ['home.meishichina.com/recipe/huaiyangcai/'], + target: '/recipe/huaiyangcai', + }, + { + title: '中式菜系 - 豫菜', + source: ['home.meishichina.com/recipe/yucai/'], + target: '/recipe/yucai', + }, + { + title: '中式菜系 - 晋菜', + source: ['home.meishichina.com/recipe/jincai/'], + target: '/recipe/jincai', + }, + { + title: '中式菜系 - 鄂菜', + source: ['home.meishichina.com/recipe/ecai/'], + target: '/recipe/ecai', + }, + { + title: '中式菜系 - 云南菜', + source: ['home.meishichina.com/recipe/yunnancai/'], + target: '/recipe/yunnancai', + }, + { + title: '中式菜系 - 北京菜', + source: ['home.meishichina.com/recipe/beijingcai/'], + target: '/recipe/beijingcai', + }, + { + title: '中式菜系 - 东北菜', + source: ['home.meishichina.com/recipe/dongbeicai/'], + target: '/recipe/dongbeicai', + }, + { + title: '中式菜系 - 西北菜', + source: ['home.meishichina.com/recipe/xibeicai/'], + target: '/recipe/xibeicai', + }, + { + title: '中式菜系 - 贵州菜', + source: ['home.meishichina.com/recipe/guizhoucai/'], + target: '/recipe/guizhoucai', + }, + { + title: '中式菜系 - 上海菜', + source: ['home.meishichina.com/recipe/shanghaicai/'], + target: '/recipe/shanghaicai', + }, + { + title: '中式菜系 - 新疆菜', + source: ['home.meishichina.com/recipe/xinjiangcai/'], + target: '/recipe/xinjiangcai', + }, + { + title: '中式菜系 - 客家菜', + source: ['home.meishichina.com/recipe/kejiacai/'], + target: '/recipe/kejiacai', + }, + { + title: '中式菜系 - 台湾美食', + source: ['home.meishichina.com/recipe/taiwancai/'], + target: '/recipe/taiwancai', + }, + { + title: '中式菜系 - 香港美食', + source: ['home.meishichina.com/recipe/xianggangcai/'], + target: '/recipe/xianggangcai', + }, + { + title: '中式菜系 - 澳门美食', + source: ['home.meishichina.com/recipe/aomeicai/'], + target: '/recipe/aomeicai', + }, + { + title: '中式菜系 - 赣菜', + source: ['home.meishichina.com/recipe/gancai/'], + target: '/recipe/gancai', + }, + { + title: '中式菜系 - 中式菜系', + source: ['home.meishichina.com/recipe/zhongshicaixi/'], + target: '/recipe/zhongshicaixi', + }, + { + title: '外国美食 - 日本料理', + source: ['home.meishichina.com/recipe/ribencai/'], + target: '/recipe/ribencai', + }, + { + title: '外国美食 - 韩国料理', + source: ['home.meishichina.com/recipe/hanguocai/'], + target: '/recipe/hanguocai', + }, + { + title: '外国美食 - 泰国菜', + source: ['home.meishichina.com/recipe/taiguocai/'], + target: '/recipe/taiguocai', + }, + { + title: '外国美食 - 印度菜', + source: ['home.meishichina.com/recipe/yiducai/'], + target: '/recipe/yiducai', + }, + { + title: '外国美食 - 法国菜', + source: ['home.meishichina.com/recipe/faguocai/'], + target: '/recipe/faguocai', + }, + { + title: '外国美食 - 意大利菜', + source: ['home.meishichina.com/recipe/yidalicai/'], + target: '/recipe/yidalicai', + }, + { + title: '外国美食 - 西班牙菜', + source: ['home.meishichina.com/recipe/xibanya/'], + target: '/recipe/xibanya', + }, + { + title: '外国美食 - 英国菜', + source: ['home.meishichina.com/recipe/yingguocai/'], + target: '/recipe/yingguocai', + }, + { + title: '外国美食 - 越南菜', + source: ['home.meishichina.com/recipe/yuenancai/'], + target: '/recipe/yuenancai', + }, + { + title: '外国美食 - 墨西哥菜', + source: ['home.meishichina.com/recipe/moxigecai/'], + target: '/recipe/moxigecai', + }, + { + title: '外国美食 - 外国美食', + source: ['home.meishichina.com/recipe/waiguomeishi/'], + target: '/recipe/waiguomeishi', + }, + { + title: '烘焙 - 蛋糕', + source: ['home.meishichina.com/recipe/dangao/'], + target: '/recipe/dangao', + }, + { + title: '烘焙 - 面包', + source: ['home.meishichina.com/recipe/mianbao/'], + target: '/recipe/mianbao', + }, + { + title: '烘焙 - 饼干', + source: ['home.meishichina.com/recipe/binggan/'], + target: '/recipe/binggan', + }, + { + title: '烘焙 - 派塔', + source: ['home.meishichina.com/recipe/paita/'], + target: '/recipe/paita', + }, + { + title: '烘焙 - 吐司', + source: ['home.meishichina.com/recipe/tusi/'], + target: '/recipe/tusi', + }, + { + title: '烘焙 - 戚风蛋糕', + source: ['home.meishichina.com/recipe/qifeng/'], + target: '/recipe/qifeng', + }, + { + title: '烘焙 - 纸杯蛋糕', + source: ['home.meishichina.com/recipe/zhibei/'], + target: '/recipe/zhibei', + }, + { + title: '烘焙 - 蛋糕卷', + source: ['home.meishichina.com/recipe/dangaojuan/'], + target: '/recipe/dangaojuan', + }, + { + title: '烘焙 - 玛芬蛋糕', + source: ['home.meishichina.com/recipe/mafen/'], + target: '/recipe/mafen', + }, + { + title: '烘焙 - 乳酪蛋糕', + source: ['home.meishichina.com/recipe/rulao/'], + target: '/recipe/rulao', + }, + { + title: '烘焙 - 芝士蛋糕', + source: ['home.meishichina.com/recipe/zhishi/'], + target: '/recipe/zhishi', + }, + { + title: '烘焙 - 奶油蛋糕', + source: ['home.meishichina.com/recipe/naiyou/'], + target: '/recipe/naiyou', + }, + { + title: '烘焙 - 批萨', + source: ['home.meishichina.com/recipe/pisa/'], + target: '/recipe/pisa', + }, + { + title: '烘焙 - 慕斯', + source: ['home.meishichina.com/recipe/musi/'], + target: '/recipe/musi', + }, + { + title: '烘焙 - 曲奇', + source: ['home.meishichina.com/recipe/quqi/'], + target: '/recipe/quqi', + }, + { + title: '烘焙 - 翻糖', + source: ['home.meishichina.com/recipe/fantang/'], + target: '/recipe/fantang', + }, + { + title: '传统美食 - 粽子', + source: ['home.meishichina.com/recipe/zongzi/'], + target: '/recipe/zongzi', + }, + { + title: '传统美食 - 月饼', + source: ['home.meishichina.com/recipe/yuebing/'], + target: '/recipe/yuebing', + }, + { + title: '传统美食 - 春饼', + source: ['home.meishichina.com/recipe/chunbing/'], + target: '/recipe/chunbing', + }, + { + title: '传统美食 - 元宵', + source: ['home.meishichina.com/recipe/yuanxiao/'], + target: '/recipe/yuanxiao', + }, + { + title: '传统美食 - 汤圆', + source: ['home.meishichina.com/recipe/tangyuan/'], + target: '/recipe/tangyuan', + }, + { + title: '传统美食 - 青团', + source: ['home.meishichina.com/recipe/qingtuan/'], + target: '/recipe/qingtuan', + }, + { + title: '传统美食 - 腊八粥', + source: ['home.meishichina.com/recipe/labazhou/'], + target: '/recipe/labazhou', + }, + { + title: '传统美食 - 春卷', + source: ['home.meishichina.com/recipe/chunjuan/'], + target: '/recipe/chunjuan', + }, + { + title: '传统美食 - 传统美食', + source: ['home.meishichina.com/recipe/chuantongmeishi/'], + target: '/recipe/chuantongmeishi', + }, + { + title: '节日食俗 - 立冬', + source: ['home.meishichina.com/recipe/lidong/'], + target: '/recipe/lidong', + }, + { + title: '节日食俗 - 冬至', + source: ['home.meishichina.com/recipe/dongzhi/'], + target: '/recipe/dongzhi', + }, + { + title: '节日食俗 - 腊八', + source: ['home.meishichina.com/recipe/laba/'], + target: '/recipe/laba', + }, + { + title: '节日食俗 - 端午节', + source: ['home.meishichina.com/recipe/duanwu/'], + target: '/recipe/duanwu', + }, + { + title: '节日食俗 - 中秋', + source: ['home.meishichina.com/recipe/zhongqiu/'], + target: '/recipe/zhongqiu', + }, + { + title: '节日食俗 - 立春', + source: ['home.meishichina.com/recipe/lichun/'], + target: '/recipe/lichun', + }, + { + title: '节日食俗 - 元宵节', + source: ['home.meishichina.com/recipe/yuanxiaojie/'], + target: '/recipe/yuanxiaojie', + }, + { + title: '节日食俗 - 贴秋膘', + source: ['home.meishichina.com/recipe/tieqiubiao/'], + target: '/recipe/tieqiubiao', + }, + { + title: '节日食俗 - 清明', + source: ['home.meishichina.com/recipe/qingming/'], + target: '/recipe/qingming', + }, + { + title: '节日食俗 - 年夜饭', + source: ['home.meishichina.com/recipe/nianyefan/'], + target: '/recipe/nianyefan', + }, + { + title: '节日食俗 - 圣诞节', + source: ['home.meishichina.com/recipe/shengdanjie/'], + target: '/recipe/shengdanjie', + }, + { + title: '节日食俗 - 感恩节', + source: ['home.meishichina.com/recipe/ganenjie/'], + target: '/recipe/ganenjie', + }, + { + title: '节日食俗 - 万圣节', + source: ['home.meishichina.com/recipe/wanshengjie/'], + target: '/recipe/wanshengjie', + }, + { + title: '节日食俗 - 情人节', + source: ['home.meishichina.com/recipe/qingrenjie/'], + target: '/recipe/qingrenjie', + }, + { + title: '节日食俗 - 复活节', + source: ['home.meishichina.com/recipe/fuhuojie/'], + target: '/recipe/fuhuojie', + }, + { + title: '节日食俗 - 雨水', + source: ['home.meishichina.com/recipe/yushui/'], + target: '/recipe/yushui', + }, + { + title: '节日食俗 - 惊蛰', + source: ['home.meishichina.com/recipe/jingzhi/'], + target: '/recipe/jingzhi', + }, + { + title: '节日食俗 - 春分', + source: ['home.meishichina.com/recipe/chunfen/'], + target: '/recipe/chunfen', + }, + { + title: '节日食俗 - 谷雨', + source: ['home.meishichina.com/recipe/guyu/'], + target: '/recipe/guyu', + }, + { + title: '节日食俗 - 立夏', + source: ['home.meishichina.com/recipe/lixia/'], + target: '/recipe/lixia', + }, + { + title: '节日食俗 - 小满', + source: ['home.meishichina.com/recipe/xiaoman/'], + target: '/recipe/xiaoman', + }, + { + title: '节日食俗 - 芒种', + source: ['home.meishichina.com/recipe/mangzhong/'], + target: '/recipe/mangzhong', + }, + { + title: '节日食俗 - 夏至', + source: ['home.meishichina.com/recipe/xiazhi/'], + target: '/recipe/xiazhi', + }, + { + title: '节日食俗 - 小暑', + source: ['home.meishichina.com/recipe/xiaoshu/'], + target: '/recipe/xiaoshu', + }, + { + title: '节日食俗 - 大暑', + source: ['home.meishichina.com/recipe/dashu/'], + target: '/recipe/dashu', + }, + { + title: '节日食俗 - 立秋', + source: ['home.meishichina.com/recipe/xiqiu/'], + target: '/recipe/xiqiu', + }, + { + title: '节日食俗 - 处暑', + source: ['home.meishichina.com/recipe/chushu/'], + target: '/recipe/chushu', + }, + { + title: '节日食俗 - 白露', + source: ['home.meishichina.com/recipe/bailu/'], + target: '/recipe/bailu', + }, + { + title: '节日食俗 - 秋分', + source: ['home.meishichina.com/recipe/qiufen/'], + target: '/recipe/qiufen', + }, + { + title: '节日食俗 - 寒露', + source: ['home.meishichina.com/recipe/hanlu/'], + target: '/recipe/hanlu', + }, + { + title: '节日食俗 - 霜降', + source: ['home.meishichina.com/recipe/shuangjiang/'], + target: '/recipe/shuangjiang', + }, + { + title: '节日食俗 - 小雪', + source: ['home.meishichina.com/recipe/xiaoxue/'], + target: '/recipe/xiaoxue', + }, + { + title: '节日食俗 - 大雪', + source: ['home.meishichina.com/recipe/daxue/'], + target: '/recipe/daxue', + }, + { + title: '节日食俗 - 小寒', + source: ['home.meishichina.com/recipe/xiaohan/'], + target: '/recipe/xiaohan', + }, + { + title: '节日食俗 - 大寒', + source: ['home.meishichina.com/recipe/dahan/'], + target: '/recipe/dahan', + }, + { + title: '节日食俗 - 二月二', + source: ['home.meishichina.com/recipe/eryueer/'], + target: '/recipe/eryueer', + }, + { + title: '节日食俗 - 母亲节', + source: ['home.meishichina.com/recipe/muqinjie/'], + target: '/recipe/muqinjie', + }, + { + title: '节日食俗 - 父亲节', + source: ['home.meishichina.com/recipe/fuqinjie/'], + target: '/recipe/fuqinjie', + }, + { + title: '节日食俗 - 儿童节', + source: ['home.meishichina.com/recipe/ertongjie/'], + target: '/recipe/ertongjie', + }, + { + title: '节日食俗 - 七夕', + source: ['home.meishichina.com/recipe/qixi/'], + target: '/recipe/qixi', + }, + { + title: '节日食俗 - 重阳节', + source: ['home.meishichina.com/recipe/chongyangjie/'], + target: '/recipe/chongyangjie', + }, + { + title: '节日食俗 - 节日习俗', + source: ['home.meishichina.com/recipe/jierixisu/'], + target: '/recipe/jierixisu', + }, + { + title: '按制作难度 - 简单', + source: ['home.meishichina.com/recipe-type-do-level-view-1.html'], + target: '/recipe/recipe-type-do-level-view-1', + }, + { + title: '按制作难度 - 普通', + source: ['home.meishichina.com/recipe-type-do-level-view-2.html'], + target: '/recipe/recipe-type-do-level-view-2', + }, + { + title: '按制作难度 - 高级', + source: ['home.meishichina.com/recipe-type-do-level-view-3.html'], + target: '/recipe/recipe-type-do-level-view-3', + }, + { + title: '按制作难度 - 神级', + source: ['home.meishichina.com/recipe-type-do-level-view-4.html'], + target: '/recipe/recipe-type-do-level-view-4', + }, + { + title: '按所需时间 - 十分钟', + source: ['home.meishichina.com/recipe-type-do-during-view-1.html'], + target: '/recipe/recipe-type-do-during-view-1', + }, + { + title: '按所需时间 - 廿分钟', + source: ['home.meishichina.com/recipe-type-do-during-view-2.html'], + target: '/recipe/recipe-type-do-during-view-2', + }, + { + title: '按所需时间 - 半小时', + source: ['home.meishichina.com/recipe-type-do-during-view-3.html'], + target: '/recipe/recipe-type-do-during-view-3', + }, + { + title: '按所需时间 - 三刻钟', + source: ['home.meishichina.com/recipe-type-do-during-view-4.html'], + target: '/recipe/recipe-type-do-during-view-4', + }, + { + title: '按所需时间 - 一小时', + source: ['home.meishichina.com/recipe-type-do-during-view-5.html'], + target: '/recipe/recipe-type-do-during-view-5', + }, + { + title: '按所需时间 - 数小时', + source: ['home.meishichina.com/recipe-type-do-during-view-6.html'], + target: '/recipe/recipe-type-do-during-view-6', + }, + { + title: '按所需时间 - 一天', + source: ['home.meishichina.com/recipe-type-do-during-view-7.html'], + target: '/recipe/recipe-type-do-during-view-7', + }, + { + title: '按所需时间 - 数天', + source: ['home.meishichina.com/recipe-type-do-during-view-8.html'], + target: '/recipe/recipe-type-do-during-view-8', + }, + { + title: '按菜品口味 - 微辣', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-1.html'], + target: '/recipe/recipe-type-do-cuisine-view-1', + }, + { + title: '按菜品口味 - 中辣', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-2.html'], + target: '/recipe/recipe-type-do-cuisine-view-2', + }, + { + title: '按菜品口味 - 超辣', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-3.html'], + target: '/recipe/recipe-type-do-cuisine-view-3', + }, + { + title: '按菜品口味 - 麻辣', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-4.html'], + target: '/recipe/recipe-type-do-cuisine-view-4', + }, + { + title: '按菜品口味 - 酸辣', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-5.html'], + target: '/recipe/recipe-type-do-cuisine-view-5', + }, + { + title: '按菜品口味 - 甜辣', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-29.html'], + target: '/recipe/recipe-type-do-cuisine-view-29', + }, + { + title: '按菜品口味 - 香辣', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-31.html'], + target: '/recipe/recipe-type-do-cuisine-view-31', + }, + { + title: '按菜品口味 - 酸甜', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-6.html'], + target: '/recipe/recipe-type-do-cuisine-view-6', + }, + { + title: '按菜品口味 - 酸咸', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-7.html'], + target: '/recipe/recipe-type-do-cuisine-view-7', + }, + { + title: '按菜品口味 - 咸鲜', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-8.html'], + target: '/recipe/recipe-type-do-cuisine-view-8', + }, + { + title: '按菜品口味 - 咸甜', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-9.html'], + target: '/recipe/recipe-type-do-cuisine-view-9', + }, + { + title: '按菜品口味 - 甜味', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-10.html'], + target: '/recipe/recipe-type-do-cuisine-view-10', + }, + { + title: '按菜品口味 - 苦味', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-11.html'], + target: '/recipe/recipe-type-do-cuisine-view-11', + }, + { + title: '按菜品口味 - 原味', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-12.html'], + target: '/recipe/recipe-type-do-cuisine-view-12', + }, + { + title: '按菜品口味 - 清淡', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-13.html'], + target: '/recipe/recipe-type-do-cuisine-view-13', + }, + { + title: '按菜品口味 - 五香', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-14.html'], + target: '/recipe/recipe-type-do-cuisine-view-14', + }, + { + title: '按菜品口味 - 鱼香', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-15.html'], + target: '/recipe/recipe-type-do-cuisine-view-15', + }, + { + title: '按菜品口味 - 葱香', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-16.html'], + target: '/recipe/recipe-type-do-cuisine-view-16', + }, + { + title: '按菜品口味 - 蒜香', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-17.html'], + target: '/recipe/recipe-type-do-cuisine-view-17', + }, + { + title: '按菜品口味 - 奶香', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-18.html'], + target: '/recipe/recipe-type-do-cuisine-view-18', + }, + { + title: '按菜品口味 - 酱香', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-19.html'], + target: '/recipe/recipe-type-do-cuisine-view-19', + }, + { + title: '按菜品口味 - 糟香', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-20.html'], + target: '/recipe/recipe-type-do-cuisine-view-20', + }, + { + title: '按菜品口味 - 咖喱', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-21.html'], + target: '/recipe/recipe-type-do-cuisine-view-21', + }, + { + title: '按菜品口味 - 孜然', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-22.html'], + target: '/recipe/recipe-type-do-cuisine-view-22', + }, + { + title: '按菜品口味 - 果味', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-23.html'], + target: '/recipe/recipe-type-do-cuisine-view-23', + }, + { + title: '按菜品口味 - 香草', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-24.html'], + target: '/recipe/recipe-type-do-cuisine-view-24', + }, + { + title: '按菜品口味 - 怪味', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-25.html'], + target: '/recipe/recipe-type-do-cuisine-view-25', + }, + { + title: '按菜品口味 - 咸香', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-26.html'], + target: '/recipe/recipe-type-do-cuisine-view-26', + }, + { + title: '按菜品口味 - 甜香', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-27.html'], + target: '/recipe/recipe-type-do-cuisine-view-27', + }, + { + title: '按菜品口味 - 麻香', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-28.html'], + target: '/recipe/recipe-type-do-cuisine-view-28', + }, + { + title: '按菜品口味 - 其他', + source: ['home.meishichina.com/recipe-type-do-cuisine-view-50.html'], + target: '/recipe/recipe-type-do-cuisine-view-50', + }, + { + title: '按主要工艺 - 烧', + source: ['home.meishichina.com/recipe-type-do-technics-view-1.html'], + target: '/recipe/recipe-type-do-technics-view-1', + }, + { + title: '按主要工艺 - 炒', + source: ['home.meishichina.com/recipe-type-do-technics-view-2.html'], + target: '/recipe/recipe-type-do-technics-view-2', + }, + { + title: '按主要工艺 - 爆', + source: ['home.meishichina.com/recipe-type-do-technics-view-3.html'], + target: '/recipe/recipe-type-do-technics-view-3', + }, + { + title: '按主要工艺 - 焖', + source: ['home.meishichina.com/recipe-type-do-technics-view-4.html'], + target: '/recipe/recipe-type-do-technics-view-4', + }, + { + title: '按主要工艺 - 炖', + source: ['home.meishichina.com/recipe-type-do-technics-view-5.html'], + target: '/recipe/recipe-type-do-technics-view-5', + }, + { + title: '按主要工艺 - 蒸', + source: ['home.meishichina.com/recipe-type-do-technics-view-6.html'], + target: '/recipe/recipe-type-do-technics-view-6', + }, + { + title: '按主要工艺 - 煮', + source: ['home.meishichina.com/recipe-type-do-technics-view-7.html'], + target: '/recipe/recipe-type-do-technics-view-7', + }, + { + title: '按主要工艺 - 拌', + source: ['home.meishichina.com/recipe-type-do-technics-view-8.html'], + target: '/recipe/recipe-type-do-technics-view-8', + }, + { + title: '按主要工艺 - 烤', + source: ['home.meishichina.com/recipe-type-do-technics-view-9.html'], + target: '/recipe/recipe-type-do-technics-view-9', + }, + { + title: '按主要工艺 - 炸', + source: ['home.meishichina.com/recipe-type-do-technics-view-10.html'], + target: '/recipe/recipe-type-do-technics-view-10', + }, + { + title: '按主要工艺 - 烩', + source: ['home.meishichina.com/recipe-type-do-technics-view-11.html'], + target: '/recipe/recipe-type-do-technics-view-11', + }, + { + title: '按主要工艺 - 溜', + source: ['home.meishichina.com/recipe-type-do-technics-view-12.html'], + target: '/recipe/recipe-type-do-technics-view-12', + }, + { + title: '按主要工艺 - 氽', + source: ['home.meishichina.com/recipe-type-do-technics-view-13.html'], + target: '/recipe/recipe-type-do-technics-view-13', + }, + { + title: '按主要工艺 - 腌', + source: ['home.meishichina.com/recipe-type-do-technics-view-14.html'], + target: '/recipe/recipe-type-do-technics-view-14', + }, + { + title: '按主要工艺 - 卤', + source: ['home.meishichina.com/recipe-type-do-technics-view-15.html'], + target: '/recipe/recipe-type-do-technics-view-15', + }, + { + title: '按主要工艺 - 炝', + source: ['home.meishichina.com/recipe-type-do-technics-view-16.html'], + target: '/recipe/recipe-type-do-technics-view-16', + }, + { + title: '按主要工艺 - 煎', + source: ['home.meishichina.com/recipe-type-do-technics-view-17.html'], + target: '/recipe/recipe-type-do-technics-view-17', + }, + { + title: '按主要工艺 - 酥', + source: ['home.meishichina.com/recipe-type-do-technics-view-18.html'], + target: '/recipe/recipe-type-do-technics-view-18', + }, + { + title: '按主要工艺 - 扒', + source: ['home.meishichina.com/recipe-type-do-technics-view-19.html'], + target: '/recipe/recipe-type-do-technics-view-19', + }, + { + title: '按主要工艺 - 熏', + source: ['home.meishichina.com/recipe-type-do-technics-view-20.html'], + target: '/recipe/recipe-type-do-technics-view-20', + }, + { + title: '按主要工艺 - 煨', + source: ['home.meishichina.com/recipe-type-do-technics-view-21.html'], + target: '/recipe/recipe-type-do-technics-view-21', + }, + { + title: '按主要工艺 - 酱', + source: ['home.meishichina.com/recipe-type-do-technics-view-22.html'], + target: '/recipe/recipe-type-do-technics-view-22', + }, + { + title: '按主要工艺 - 煲', + source: ['home.meishichina.com/recipe-type-do-technics-view-30.html'], + target: '/recipe/recipe-type-do-technics-view-30', + }, + { + title: '按主要工艺 - 烘焙', + source: ['home.meishichina.com/recipe-type-do-technics-view-23.html'], + target: '/recipe/recipe-type-do-technics-view-23', + }, + { + title: '按主要工艺 - 火锅', + source: ['home.meishichina.com/recipe-type-do-technics-view-24.html'], + target: '/recipe/recipe-type-do-technics-view-24', + }, + { + title: '按主要工艺 - 砂锅', + source: ['home.meishichina.com/recipe-type-do-technics-view-25.html'], + target: '/recipe/recipe-type-do-technics-view-25', + }, + { + title: '按主要工艺 - 拔丝', + source: ['home.meishichina.com/recipe-type-do-technics-view-26.html'], + target: '/recipe/recipe-type-do-technics-view-26', + }, + { + title: '按主要工艺 - 生鲜', + source: ['home.meishichina.com/recipe-type-do-technics-view-27.html'], + target: '/recipe/recipe-type-do-technics-view-27', + }, + { + title: '按主要工艺 - 调味', + source: ['home.meishichina.com/recipe-type-do-technics-view-28.html'], + target: '/recipe/recipe-type-do-technics-view-28', + }, + { + title: '按主要工艺 - 技巧', + source: ['home.meishichina.com/recipe-type-do-technics-view-29.html'], + target: '/recipe/recipe-type-do-technics-view-29', + }, + { + title: '按主要工艺 - 烙', + source: ['home.meishichina.com/recipe-type-do-technics-view-31.html'], + target: '/recipe/recipe-type-do-technics-view-31', + }, + { + title: '按主要工艺 - 榨汁', + source: ['home.meishichina.com/recipe-type-do-technics-view-32.html'], + target: '/recipe/recipe-type-do-technics-view-32', + }, + { + title: '按主要工艺 - 冷冻', + source: ['home.meishichina.com/recipe-type-do-technics-view-33.html'], + target: '/recipe/recipe-type-do-technics-view-33', + }, + { + title: '按主要工艺 - 焗', + source: ['home.meishichina.com/recipe-type-do-technics-view-34.html'], + target: '/recipe/recipe-type-do-technics-view-34', + }, + { + title: '按主要工艺 - 焯', + source: ['home.meishichina.com/recipe-type-do-technics-view-35.html'], + target: '/recipe/recipe-type-do-technics-view-35', + }, + { + title: '按主要工艺 - 干煸', + source: ['home.meishichina.com/recipe-type-do-technics-view-36.html'], + target: '/recipe/recipe-type-do-technics-view-36', + }, + { + title: '按主要工艺 - 干锅', + source: ['home.meishichina.com/recipe-type-do-technics-view-37.html'], + target: '/recipe/recipe-type-do-technics-view-37', + }, + { + title: '按主要工艺 - 铁板', + source: ['home.meishichina.com/recipe-type-do-technics-view-38.html'], + target: '/recipe/recipe-type-do-technics-view-38', + }, + { + title: '按主要工艺 - 微波', + source: ['home.meishichina.com/recipe-type-do-technics-view-39.html'], + target: '/recipe/recipe-type-do-technics-view-39', + }, + { + title: '按主要工艺 - 其他', + source: ['home.meishichina.com/recipe-type-do-technics-view-50.html'], + target: '/recipe/recipe-type-do-technics-view-50', + }, + ], +}; diff --git a/lib/routes/meishichina/namespace.ts b/lib/routes/meishichina/namespace.ts new file mode 100644 index 00000000000000..d3646cc1df04cd --- /dev/null +++ b/lib/routes/meishichina/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '美食天下', + url: 'meishichina.com', + categories: ['new-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/meituan/namespace.ts b/lib/routes/meituan/namespace.ts new file mode 100644 index 00000000000000..af0fd0b1fb53f5 --- /dev/null +++ b/lib/routes/meituan/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '美团', + url: 'meituan.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/meituan/tech.ts b/lib/routes/meituan/tech.ts new file mode 100644 index 00000000000000..a41d0d94215dce --- /dev/null +++ b/lib/routes/meituan/tech.ts @@ -0,0 +1,61 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import parser from '@/utils/rss-parser'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; + +const rootUrl = 'https://tech.meituan.com/'; + +export const route: Route = { + path: '/tech', + categories: ['programming'], + example: '/meituan/tech', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + supportRadar: true, + }, + radar: [ + { + source: ['tech.meituan.com'], + }, + ], + name: '技术团队博客', + url: 'tech.meituan.com', + maintainers: ['ktKongTong', 'cscnk52'], + handler, +}; + +async function handler() { + const rssUrl = `${rootUrl}feed/`; + const feed = await parser.parseURL(rssUrl); + const items = await Promise.all( + feed.items.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + const content = $('div.content').html(); + return { + title: item.title, + link: item.link, + pubDate: item.pubDate, + author: item.creator, + description: content, + }; + }) + ) + ); + + return { + title: feed.title, + link: rootUrl, + description: feed.description, + language: feed.language, + item: items, + }; +} diff --git a/lib/routes/mercari/keyword.ts b/lib/routes/mercari/keyword.ts new file mode 100644 index 00000000000000..f74ed46d3c8779 --- /dev/null +++ b/lib/routes/mercari/keyword.ts @@ -0,0 +1,67 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import { fetchSearchItems, fetchItemDetail, MercariSort, MercariOrder, MercariStatus, formatItemDetail } from './util'; + +export const route: Route = { + path: '/:sort/:order/:status/:keyword', + categories: ['shopping'], + parameters: { + sort: { + description: '排序方式', + default: 'default', + options: [ + { value: 'default', label: '默认排序' }, + { value: 'create_time', label: '发布时间' }, + { value: 'score', label: '评分' }, + { value: 'like', label: '点赞' }, + { value: 'price', label: '价格' }, + ], + }, + order: { + description: '排序顺序', + default: 'desc', + options: [ + { value: 'desc', label: '降序' }, + { value: 'asc', label: '升序' }, + ], + }, + status: { + description: '商品状态', + default: 'default', + options: [ + { value: 'default', label: '全部' }, + { value: 'onsale', label: '在售' }, + { value: 'soldout', label: '已售' }, + ], + }, + keyword: { + description: '关键词', + }, + }, + example: '/mercari/create_time/desc/default/ふもふも', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '关键词', + maintainers: ['yana9i'], + url: 'jp.mercari.com', + handler, +}; + +async function handler(ctx) { + const { sort, order, status, keyword } = ctx.req.param(); + const searchItems = (await fetchSearchItems(MercariSort[sort], MercariOrder[order], MercariStatus[status], keyword)).items; + const items = await Promise.all(searchItems.map((item) => cache.tryGet(`mercari:${item.id}`, async () => await fetchItemDetail(item.id, item.itemType).then((detail) => formatItemDetail(detail))))); + + return { + title: `${keyword} の検索結果`, + link: `https://jp.mercari.com/search?sort=${MercariSort[sort]}&order=${MercariOrder[order]}&status=${MercariStatus[status]}&keyword=${encodeURIComponent(keyword)}`, + description: `Search results for keyword: ${keyword}`, + item: items, + }; +} diff --git a/lib/routes/mercari/namespace.ts b/lib/routes/mercari/namespace.ts new file mode 100644 index 00000000000000..2126b801231033 --- /dev/null +++ b/lib/routes/mercari/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Mercari', + url: 'jp.mercari.com', + zh: { name: '煤炉' }, + ja: { name: 'メルカリ' }, + 'zh-TW': { name: '美露可利' }, +}; diff --git a/lib/routes/mercari/templates/item.art b/lib/routes/mercari/templates/item.art new file mode 100644 index 00000000000000..37f1025719007e --- /dev/null +++ b/lib/routes/mercari/templates/item.art @@ -0,0 +1,46 @@ +<p> ¥{{ data.price}} </p> + +<p> +{{each data.photos}} + <img src={{$value}} style="width:100%" /> +{{/each}} +</p> + +<h2> 商品の説明 </h2> +<div> <%- data.description.replaceAll(`\n`,'<br/>') %> </div> +<h2> 商品の情報 </h2> +<table> + <tr> + <td>カテゴリー</td> + <td>{{ data.item_category.root_category_name }} > {{ data.item_category.parent_category_name }} > {{data.item_category.name}}</td> + </tr> + <tr> + <td>商品の状態</td> + <td> {{data.item_condition.name}} </td> + </tr> + <tr> + <td>配送料の負担</td> + <td> {{data.shipping_payer.name}} </td> + </tr> + <tr> + <td>配送の方法</td> + <td> {{data.shipping_method.name}} </td> + </tr> + <tr> + <td>発送元の地域</td> + <td> {{data.shipping_from_area.name}} </td> + </tr> + <tr> + <td>発送までの日数</td> + <td> {{data.shipping_duration.name}} </td> + </tr> +</table> + +<h2> 出品者 </h2> +<div style="display:flex"> + <img src={{data.seller.photo_url}} style=" + width: 4em; + height: 4em; + border-radius: 50%;" /> + <p> {{data.seller.name}}</p> +</div> \ No newline at end of file diff --git a/lib/routes/mercari/templates/shopItem.art b/lib/routes/mercari/templates/shopItem.art new file mode 100644 index 00000000000000..806e4755d1d36c --- /dev/null +++ b/lib/routes/mercari/templates/shopItem.art @@ -0,0 +1,46 @@ +<p> ¥{{ price }} </p> + +<p> +{{each productDetail.photos}} + <img src={{$value}} style="width:100%" /> +{{/each}} +</p> + +<h2> 商品の説明 </h2> +<div> <%- productDetail.description.replaceAll(`\n`,'<br/>') %> </div> +<h2> 商品の情報 </h2> +<table> + <tr> + <td>カテゴリー</td> + <td> {{productDetail.categories.reverse().map(item => item.displayName).join(" > ")}} </td> + </tr> + <tr> + <td>商品の状態</td> + <td> {{productDetail.condition.displayName}} </td> + </tr> + <tr> + <td>配送料の負担</td> + <td> {{productDetail.shippingPayer.displayName}} </td> + </tr> + <tr> + <td>配送の方法</td> + <td> {{productDetail.shippingMethod.displayName}} </td> + </tr> + <tr> + <td>発送元の地域</td> + <td> {{productDetail.shippingFromArea.displayName}} </td> + </tr> + <tr> + <td>発送までの日数</td> + <td> {{productDetail.shippingDuration.displayName}} </td> + </tr> +</table> + +<h2> 出品者 </h2> +<div style="display:flex"> + <img src={{productDetail.shop.thumbnail}} style=" + width: 4em; + height: 4em; + border-radius: 50%;" /> + <p> {{productDetail.shop.displayName}}</p> +</div> \ No newline at end of file diff --git a/lib/routes/mercari/types.ts b/lib/routes/mercari/types.ts new file mode 100644 index 00000000000000..f2c11e5ef58e75 --- /dev/null +++ b/lib/routes/mercari/types.ts @@ -0,0 +1,278 @@ +export interface SearchResponse { + meta: { + nextPageToken: string; + previousPageToken: string; + numFound: string; + }; + items: [ + { + id: string; + sellerId: string; + buyerId: string; + status: string; + name: string; + price: string; + created: string; + updated: string; + thumbnails: string[]; + itemType: string; + itemConditionId: string; + shippingPayerId: string; + itemSizes: any[]; + itemBrand: any; + itemPromotions: any[]; + shopName: string; + itemSize: any; + shippingMethodId: string; + categoryId: string; + isNoPrice: boolean; + title: string; + isLiked: boolean; + photos: [ + { + uri: string; + }, + ]; + auction: any; + }, + ]; + components: any[]; + searchCondition: any; + searchConditionId: string; +} + +export interface ItemDetail { + result: string; + data: { + id: string; + seller: { + id: number; + name: string; + photo_url: string; + photo_thumbnail_url: string; + register_sms_confirmation: string; + register_sms_confirmation_at: string; + created: number; + num_sell_items: number; + ratings: { + good: number; + normal: number; + bad: number; + }; + num_ratings: number; + score: number; + is_official: boolean; + quick_shipper: boolean; + is_followable: boolean; + is_blocked: boolean; + star_rating_score: number; + }; + status: string; + name: string; + price: number; + description: string; + photos: string[]; + photo_paths: string[]; + thumbnails: string[]; + item_category: { + id: number; + name: string; + display_order: number; + parent_category_id: number; + parent_category_name: string; + root_category_id: number; + root_category_name: string; + }; + item_category_ntiers: { + id: number; + name: string; + display_order: number; + parent_category_id: number; + parent_category_name: string; + root_category_id: number; + root_category_name: string; + brand_group_id: number; + }; + parent_categories_ntiers: { + id: number; + name: string; + display_order: number; + }[]; + item_condition: { + id: number; + name: string; + subname: string; + }; + colors: any[]; + shipping_payer: { + id: number; + name: string; + code: string; + }; + shipping_method: { + id: number; + name: string; + is_deprecated: string; + }; + shipping_from_area: { + id: number; + name: string; + }; + shipping_duration: { + id: number; + name: string; + min_days: number; + max_days: number; + }; + shipping_class: { + id: number; + fee: number; + icon_id: number; + pickup_fee: number; + shipping_fee: number; + total_fee: number; + is_pickup: boolean; + }; + num_likes: number; + num_comments: number; + registered_prices_count: number; + comments: any[]; + updated: number; + created: number; + pager_id: number; + liked: boolean; + checksum: string; + is_dynamic_shipping_fee: boolean; + application_attributes: any; + is_shop_item: string; + hash_tags: any[]; + is_anonymous_shipping: boolean; + is_web_visible: boolean; + is_offerable: boolean; + is_organizational_user: boolean; + organizational_user_status: string; + is_stock_item: boolean; + is_cancelable: boolean; + shipped_by_worker: boolean; + additional_services: any[]; + has_additional_service: boolean; + delivery_facility_type: string; + has_like_list: boolean; + is_offerable_v2: boolean; + offer_coupon_display: { + display_text: string; + display_price: number; + display_discount_label: string; + include_coupon: boolean; + expire_time: number; + current_time: number; + repeated: boolean; + breakdown: { + coupon_discount: number; + offer_discount: number; + total: number; + }; + omakase: boolean; + }; + item_attributes: { + id: string; + text: string; + values: { + id: string; + text: string; + }[]; + deep_facet_filterable: boolean; + show_on_ui: boolean; + }[]; + is_dismissed: boolean; + photo_descriptions: string[]; + meta_title: string; + meta_subtitle: string; + price_promotion_area_details: { + promotion_type: string; + promotion_info: { + label_text: string; + supplementary_text: string; + expire_time: number; + promotion_duration: number; + }[]; + }; + }; + meta: object; +} + +export interface ShopItemDetail { + name: string; + displayName: string; + productTags: string[]; + thumbnail: string; + price: string; + createTime: string; + updateTime: string; + attributes: any[]; + productDetail: { + shop: { + name: string; + displayName: string; + thumbnail: string; + shopStats: { + shopId: string; + score: number; + reviewCount: string; + }; + allowDirectMessage: boolean; + shopItems: { + productId: string; + displayName: string; + productTags: string[]; + thumbnail: string; + price: string; + }[]; + isInboundXb: boolean; + }; + photos: string[]; + description: string; + categories: { + categoryId: string; + displayName: string; + parentId: string; + rootId: string; + hasChild: boolean; + }[]; + brand: null; + condition: { + displayName: string; + }; + shippingMethod: { + shippingMethodId: string; + displayName: string; + isAnonymous: boolean; + }; + shippingPayer: { + shippingPayerId: string; + displayName: string; + code: string; + }; + shippingDuration: { + shippingDurationId: string; + displayName: string; + minDays: number; + maxDays: number; + }; + shippingFromArea: { + shippingAreaCode: string; + displayName: string; + }; + promotions: any[]; + productStats: null; + timeSaleDetails: null; + variants: { + variantId: string; + displayName: string; + quantity: string; + size: string; + }[]; + shippingFeeConfig: null; + variationGrouping: null; + }; +} diff --git a/lib/routes/mercari/util.ts b/lib/routes/mercari/util.ts new file mode 100644 index 00000000000000..b42e722daccda8 --- /dev/null +++ b/lib/routes/mercari/util.ts @@ -0,0 +1,308 @@ +import crypto from 'node:crypto'; +import { Buffer } from 'node:buffer'; +import ofetch from '@/utils/ofetch'; +import { v4 as uuidv4 } from 'uuid'; +import { SearchResponse, ItemDetail, ShopItemDetail } from './types'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { DataItem } from '@/types'; + +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); +const rootURL = 'https://api.mercari.jp/'; +const rootProductURL = 'https://jp.mercari.com/item/'; +const rootShopProductURL = 'https://jp.mercari.com/shops/product/'; +const searchURL = `${rootURL}v2/entities:search`; +const itemInfoURL = `${rootURL}items/get`; +const shopItemInfoURL = `${rootURL}v1/marketplaces/shops/products/`; + +const MercariStatus = { + default: '', + onsale: 'STATUS_ON_SALE', + soldout: 'STATUS_SOLD_OUT', +} as const; + +const MercariSort = { + default: 'SORT_DEFAULT', + create_time: 'SORT_CREATED_TIME', + like: 'SORT_NUM_LIKES', + score: 'SORT_SCORE', + price: 'SORT_PRICE', +} as const; + +const MercariOrder = { + desc: 'ORDER_DESC', + asc: 'ORDER_ASC', +} as const; + +function bytesToBase64URL(b: Buffer): string { + return b.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); +} + +function strToBase64URL(s: string): string { + return bytesToBase64URL(Buffer.from(s, 'utf-8')); +} + +function publicKeyToJWK(publicKey: crypto.KeyObject): any { + const jwk = publicKey.export({ format: 'jwk' }) as { x: string; y: string }; + return { + crv: 'P-256', + kty: 'EC', + x: jwk.x, + y: jwk.y, + }; +} + +function publicKeyToHeader(publicKey: crypto.KeyObject): any { + return { + typ: 'dpop+jwt', + alg: 'ES256', + jwk: publicKeyToJWK(publicKey), + }; +} + +function derDecode(der: Buffer): { r: Buffer; s: Buffer } { + let offset = 0; + + if (der[offset++] !== 0x30) { + throw new Error('Invalid DER signature'); + } + const lenInfo = readDerLength(der, offset); + offset += lenInfo.bytesRead; + + if (der[offset++] !== 0x02) { + throw new Error('Expected INTEGER for R'); + } + const rLen = readDerLength(der, offset); + offset += rLen.bytesRead; + const r = der.subarray(offset, offset + rLen.length); + offset += rLen.length; + + if (der[offset++] !== 0x02) { + throw new Error('Expected INTEGER for S'); + } + const sLen = readDerLength(der, offset); + offset += sLen.bytesRead; + const s = der.subarray(offset, offset + sLen.length); + offset += sLen.length; + + if (offset !== der.length) { + throw new Error('Extra bytes in DER signature'); + } + + return { + r: fixBufferLength(r, 32), + s: fixBufferLength(s, 32), + }; +} + +function readDerLength(buf: Buffer, offset: number): { length: number; bytesRead: number } { + const byte = buf[offset]; + if (byte < 0x80) { + return { length: byte, bytesRead: 1 }; + } + const bytesCount = byte & 0x7F; + if (bytesCount > 4) { + throw new Error('DER length too long'); + } + + let length = 0; + for (let i = 0; i < bytesCount; i++) { + length = (length << 8) | buf[offset + 1 + i]; + } + return { length, bytesRead: 1 + bytesCount }; +} + +function fixBufferLength(buffer: Buffer, length: number): Buffer { + if (buffer.length > length) { + const start = buffer.length - length; + return buffer.subarray(start); + } + if (buffer.length < length) { + return Buffer.concat([Uint8Array.from(Buffer.alloc(length - buffer.length)), Uint8Array.from(buffer)]); + } + return buffer; +} + +function generateDPOP({ uuid, method, url }: { uuid: string; method: string; url: string }): string { + // Generate ECDSA key pair + const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'prime256v1', + }); + + // Create JWT payload + const payload = { + iat: Math.floor(Date.now() / 1000), + jti: uuid, + htu: url, + htm: method.toUpperCase(), + }; + + // Create JWT header + const header = publicKeyToHeader(publicKey); + + // Prepare signing input + const headerB64 = strToBase64URL(JSON.stringify(header)); + const payloadB64 = strToBase64URL(JSON.stringify(payload)); + const signingInput = `${headerB64}.${payloadB64}`; + + // Sign the input + const sign = crypto.createSign('SHA256'); + sign.update(signingInput); + const derSignature = sign.sign(privateKey); + + // Process signature + const { r, s } = derDecode(derSignature); + const signature = bytesToBase64URL(Buffer.concat([Uint8Array.from(r), Uint8Array.from(s)])); + + return `${signingInput}.${signature}`; +} + +const fetchFromMercari = async <T>(url: string, data: any, method: 'POST' | 'GET' = 'POST'): Promise<T> => { + const DPOP = generateDPOP({ + uuid: uuidv4(), + method, + url, + }); + + const headers = new Headers({ + DPOP, + 'X-Platform': 'web', + 'Accept-Encoding': 'gzip, deflate', + 'Content-Type': 'application/json; charset=utf-8', + }); + + const options = { + method, + headers, + body: method === 'POST' ? JSON.stringify(data) : undefined, + query: method === 'GET' ? data : undefined, + }; + + try { + return await ofetch<T>(url, options); + } catch (error) { + throw new Error(`API request failed: ${error}`); + } +}; + +const pageToPageToken = (page: number): string => { + if (page === 0) { + return ''; + } + return `v1:${page}`; +}; + +const fetchSearchItems = async (sort: string, order: string, status: string, keyword: string): Promise<SearchResponse> => { + const data = { + userId: `MERCARI_BOT_${uuidv4()}`, + pageSize: 120, + pageToken: pageToPageToken(0), + searchSessionId: uuidv4(), + indexRouting: 'INDEX_ROUTING_UNSPECIFIED', + thumbnailTypes: [], + searchCondition: { + keyword, + excludeKeyword: '', + sort, + order, + status: [], + sizeId: [], + categoryId: [], + brandId: [], + sellerId: [], + priceMin: 0, + priceMax: 0, + itemConditionId: [], + shippingPayerId: [], + shippingFromArea: [], + shippingMethod: [], + colorId: [], + hasCoupon: false, + attributes: [], + itemTypes: [], + skuIds: [], + shopIds: [], + excludeShippingMethodIds: [], + }, + serviceFrom: 'suruga', + withItemBrand: true, + withItemSize: false, + withItemPromotions: true, + withItemSizes: true, + withShopname: false, + useDynamicAttribute: true, + withSuggestedItems: true, + withOfferPricePromotion: true, + withProductSuggest: true, + withParentProducts: false, + withProductArticles: true, + withSearchConditionId: true, + withAuction: true, + }; + + return await fetchFromMercari<SearchResponse>(searchURL, data, 'POST'); +}; + +const fetchItemDetail = (item_id: string, item_type: string, country_code?: string): Promise<ItemDetail | ShopItemDetail> => { + if (item_type === 'ITEM_TYPE_BEYOND') { + return fetchFromMercari<ShopItemDetail>( + shopItemInfoURL + item_id, + { + view: 'FULL', + imageType: 'JPEG', + }, + 'GET' + ); + } + + return fetchFromMercari<ItemDetail>( + itemInfoURL, + { + id: item_id, + country_code, + include_item_attributes: true, + include_product_page_component: true, + include_non_ui_item_attributes: true, + include_donation: true, + include_offer_like_coupon_display: true, + include_offer_coupon_display: true, + include_item_attributes_sections: true, + include_auction: false, + }, + 'GET' + ); +}; + +const formatItemDetail = (detail: ItemDetail | ShopItemDetail): DataItem => { + if ((detail as ShopItemDetail).displayName) { + const shopItemDetail = detail as ShopItemDetail; + return { + title: shopItemDetail.displayName, + description: art(path.join(__dirname, 'templates/shopItem.art'), shopItemDetail), + pubDate: parseDate(shopItemDetail.createTime), + guid: shopItemDetail.name, + link: `${rootShopProductURL}${shopItemDetail.name}`, + image: shopItemDetail.thumbnail, + language: 'ja', + author: shopItemDetail.productDetail.shop.displayName, + updated: parseDate(shopItemDetail.updateTime), + }; + } + + const itemDetail = detail as ItemDetail; + return { + title: itemDetail.data.name, + description: art(path.join(__dirname, 'templates/item.art'), itemDetail), + pubDate: parseDate(itemDetail.data.created * 1000), + guid: itemDetail.data.id, + link: `${rootProductURL}${itemDetail.data.id}`, + image: itemDetail.data.thumbnails[0], + language: 'ja', + author: itemDetail.data.seller.name, + updated: parseDate(itemDetail.data.updated * 1000), + }; +}; + +export { fetchSearchItems, fetchItemDetail, formatItemDetail, MercariSort, MercariOrder, MercariStatus }; diff --git a/lib/routes/metacritic/index.ts b/lib/routes/metacritic/index.ts index bc20e9c9da2ec2..ba17895ac69869 100644 --- a/lib/routes/metacritic/index.ts +++ b/lib/routes/metacritic/index.ts @@ -22,7 +22,7 @@ async function handler(ctx) { const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 50; const rootUrl = 'https://www.metacritic.com'; - const rootApiUrl = 'https://internal-prod.apigee.fandom.net'; + const rootApiUrl = 'https://backend.metacritic.com'; const apiUrl = new URL('v1/xapi/finder/metacritic/web', rootApiUrl).href; const currentUrlObject = new URL(`/browse/${type}/all/all/all-time/${sort}/${filter ? `?${filter}` : ''}`, rootUrl); @@ -42,6 +42,8 @@ async function handler(ctx) { const genres = currentUrlParams.getAll('genre').join(',').toLowerCase(); const releaseTypes = currentUrlParams.getAll('releaseType').join(','); + const releaseYearMin = currentUrlParams.get('releaseYearMin'); + const releaseYearMax = currentUrlParams.get('releaseYearMax'); if (genres) { searchParams.genres = genres; @@ -51,12 +53,20 @@ async function handler(ctx) { searchParams.releaseType = releaseTypes; } + if (releaseYearMin) { + searchParams.releaseYearMin = releaseYearMin; + } + + if (releaseYearMax) { + searchParams.releaseYearMax = releaseYearMax; + } + const platforms = currentUrlParams.getAll('platform'); const networks = currentUrlParams.getAll('network'); if (platforms.length || networks.length) { const labels = {}; - const labelPattern = '{label:"([^"]+)",value:(\\d+),href:a,meta:{mcDisplayWeight'; + const labelPattern = String.raw`{label:"([^"]+)",value:(\d+),href:a,meta:{mcDisplayWeight`; for (const m of currentResponse.match(new RegExp(labelPattern, 'g'))) { const matches = m.match(new RegExp(labelPattern)); diff --git a/lib/routes/metacritic/namespace.ts b/lib/routes/metacritic/namespace.ts index 839c7172ed3739..8a5a65c2e8e536 100644 --- a/lib/routes/metacritic/namespace.ts +++ b/lib/routes/metacritic/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Metacritic', url: 'metacritic.com', + lang: 'en', }; diff --git a/lib/routes/meteor/index.ts b/lib/routes/meteor/index.ts index 5d83fdd402ce31..3c0cc20288c1d9 100644 --- a/lib/routes/meteor/index.ts +++ b/lib/routes/meteor/index.ts @@ -58,7 +58,7 @@ async function handler(ctx) { return { title: `${board === 'all' ? '全部看板' : boardInfo.title} | Meteor 學生社群`, description: board === 'all' ? null : boardInfo.feedDescription, - image: board === 'all' ? null : boardInfo.imgUrl === 'not_set' ? null : boardInfo.imgUrl, + image: board === 'all' ? null : (boardInfo.imgUrl === 'not_set' ? null : boardInfo.imgUrl), link: `${board === 'all' ? `${baseUrl}/board/all` : boardInfo.link}/new`, item: items, }; diff --git a/lib/routes/meteor/namespace.ts b/lib/routes/meteor/namespace.ts index b064b1926d524b..5f6139a7dade5f 100644 --- a/lib/routes/meteor/namespace.ts +++ b/lib/routes/meteor/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Meteor', url: 'meteor.today', + lang: 'en', }; diff --git a/lib/routes/metmuseum/namespace.ts b/lib/routes/metmuseum/namespace.ts index e2f6675de586e0..d1640165e60de8 100644 --- a/lib/routes/metmuseum/namespace.ts +++ b/lib/routes/metmuseum/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Unknown', + name: 'The Metropolitan Museum of Art', url: 'www.metmuseum.org', + lang: 'en', }; diff --git a/lib/routes/metrics.ts b/lib/routes/metrics.ts new file mode 100644 index 00000000000000..c762497485d180 --- /dev/null +++ b/lib/routes/metrics.ts @@ -0,0 +1,12 @@ +import type { Handler } from 'hono'; +import { getContext } from '@/utils/otel'; + +const handler: Handler = (ctx) => + getContext() + .then((val) => ctx.text(val)) + .catch((error) => { + ctx.status(500); + ctx.json({ error }); + }); + +export default handler; diff --git a/lib/routes/mi/crowdfunding.ts b/lib/routes/mi/crowdfunding.ts index fcd27a08c566f4..eeb805a6b7f12b 100644 --- a/lib/routes/mi/crowdfunding.ts +++ b/lib/routes/mi/crowdfunding.ts @@ -1,37 +1,58 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; +import { Data, DataItem, Route, ViewType } from '@/types'; +import { CrowdfundingDetailInfo, CrowdfundingList } from './types'; +import utils from './utils'; export const route: Route = { path: '/crowdfunding', categories: ['shopping'], example: '/mi/crowdfunding', name: '小米众筹', - maintainers: ['DIYgod'], + maintainers: ['DIYgod', 'nuomi1'], handler, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['m.mi.com/crowdfunding/home'], + target: '/crowdfunding', + }, + ], + view: ViewType.Notifications, +}; + +const getDetails = async (list: CrowdfundingList[]) => { + const result: Promise<CrowdfundingDetailInfo>[] = list.flatMap((section) => section.items.map((item) => utils.getCrowdfundingItem(item))); + return await Promise.all(result); }; +const getDataItem = (item: CrowdfundingDetailInfo) => + ({ + title: item.project_name, + description: utils.renderCrowdfunding(item), + link: `https://m.mi.com/crowdfunding/proddetail/${item.project_id}`, + image: item.big_image, + language: 'zh-cn', + }) as DataItem; + async function handler() { - const response = await got({ - method: 'post', - url: 'http://api.m.mi.com/v1/microwd/home', - headers: { - 'Mishop-Client-Id': '180100031055', - 'User-Agent': 'MiShop/4.3.68 (iPhone; iOS 12.0.1; Scale/3.00)', - 'IOS-App-Version': '4.3.68', - 'IOS-Version': 'system=12.0.1&device=iPhone10,3', - }, - }); - const list = response.data.data.list.flatMap((a) => a.items || []); + const list = await utils.getCrowdfundingList(); + const details = await getDetails(list); + + const items: DataItem[] = details.map((item) => getDataItem(item)); return { title: '小米众筹', - link: '', + link: 'https://m.mi.com/crowdfunding/home', + item: items, allowEmpty: true, - item: - list && - list.map((item) => ({ - title: item.product_name, - description: `<img src="${item.img_url}"><br>价格:${item.product_price}元`, - })), - }; + image: 'https://m.mi.com/static/img/icons/apple-touch-icon-152x152.png', + language: 'zh-cn', + } as Data; } diff --git a/lib/routes/mi/namespace.ts b/lib/routes/mi/namespace.ts index 237d9344045837..9e55f522b83044 100644 --- a/lib/routes/mi/namespace.ts +++ b/lib/routes/mi/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '小米', url: 'mi.com', + lang: 'zh-CN', }; diff --git a/lib/routes/mi/templates/crowdfunding.art b/lib/routes/mi/templates/crowdfunding.art new file mode 100644 index 00000000000000..a58e19aca9a131 --- /dev/null +++ b/lib/routes/mi/templates/crowdfunding.art @@ -0,0 +1,28 @@ +<img src="{{ big_image }}"> +<br> +{{ project_name }} +<br> +{{ project_desc }} +<br> +众筹价:{{ price }} 元,建议零售价:{{ product_market_price }} 元 +<br> +众筹开始:{{ start_time_desc }},众筹结束:{{ end_time_desc }} +<br> +物流:{{ send_info }} +<br> +<table> + <tbody> + <tr> + <th>档位</th> + <th>价格</th> + <th>描述</th> + </tr> + {{ each support_list }} + <tr> + <td>{{ $value.name }}</td> + <td>{{ $value.price }} 元</td> + <td>{{ $value.support_desc }}</td> + </tr> + {{ /each }} + </tbody> +</table> diff --git a/lib/routes/mi/types.ts b/lib/routes/mi/types.ts new file mode 100644 index 00000000000000..6b7cf53ac8dac1 --- /dev/null +++ b/lib/routes/mi/types.ts @@ -0,0 +1,40 @@ +export interface DataResponse<Data> { + data: Data; +} + +export interface CrowdfundingData { + list: CrowdfundingList[]; +} + +export interface CrowdfundingList { + items: CrowdfundingItem[]; +} + +export interface CrowdfundingItem { + project_id: number; + product_market_price: string; +} + +export interface CrowdfundingDetailData { + crowd_funding_info: CrowdfundingDetailInfo; +} + +export interface CrowdfundingDetailInfo { + big_image: string; + end_time: number; + end_time_desc: string; // injected + price: string; + product_market_price: string; // injected + project_desc: string; + project_id: number; + project_name: string; + start_time: number; + start_time_desc: string; // injected + support_list: CrowdfundingDetailSupportList[]; +} + +export interface CrowdfundingDetailSupportList { + name: string; + price: string; + support_desc: string; +} diff --git a/lib/routes/mi/utils.ts b/lib/routes/mi/utils.ts new file mode 100644 index 00000000000000..2fc2c90d275381 --- /dev/null +++ b/lib/routes/mi/utils.ts @@ -0,0 +1,80 @@ +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { art } from '@/utils/render'; +import dayjs from 'dayjs'; +import 'dayjs/locale/zh-cn'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; +import timezone from 'dayjs/plugin/timezone'; +import utc from 'dayjs/plugin/utc'; +import path from 'node:path'; +import { CrowdfundingData, CrowdfundingDetailData, CrowdfundingDetailInfo, CrowdfundingItem, CrowdfundingList, DataResponse } from './types'; + +dayjs.extend(localizedFormat); +dayjs.extend(timezone); +dayjs.extend(utc); + +/** + * 获取众筹项目列表 + * + * @returns {Promise<CrowdfundingList[]>} 众筹项目列表。 + */ +export const getCrowdfundingList = async (): Promise<CrowdfundingList[]> => { + const response = await ofetch<DataResponse<CrowdfundingData>>('https://m.mi.com/v1/crowd/crowd_home', { + headers: { + referrer: 'https://m.mi.com/', + }, + method: 'POST', + }); + return response.data.list; +}; + +/** + * 获取众筹项目详情并缓存 + * + * @param {CrowdfundingItem} item - 众筹项目。 + * @returns {Promise<CrowdfundingDetailInfo>} 众筹项目详情。 + */ +export const getCrowdfundingItem = (item: CrowdfundingItem): Promise<CrowdfundingDetailInfo> => + cache.tryGet(`mi:crowdfunding:${item.project_id}`, async () => { + const response = await ofetch<DataResponse<CrowdfundingDetailData>>('https://m.mi.com/v1/crowd/crowd_detail', { + headers: { + referrer: 'https://m.mi.com/crowdfunding/home', + }, + method: 'POST', + query: { + project_id: item.project_id, + }, + }); + // 建议零售价 + if (response.data.crowd_funding_info.product_market_price === undefined) { + response.data.crowd_funding_info.product_market_price = item.product_market_price; + } + // 众筹开始 + if (response.data.crowd_funding_info.start_time_desc === undefined) { + response.data.crowd_funding_info.start_time_desc = formatDate(response.data.crowd_funding_info.start_time); + } + // 众筹结束 + if (response.data.crowd_funding_info.end_time_desc === undefined) { + response.data.crowd_funding_info.end_time_desc = formatDate(response.data.crowd_funding_info.end_time); + } + return response.data.crowd_funding_info; + }) as Promise<CrowdfundingDetailInfo>; + +/** + * 渲染众筹项目模板 + * + * @param {CrowdfundingDetailInfo} item - 众筹项目详情。 + * @returns {string} 渲染后的众筹项目模板字符串。 + */ +export const renderCrowdfunding = (item: CrowdfundingDetailInfo): string => art(path.join(__dirname, 'templates/crowdfunding.art'), item); + +const formatDate = (timestamp: number): string => dayjs.unix(timestamp).tz('Asia/Shanghai').locale('zh-cn').format('lll'); + +export default { + getCrowdfundingList, + getCrowdfundingItem, + renderCrowdfunding, +}; diff --git a/lib/routes/microsoft/mcr.ts b/lib/routes/microsoft/mcr.ts new file mode 100644 index 00000000000000..da124729163fe9 --- /dev/null +++ b/lib/routes/microsoft/mcr.ts @@ -0,0 +1,63 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; + +export const route: Route = { + path: '/mcr/product/*', + categories: ['program-update'], + example: '/microsoft/mcr/product/dotnet/framework/runtime', + parameters: { product: 'repository path in mcr.microsoft.com' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['https://mcr.microsoft.com/en-us/product/:product/tags'], + }, + ], + name: 'Product tags in mcr.microsoft.com', + maintainers: ['margani'], + handler, +}; + +async function handler(ctx) { + const product = ctx.req.path.replace('/microsoft/mcr/product/', ''); + const { data: details } = await got({ + method: 'get', + url: `https://mcr.microsoft.com/api/v1/catalog/${product}/details?reg=mar`, + }); + const { data: tags } = await got({ + method: 'get', + url: `https://mcr.microsoft.com/api/v1/catalog/${product}/tags?reg=mar`, + }); + + return { + title: `${details.name} - Microsoft Artifact Registry`, + description: String(details.shortDescription), + image: `https://mcr.microsoft.com${details.imagePath}`, + link: `https://mcr.microsoft.com/en-us/product/${product}`, + item: tags.map((tag: any) => { + const descriptionItems = [`Digest: \`${tag.digest}\``, `Last modified date: ${new Date(tag.lastModifiedDate).toDateString()}`]; + + if (tag.architecture) { + descriptionItems.push(`Architecture: ${tag.architecture}`); + } + if (tag.operatingSystem) { + descriptionItems.push(`Operating system: ${tag.operatingSystem}`); + } + + return { + title: `${details.name} - ${tag.name}`, + author: details.publisher, + description: descriptionItems.join('<br />'), + pubDate: new Date(tag.lastModifiedDate), + guid: `mcr::${product}::${tag.name}::${tag.digest}`, + link: `https://mcr.microsoft.com/en-us/product/${product}/tags?name=${tag.name}&digest=${tag.digest}`, + }; + }), + }; +} diff --git a/lib/routes/microsoft/namespace.ts b/lib/routes/microsoft/namespace.ts index 74ca7a2610cd21..df3f2cedd9f1e0 100644 --- a/lib/routes/microsoft/namespace.ts +++ b/lib/routes/microsoft/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Microsoft Edge', - url: 'microsoftedge.microsoft.com', + name: 'Microsoft', + url: 'microsoft.com', + lang: 'en', }; diff --git a/lib/routes/mihoyo/bbs/img-ranking.ts b/lib/routes/mihoyo/bbs/img-ranking.ts index a4a43cc71027c0..75bc1abc889723 100644 --- a/lib/routes/mihoyo/bbs/img-ranking.ts +++ b/lib/routes/mihoyo/bbs/img-ranking.ts @@ -62,43 +62,43 @@ export const route: Route = { maintainers: ['CaoMeiYouRen'], handler, description: `| 键 | 含义 | 接受的值 | 默认值 | - | ----------- | ------------------------------------- | -------------------------------------------------------------------- | ------------ | - | forumType | 主榜类型(仅原神、大别野有 cos 主榜) | tongren/cos | tongren | - | cateType | 子榜类型(仅崩坏三、原神有子榜) | 崩坏三:illustration/comic/cos;原神:illustration/comic/qute/manual | illustration | - | rankingType | 排行榜类型(崩坏二没有日榜) | daily/weekly/monthly | daily | - | lastId | 当前页 id(用于分页) | 数字 | 1 | +| ----------- | ------------------------------------- | -------------------------------------------------------------------- | ------------ | +| forumType | 主榜类型(仅原神、大别野有 cos 主榜) | tongren/cos | tongren | +| cateType | 子榜类型(仅崩坏三、原神有子榜) | 崩坏三:illustration/comic/cos;原神:illustration/comic/qute/manual | illustration | +| rankingType | 排行榜类型(崩坏二没有日榜) | daily/weekly/monthly | daily | +| lastId | 当前页 id(用于分页) | 数字 | 1 | - 游戏缩写(目前绝区零还没有同人榜 + 游戏缩写 - | 崩坏三 | 原神 | 崩坏二 | 未定事件簿 | 星穹铁道 | 大别野 | - | ------ | ---- | ------ | ---------- | -------- | ------ | - | bh3 | ys | bh2 | wd | sr | dby | +| 崩坏三 | 原神 | 崩坏二 | 未定事件簿 | 星穹铁道 | 大别野 | 绝区零 | +| ------ | ---- | ------ | ---------- | -------- | ------ | ------ | +| bh3 | ys | bh2 | wd | sr | dby | zzz | 主榜类型 - | 同人榜 | COS 榜 | - | ------- | ------ | - | tongren | cos | +| 同人榜 | COS 榜 | +| ------- | ------ | +| tongren | cos | 子榜类型 崩坏三 子榜 - | 插画 | 漫画 | COS | - | ------------ | ----- | --- | - | illustration | comic | cos | +| 插画 | 漫画 | COS | +| ------------ | ----- | --- | +| illustration | comic | cos | 原神 子榜 - | 插画 | 漫画 | Q 版 | 手工 | - | ------------ | ----- | ---- | ------ | - | illustration | comic | qute | manual | +| 插画 | 漫画 | Q 版 | 手工 | +| ------------ | ----- | ---- | ------ | +| illustration | comic | qute | manual | 排行榜类型 - | 日榜 | 周榜 | 月榜 | - | ----- | ------ | ------- | - | daily | weekly | monthly |`, +| 日榜 | 周榜 | 月榜 | +| ----- | ------ | ------- | +| daily | weekly | monthly |`, }; async function handler(ctx) { diff --git a/lib/routes/mihoyo/bbs/official.ts b/lib/routes/mihoyo/bbs/official.ts index 3e76a2fd2eec97..a3121a2d88b99b 100644 --- a/lib/routes/mihoyo/bbs/official.ts +++ b/lib/routes/mihoyo/bbs/official.ts @@ -5,6 +5,7 @@ import { art } from '@/utils/render'; import path from 'node:path'; import { parseDate } from '@/utils/parse-date'; import { getCurrentPath } from '@/utils/helpers'; +import logger from '@/utils/logger'; const __dirname = getCurrentPath(import.meta.url); // 游戏id @@ -43,14 +44,22 @@ const OFFICIAL_PAGE_MAP = { 8: '58', }; +class MiHoYoOfficialError extends Error { + constructor(message) { + super(message); + this.name = 'MiHoYoOfficialError'; + } +} + const getNewsList = async ({ gids, type, page_size, last_id }) => { const query = new URLSearchParams({ + client_type: '4', gids, type, page_size, last_id, }).toString(); - const url = `https://bbs-api.miyoushe.com/post/wapi/getNewsList?${query}`; + const url = `https://bbs-api-static.miyoushe.com/painter/wapi/getNewsList?${query}`; const response = await got({ method: 'get', url, @@ -59,45 +68,58 @@ const getNewsList = async ({ gids, type, page_size, last_id }) => { return list; }; -const getPostContent = (list) => +const getPostContent = async (row, default_gid = '2') => { + const post = row.post; + const post_id = post.post_id; + const query = new URLSearchParams({ + post_id, + }).toString(); + const url = `https://bbs-api.miyoushe.com/post/wapi/getPostFull?${query}`; + return await cache.tryGet(url, async () => { + const res = await got(url); + const fullRow = res?.data?.data?.post; + if (!fullRow) { + // throw an error to prevent an empty item from being cached and returned + throw new MiHoYoOfficialError(`mihoyo/bbs/official: getPostContent failed: ${url} - ${JSON.stringify(res)}`); + } + // default_gid should be useless since the above error-throwing line, but just in case + const gid = fullRow?.post?.game_id || default_gid; + const author = fullRow?.user?.nickname || ''; + const content = fullRow?.post?.content || ''; + const tags = fullRow?.topics?.map((item) => item.name) || []; + const description = art(path.join(__dirname, '../templates/official.art'), { + hasCover: post.has_cover, + coverList: row.cover_list, + content, + }); + return { + // 文章标题 + title: post.subject, + // 文章链接 + link: `https://www.miyoushe.com/${GAME_SHORT_MAP[gid]}/article/${post_id}`, + // 文章正文 + description, + // 文章发布日期 + pubDate: parseDate(post.created_at * 1000), + // 文章标签 + category: tags, + author, + }; + }); +}; + +const getPostContents = (list, default_gid = '2') => Promise.all( - list.map(async (row) => { - const post = row.post; - const post_id = post.post_id; - const query = new URLSearchParams({ - post_id, - }).toString(); - const url = `https://bbs-api.miyoushe.com/post/wapi/getPostFull?${query}`; - return await cache.tryGet(url, async () => { - const res = await got({ - method: 'get', - url, - }); - const gid = res?.data?.data?.post?.post?.game_id || '2'; - const author = res?.data?.data?.post?.user?.nickname || ''; - const content = res?.data?.data?.post?.post?.content || ''; - const tags = res?.data?.data?.post?.topics?.map((item) => item.name) || []; - const description = art(path.join(__dirname, '../templates/official.art'), { - hasCover: post.has_cover, - coverList: row.cover_list, - content, - }); - return { - // 文章标题 - title: post.subject, - // 文章链接 - link: `https://www.miyoushe.com/${GAME_SHORT_MAP[gid]}/article/${post_id}`, - // 文章正文 - description, - // 文章发布日期 - pubDate: parseDate(post.created_at * 1000), - // 文章标签 - category: tags, - author, - }; - }); - }) - ); + list.map((item) => + getPostContent(item, default_gid).catch((error) => { + if (error instanceof MiHoYoOfficialError) { + logger.error(error.message); + return null; // skip it now and pray that it will be available next time + } + throw error; + }) + ) + ).then((items) => items.filter(Boolean)); export const route: Route = { path: '/bbs/official/:gids/:type?/:page_size?/:last_id?', @@ -117,22 +139,22 @@ export const route: Route = { handler, description: `游戏 id - | 崩坏三 | 原神 | 崩坏二 | 未定事件簿 | 星穹铁道 | 绝区零 | - | ------ | ---- | ------ | ---------- | -------- | ------ | - | 1 | 2 | 3 | 4 | 6 | 8 | +| 崩坏三 | 原神 | 崩坏二 | 未定事件簿 | 星穹铁道 | 绝区零 | +| ------ | ---- | ------ | ---------- | -------- | ------ | +| 1 | 2 | 3 | 4 | 6 | 8 | 公告类型 - | 公告 | 活动 | 资讯 | - | ---- | ---- | ---- | - | 1 | 2 | 3 |`, +| 公告 | 活动 | 资讯 | +| ---- | ---- | ---- | +| 1 | 2 | 3 |`, }; async function handler(ctx) { const { gids, type = '2', page_size = '20', last_id = '' } = ctx.req.param(); const list = await getNewsList({ gids, type, page_size, last_id }); - const items = await getPostContent(list); + const items = await getPostContents(list, gids); const title = `米游社 - ${GITS_MAP[gids] || ''} - ${TYPE_MAP[type] || ''}`; const url = `https://www.miyoushe.com/${GAME_SHORT_MAP[gids]}/home/${OFFICIAL_PAGE_MAP[gids]}?type=${type}`; const data = { diff --git a/lib/routes/mihoyo/bbs/static-data.ts b/lib/routes/mihoyo/bbs/static-data.ts index 1d3c48cc9b97d3..7dba9bd3a50f5f 100644 --- a/lib/routes/mihoyo/bbs/static-data.ts +++ b/lib/routes/mihoyo/bbs/static-data.ts @@ -97,6 +97,13 @@ const DATA_MAP = { zzz: { title: '绝区零', gids: 8, + default_forum: 'tongren', + forums: { + tongren: { + title: '同人', + forum_id: 59, + }, + }, }, dby: { title: '大别野', diff --git a/lib/routes/mihoyo/bbs/timeline.ts b/lib/routes/mihoyo/bbs/timeline.ts index af60ba5f8a70ac..c390636f12b870 100644 --- a/lib/routes/mihoyo/bbs/timeline.ts +++ b/lib/routes/mihoyo/bbs/timeline.ts @@ -31,9 +31,9 @@ export const route: Route = { name: '米游社 - 用户关注动态', maintainers: ['CaoMeiYouRen'], handler, - description: `:::warning + description: `::: warning 用户关注动态需要米游社登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 - :::`, +:::`, }; async function handler(ctx) { @@ -47,7 +47,7 @@ async function handler(ctx) { page_size, }; const link = 'https://www.miyoushe.com/ys/timeline'; - const url = 'https://bbs-api.miyoushe.com/post/wapi/timelines'; + const url = 'https://bbs-api.miyoushe.com/painter/wapi/timeline/list'; const response = await got({ method: 'get', url, diff --git a/lib/routes/mihoyo/namespace.ts b/lib/routes/mihoyo/namespace.ts index ba70ba4174d5ff..85d8c58d43f0eb 100644 --- a/lib/routes/mihoyo/namespace.ts +++ b/lib/routes/mihoyo/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '米哈游', url: 'genshin.hoyoverse.com', + lang: 'zh-CN', }; diff --git a/lib/routes/mihoyo/sr/news.ts b/lib/routes/mihoyo/sr/news.ts index 69a07c4663b579..4cba9a94502523 100644 --- a/lib/routes/mihoyo/sr/news.ts +++ b/lib/routes/mihoyo/sr/news.ts @@ -68,9 +68,9 @@ export const route: Route = { url: 'sr.mihoyo.com/news', description: `#### 新闻 {#mi-ha-you-beng-huai-xing-qiong-tie-dao-xin-wen} - | 最新 | 新闻 | 公告 | 活动 | - | -------- | ---- | ------ | -------- | - | news-all | news | notice | activity |`, +| 最新 | 新闻 | 公告 | 活动 | +| -------- | ---- | ------ | -------- | +| news-all | news | notice | activity |`, }; async function handler(ctx) { diff --git a/lib/routes/mihoyo/ys/news.ts b/lib/routes/mihoyo/ys/news.ts index 4ae1226b4ca6f7..30fb94ccfc8b3e 100644 --- a/lib/routes/mihoyo/ys/news.ts +++ b/lib/routes/mihoyo/ys/news.ts @@ -104,9 +104,9 @@ export const route: Route = { handler, description: `#### 新闻 {#mi-ha-you-yuan-shen-xin-wen} - | 最新 | 新闻 | 公告 | 活动 | - | ------ | ---- | ------ | -------- | - | latest | news | notice | activity |`, +| 最新 | 新闻 | 公告 | 活动 | +| ------ | ---- | ------ | -------- | +| latest | news | notice | activity |`, }; async function handler(ctx) { diff --git a/lib/routes/mindmeister/example.ts b/lib/routes/mindmeister/example.ts index e83bbc69a25a01..62a45c3f280617 100644 --- a/lib/routes/mindmeister/example.ts +++ b/lib/routes/mindmeister/example.ts @@ -25,34 +25,34 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| Categories | parameter | - | ------------- | ----------------- | - | Featured Map | mind-map-examples | - | Business | business | - | Design | design | - | Education | education | - | Entertainment | entertainment | - | Life | life | - | Marketing | marketing | - | Productivity | productivity | - | Summaries | summaries | - | Technology | technology | - | Other | other | +| ------------- | ----------------- | +| Featured Map | mind-map-examples | +| Business | business | +| Design | design | +| Education | education | +| Entertainment | entertainment | +| Life | life | +| Marketing | marketing | +| Productivity | productivity | +| Summaries | summaries | +| Technology | technology | +| Other | other | - | Languages | parameter | - | ---------- | --------- | - | English | en | - | Deutsch | de | - | Français | fr | - | Español | es | - | Português | pt | - | Nederlands | nl | - | Dansk | da | - | Русский | ru | - | 日本語 | ja | - | Italiano | it | - | 简体中文 | zh | - | 한국어 | ko | - | Other | other |`, +| Languages | parameter | +| ---------- | --------- | +| English | en | +| Deutsch | de | +| Français | fr | +| Español | es | +| Português | pt | +| Nederlands | nl | +| Dansk | da | +| Русский | ru | +| 日本語 | ja | +| Italiano | it | +| 简体中文 | zh | +| 한국어 | ko | +| Other | other |`, }; async function handler(ctx) { diff --git a/lib/routes/mindmeister/namespace.ts b/lib/routes/mindmeister/namespace.ts index 079a1a78f2da68..032d1dfa974759 100644 --- a/lib/routes/mindmeister/namespace.ts +++ b/lib/routes/mindmeister/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MindMeister', url: 'mindmeister.com', + lang: 'en', }; diff --git a/lib/routes/minecraft/namespace.ts b/lib/routes/minecraft/namespace.ts index f02a2ec8183c89..b8ff972ba6441f 100644 --- a/lib/routes/minecraft/namespace.ts +++ b/lib/routes/minecraft/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Minecraft', url: 'minecraft.net', + lang: 'en', }; diff --git a/lib/routes/minecraft/version.ts b/lib/routes/minecraft/version.ts index f53a6c38c3b346..a8c7c279eb240e 100644 --- a/lib/routes/minecraft/version.ts +++ b/lib/routes/minecraft/version.ts @@ -120,7 +120,7 @@ async function handler(ctx?: Context) { data = data.filter((item) => item.type === versionType); } - const title = `Minecraft Java版${versionType === 'all' ? '' : typeName[versionType] ?? versionType}游戏更新`; + const title = `Minecraft Java版${versionType === 'all' ? '' : (typeName[versionType] ?? versionType)}游戏更新`; return { title, diff --git a/lib/routes/mingpao/index.ts b/lib/routes/mingpao/index.ts index 23447ef577db12..b8f187884f809d 100644 --- a/lib/routes/mingpao/index.ts +++ b/lib/routes/mingpao/index.ts @@ -3,13 +3,12 @@ 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 parser from '@/utils/rss-parser'; -import { load } from 'cheerio'; +import * as cheerio from 'cheerio'; import { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; import path from 'node:path'; -import logger from '@/utils/logger'; const renderFanBox = (media) => art(path.join(__dirname, 'templates/fancybox.art'), { @@ -22,11 +21,79 @@ const renderDesc = (media, desc) => desc, }); +const fixFancybox = (element, $) => { + const $e = $(element); + const url = new URL($e.attr('href')); + let video; + if (url.hostname === 'videop.mingpao.com') { + video = new URL(url.searchParams.get('file')); + video.hostname = 'cfrvideo.mingpao.com'; // use cloudflare cdn + video = video.href; + } + return { + href: url.href, + title: $e.attr('title'), + video, + }; +}; + export const route: Route = { path: '/:type?/:category?', - name: 'Unknown', + name: '新聞', + example: '/mingpao/ins/all', + parameters: { + type: { + description: '新聞類型', + default: 'ins', + options: [ + { value: 'ins', label: '即時新聞' }, + { value: 'pns', label: '每日明報' }, + ], + }, + category: '頻道,見下表', + }, + radar: [ + { + title: '即時新聞', + source: ['news.mingpao.com/ins/:categoryName/section/:date/:category'], + target: '/mingpao/ins/:category', + }, + { + title: '每日明報', + source: ['news.mingpao.com/pns/:categoryName/section/:date/:category'], + target: '/mingpao/pns/:category', + }, + ], maintainers: ['TonyRL'], handler, + description: `| category | 即時新聞頻道 | +| -------- | ------------ | +| all | 總目錄 | +| s00001 | 港聞 | +| s00002 | 經濟 | +| s00003 | 地產 | +| s00004 | 兩岸 | +| s00005 | 國際 | +| s00006 | 體育 | +| s00007 | 娛樂 | +| s00022 | 文摘 | +| s00024 | 熱點 | + +| category | 每日明報頻道 | +| -------- | ------------ | +| s00001 | 要聞 | +| s00002 | 港聞 | +| s00003 | 社評 | +| s00004 | 經濟 | +| s00005 | 副刊 | +| s00011 | 教育 | +| s00012 | 觀點 | +| s00013 | 中國 | +| s00014 | 國際 | +| s00015 | 體育 | +| s00016 | 娛樂 | +| s00017 | English | +| s00018 | 作家專欄 |`, }; async function handler(ctx) { @@ -35,26 +102,22 @@ async function handler(ctx) { const link = `https://news.mingpao.com/rss/${type}/${category}.xml`; const feed = await parser.parseURL(link); - const items = await Promise.all( feed.items.map((item) => cache.tryGet(item.link, async () => { - let response; - try { - response = await got(item.link, { - headers: { - Referer: 'https://news.mingpao.com/', - }, - }); - } catch (error) { - if (error instanceof got.MaxRedirectsError) { - logger.error(`MaxRedirectsError when requesting ${decodeURIComponent(item.link)}`); - return item; - } - throw error; - } + const response = await ofetch(item.link, { + headers: { + Referer: 'https://news.mingpao.com/', + }, + }); - const $ = load(response.data); + const $ = cheerio.load(response); + const topVideo = $('#topvideo').length + ? $('#topvideo iframe') + .toArray() + .map((e) => $(e).attr('href', $(e).attr('src'))) + .map((e) => fixFancybox(e, $)) + : []; const fancyboxImg = $('a.fancybox').length ? $('a.fancybox') : $('a.fancybox-buttons'); // remove unwanted elements @@ -68,21 +131,25 @@ async function handler(ctx) { item.category = item.categories; // fix fancybox image - const fancybox = fancyboxImg.toArray().map((e) => { - e = $(e); - const href = new URL(e.attr('href')); - let video; - if (href.hostname === 'videop.mingpao.com') { - video = new URL(href.searchParams.get('file')); - video.hostname = 'cfrvideo.mingpao.com'; // use cloudflare cdn - video = video.href; - } - return { - href: href.href, - title: e.attr('title'), - video, - }; - }); + let fancybox = [...topVideo, ...fancyboxImg.toArray().map((e) => fixFancybox(e, $))]; + const script = $('script') + .toArray() + .find((e) => $(e).text()?.includes("$('#lower').prepend('")); + const lowerContent = script + ? $(script) + .text() + ?.match(/\$\('#lower'\)\.prepend\('(.*)'\);/)?.[1] + ?.replaceAll(/\\"/g, '"') + : ''; + if (lowerContent) { + const $ = cheerio.load(lowerContent, null, false); + fancybox = [ + ...fancybox, + ...$('a.fancybox') + .toArray() + .map((e) => fixFancybox(e, $)), + ]; + } // remove unwanted key value delete item.categories; @@ -91,7 +158,7 @@ async function handler(ctx) { delete item.creator; delete item.isoDate; - item.description = renderDesc(fancybox, $('.txt4').html() ?? $('.article_content.line_1_5em').html()); + item.description = renderDesc(fancybox, $('.txt4').html() ?? $('.article_content.line_1_5em').html() ?? $('.txt3').html()); item.pubDate = parseDate(item.pubDate); item.guid = item.link.includes('?') ? item.link : item.link.substring(0, item.link.lastIndexOf('/')); diff --git a/lib/routes/mingpao/namespace.ts b/lib/routes/mingpao/namespace.ts index b50003246a0803..e76449b73ec712 100644 --- a/lib/routes/mingpao/namespace.ts +++ b/lib/routes/mingpao/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Ming Pao 明报', + name: '明報', url: 'mingpao.com', + lang: 'zh-TW', }; diff --git a/lib/routes/miniflux/namespace.ts b/lib/routes/miniflux/namespace.ts index e3ed1d00b18079..6cddc83dc572e0 100644 --- a/lib/routes/miniflux/namespace.ts +++ b/lib/routes/miniflux/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MiniFlux', url: 'miniflux.app', + lang: 'en', }; diff --git a/lib/routes/mirror/index.ts b/lib/routes/mirror/index.ts index 9025647795a577..e477c12d819c68 100644 --- a/lib/routes/mirror/index.ts +++ b/lib/routes/mirror/index.ts @@ -11,7 +11,7 @@ import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { path: '/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/mirror/tingfei.eth', parameters: { id: 'user id' }, features: { diff --git a/lib/routes/mirror/namespace.ts b/lib/routes/mirror/namespace.ts index 017ce9fb876ff0..ac0cdaa0f7ff97 100644 --- a/lib/routes/mirror/namespace.ts +++ b/lib/routes/mirror/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Mirror', url: 'mirror.xyz', + lang: 'en', }; diff --git a/lib/routes/mirrormedia/category.ts b/lib/routes/mirrormedia/category.ts new file mode 100644 index 00000000000000..b9f2e67f8baabf --- /dev/null +++ b/lib/routes/mirrormedia/category.ts @@ -0,0 +1,101 @@ +import { Route } from '@/types'; + +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; +import { getArticle } from './utils'; + +export const route: Route = { + path: ['/category/:category', '/section/:section'], + categories: ['traditional-media'], + example: '/mirrormedia/category/political', + parameters: { category: '分类名', section: '子板名' }, + name: '分类', + maintainers: ['dzx-dzx'], + radar: [ + { + source: ['mirrormedia.mg/category/:category', 'mirrormedia.mg/section/:section'], + }, + ], + handler, +}; + +async function handler(ctx) { + const { category, section } = ctx.req.param(); + const categoryFilter = category ? { categories: { some: { slug: { equals: category } } } } : {}; + const sectionFilter = section ? { sections: { some: { slug: { equals: section } } } } : {}; + const rootUrl = 'https://www.mirrormedia.mg'; + + const response = await ofetch('https://adam-weekly-api-server-prod-ufaummkd5q-de.a.run.app/content/graphql', { + method: 'POST', + body: { + variables: { + take: ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 24, + skip: 0, + orderBy: { publishedDate: 'desc' }, + filter: { + state: { equals: 'published' }, + ...categoryFilter, + ...sectionFilter, + }, + }, + query: ` +fragment section on Section { + id + name + slug + state + __typename +} + +fragment category on Category { + id + name + slug + state + __typename +} + +fragment listingPost on Post { + id + slug + title + brief + publishedDate + state + sections(where: {state: {equals: "active"}}) { + ...section + __typename + } + categories(where: {state: {equals: "active"}}) { + ...category + __typename + } + isFeatured + __typename +} + +query ($take: Int, $skip: Int, $orderBy: [PostOrderByInput!]!, $filter: PostWhereInput!) { + postsCount(where: $filter) + posts(take: $take, skip: $skip, orderBy: $orderBy, where: $filter) { + ...listingPost + __typename + } +}`, + }, + }); + + const items = response.data.posts.map((e) => ({ + title: e.title, + pubDate: parseDate(e.publishedDate), + category: [...(e.sections ?? []).map((_) => `section:${_.name}`), ...(e.categories ?? []).map((_) => `category:${_.name}`)], + link: `${rootUrl}/${'story'}/${e.slug}`, + })); + + const list = await Promise.all(items.map((item) => getArticle(item))); + + return { + title: `鏡週刊 Mirror Media - ${category}`, + link: rootUrl, + item: list, + }; +} diff --git a/lib/routes/mirrormedia/index.ts b/lib/routes/mirrormedia/index.ts new file mode 100644 index 00000000000000..75d14b6d2f9ee8 --- /dev/null +++ b/lib/routes/mirrormedia/index.ts @@ -0,0 +1,43 @@ +import { Route } from '@/types'; + +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; +import { getArticle } from './utils'; + +export const route: Route = { + path: '/', + categories: ['traditional-media'], + example: '/mirrormedia', + parameters: {}, + name: '首页', + maintainers: ['dzx-dzx'], + radar: [ + { + source: ['mirrormedia.mg'], + }, + ], + handler, +}; + +async function handler(ctx) { + const rootUrl = 'https://www.mirrormedia.mg'; + + const response = await ofetch('https://v3-statics.mirrormedia.mg/files/json/post_external01.json'); + + const items = [...response.choices.map((e) => ({ __from: 'choices', ...e })), ...response.latest.map((e) => ({ __from: 'latest', ...e }))] + .map((e) => ({ + title: e.title, + pubDate: parseDate(e.publishedDate), + category: [...(e.sections ?? []).map((_) => _.name), e.__from], + link: `${rootUrl}/${e.style === '' ? 'external' : 'story'}/${e.slug}`, + })) + .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20); + + const list = await Promise.all(items.map((item) => getArticle(item))); + + return { + title: '鏡週刊 Mirror Media', + link: rootUrl, + item: list, + }; +} diff --git a/lib/routes/mirrormedia/namespace.ts b/lib/routes/mirrormedia/namespace.ts new file mode 100644 index 00000000000000..2fe0e84180c84b --- /dev/null +++ b/lib/routes/mirrormedia/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '鏡週刊 Mirror Media', + url: 'mirrormedia.mg', + lang: 'zh-TW', +}; diff --git a/lib/routes/mirrormedia/utils.ts b/lib/routes/mirrormedia/utils.ts new file mode 100644 index 00000000000000..6af24b14ba045e --- /dev/null +++ b/lib/routes/mirrormedia/utils.ts @@ -0,0 +1,19 @@ +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; + +export function getArticle(item) { + return cache.tryGet(item.link, async () => { + const detailResponse = await ofetch(item.link); + + const content = load(detailResponse); + + item.description = item.link.includes('external') + ? content(':is([class^=external-article-brief],[class^=external-article-content])').html() + : content(':is([class^=brief__BriefContainer],[class^=article-content__Wrapper])').html(); + + item.category = [...item.category, ...(content("meta[name='keywords']").attr('content') ?? '').split(',')]; + + return item; + }); +} diff --git a/lib/routes/missav/namespace.ts b/lib/routes/missav/namespace.ts index a0216d37673d14..808c12e0a1fea5 100644 --- a/lib/routes/missav/namespace.ts +++ b/lib/routes/missav/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'MissAV.com', - url: 'missav.com', + name: 'MissAV', + url: 'missav.ws', + lang: 'en', }; diff --git a/lib/routes/missav/new.ts b/lib/routes/missav/new.ts index d9e720deee3b5d..45d07898600aca 100644 --- a/lib/routes/missav/new.ts +++ b/lib/routes/missav/new.ts @@ -2,16 +2,16 @@ import { Route } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); -import got from '@/utils/got'; -import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; import { art } from '@/utils/render'; import path from 'node:path'; +import { config } from '@/config'; export const route: Route = { path: '/new', categories: ['multimedia'], example: '/missav/new', - parameters: {}, features: { requireConfig: false, requirePuppeteer: false, @@ -22,28 +22,35 @@ export const route: Route = { }, radar: [ { - source: ['missav.com/dm397/new', 'missav.com/new', 'missav.com/'], + source: ['missav.ws/dm397/new', 'missav.ws/new', 'missav.ws/'], + }, + { + source: ['missav.ai/dm397/new', 'missav.ai/new', 'missav.ai/'], }, ], name: '最近更新', maintainers: ['TonyRL'], handler, - url: 'missav.com/dm397/new', + url: 'missav.ws/dm397/new', }; async function handler() { - const baseUrl = 'https://missav.com'; - const { data: response } = await got(`${baseUrl}/dm397/new`); - const $ = load(response); + const baseUrl = 'https://missav.ws'; + const response = await ofetch(`${baseUrl}/dm397/new`, { + headers: { + 'User-Agent': config.trueUA, + }, + }); + const $ = cheerio.load(response); const items = $('.grid .group') .toArray() .map((item) => { - item = $(item); - const title = item.find('.text-secondary'); - const poster = new URL(item.find('img').data('src')); + const $item = $(item); + const title = $item.find('.text-secondary'); + const poster = new URL($item.find('img').data('src')); poster.searchParams.set('class', 'normal'); - const video = item.find('video').data('src'); + const video = $item.find('video').data('src'); return { title: title.text().trim(), link: title.attr('href'), diff --git a/lib/routes/misskey/featured-notes.ts b/lib/routes/misskey/featured-notes.ts index 2891809315cb68..ac7751bba261c8 100644 --- a/lib/routes/misskey/featured-notes.ts +++ b/lib/routes/misskey/featured-notes.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import utils from './utils'; import { config } from '@/config'; @@ -6,7 +6,8 @@ import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { path: '/notes/featured/:site', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/misskey/notes/featured/misskey.io', parameters: { site: 'instance address, domain only, without `http://` or `https://` protocol header' }, features: { diff --git a/lib/routes/misskey/home-timeline.ts b/lib/routes/misskey/home-timeline.ts new file mode 100644 index 00000000000000..4e389492bdd7d4 --- /dev/null +++ b/lib/routes/misskey/home-timeline.ts @@ -0,0 +1,99 @@ +import { config } from '@/config'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import { Route, ViewType } from '@/types'; +import got from '@/utils/got'; +import { queryToBoolean } from '@/utils/readable-social'; +import querystring from 'querystring'; +import utils from './utils'; + +export const route: Route = { + path: '/timeline/home/:site/:routeParams?', + categories: ['social-media'], + view: ViewType.SocialMedia, + example: '/misskey/timeline/home/misskey.io', + parameters: { + site: 'instance address, domain only, without `http://` or `https://` protocol header', + routeParams: ` +| Key | Description | Accepted Values | Default | +| -------------------- | --------------------------------------- | --------------- | ------- | +| limit | Number of notes to return | integer | 10 | +| withFiles | Only return notes containing files | 0/1/true/false | false | +| withRenotes | Include renotes in the timeline | 0/1/true/false | true | +| allowPartial | Allow partial results | 0/1/true/false | true | +| simplifyAuthor | Simplify author field in feed items | 0/1/true/false | true | + +Note: If \`withFiles\` is set to true, renotes will not be included in the timeline regardless of the value of \`withRenotes\`. + +Examples: +- /misskey/timeline/home/misskey.io/limit=20&withFiles=true +- /misskey/timeline/home/misskey.io/withRenotes=false + `, + }, + features: { + requireConfig: [ + { + name: 'MISSKEY_ACCESS_TOKEN', + optional: false, + description: ` + Access token for Misskey API. Requires \`read:account\` access. + + Visit the specified site's settings page to obtain an access token. E.g. https://misskey.io/settings/api + `, + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['misskey.io'], + }, + ], + name: 'Home Timeline', + maintainers: ['HanaokaYuzu'], + handler, + description: `::: warning + This route is only available for self-hosted instances. +:::`, +}; + +async function handler(ctx) { + const access_token = config.misskey.accessToken; + if (!access_token) { + throw new ConfigNotFoundError('Missing access token for Misskey API. Please set `MISSKEY_ACCESS_TOKEN` environment variable.'); + } + + const site = ctx.req.param('site'); + if (!config.feature.allow_user_supply_unsafe_domain && !utils.allowSiteList.includes(site)) { + throw new ConfigNotFoundError(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`); + } + + // docs on: https://misskey.io/api-doc#tag/notes/operation/notes___timeline + const url = `https://${site}/api/notes/timeline`; + const routeParams = querystring.parse(ctx.req.param('routeParams')); + const response = await got({ + method: 'post', + url, + headers: { + Authorization: `Bearer ${access_token}`, + }, + json: { + limit: Number(routeParams.limit ?? 10), + withFiles: queryToBoolean(routeParams.withFiles ?? false), + withRenotes: queryToBoolean(routeParams.withRenotes ?? true), + allowPartial: queryToBoolean(routeParams.allowPartial ?? true), + }, + }); + + const list = response.data; + const simplifyAuthor = queryToBoolean(routeParams.simplifyAuthor ?? true); + + return { + title: `Home Timeline on ${site}`, + link: `https://${site}`, + item: utils.parseNotes(list, site, simplifyAuthor), + }; +} diff --git a/lib/routes/misskey/namespace.ts b/lib/routes/misskey/namespace.ts index 2851e1f58dbc43..54adaaf18673b4 100644 --- a/lib/routes/misskey/namespace.ts +++ b/lib/routes/misskey/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Misskey', url: 'misskey.io', + lang: 'en', }; diff --git a/lib/routes/misskey/templates/note.art b/lib/routes/misskey/templates/note.art index c748ed2e1887df..b4922d5acd280b 100644 --- a/lib/routes/misskey/templates/note.art +++ b/lib/routes/misskey/templates/note.art @@ -1,3 +1,9 @@ +{{ if reply }} + <blockquote> + <p>{{ reply.text }}</p> + </blockquote> +{{ /if }} + {{ if text }} <p>{{ text.replace(/\n/g, '<br>') }}</p> {{ /if }} diff --git a/lib/routes/misskey/types.ts b/lib/routes/misskey/types.ts new file mode 100644 index 00000000000000..2aff1cc2f15510 --- /dev/null +++ b/lib/routes/misskey/types.ts @@ -0,0 +1,111 @@ +// https://github.com/misskey-dev/misskey/blob/d2e8dc4fe3c6e90e68001ed1f092d4e3d2454283/packages/backend/src/misc/json-schema.ts + +// https://github.com/misskey-dev/misskey/blob/d2e8dc4fe3c6e90e68001ed1f092d4e3d2454283/packages/backend/src/models/json-schema/note.ts +export interface MisskeyNote { + id: string; + createdAt: string; + userId: string; + user: MisskeyUser; + text: string | null; + cw: string | null; + visibility: 'public' | 'home' | 'followers' | 'specified'; + localOnly: boolean; + reactionAcceptance: string | null; + renoteCount: number; + repliesCount: number; + reactions: Record<string, number>; + reactionEmojis: Record<string, string>; + fileIds: string[]; + files: MisskeyFile[]; + replyId: string | null; + renoteId: string | null; + channelId: string | undefined; + channel?: { + id: string; + name: string; + color: string; + isSensitive: boolean; + allowRenoteToExternal: boolean; + userId: string | null; + }; + mentions?: string[]; + uri?: string; + url?: string; + reactionAndUserPairCache?: string[]; + poll?: { + expiresAt: string | null; + multiple: boolean; + choices: { + text: string; + votes: number; + isVoted: boolean; + }[]; + }; + emojis?: Record<string, string>; + tags?: string[]; + clippedCount?: number; + myReaction?: string | null; + + reply?: MisskeyNote; + renote?: MisskeyNote; +} + +// https://github.com/misskey-dev/misskey/blob/d2e8dc4fe3c6e90e68001ed1f092d4e3d2454283/packages/backend/src/models/json-schema/user.ts +export interface MisskeyUser { + id: string; + name: string | null; + username: string; + host: string | null; + avatarUrl: string | null; + avatarBlurhash: string | null; + avatarDecorations: Array<{ + id: string; + url: string; + angle?: number; + flipH?: boolean; + offsetX?: number; + offsetY?: number; + }>; + isBot?: boolean; + isCat?: boolean; + instance?: { + name: string | null; + softwareName: string | null; + softwareVersion: string | null; + iconUrl: string | null; + faviconUrl: string | null; + themeColor: string | null; + }; + emojis: Record<string, string>; + onlineStatus: 'unknown' | 'online' | 'active' | 'offline'; + badgeRoles?: Array<{ + name: string; + iconUrl: string | null; + displayOrder: number; + }>; +} + +// https://github.com/misskey-dev/misskey/blob/d2e8dc4fe3c6e90e68001ed1f092d4e3d2454283/packages/backend/src/models/json-schema/drive-file.ts +interface MisskeyFile { + id: string; + createdAt: string; + name: string; + type: string; + md5: string; + size: number; + isSensitive: boolean; + blurhash: string | null; + properties: { + width?: number; + height?: number; + orientation?: number; + avgColor?: string; + }; + url: string; + thumbnailUrl: string | null; + comment: string | null; + folderId: string | null; + folder?: unknown | null; + userId: string | null; + user?: MisskeyUser | null; +} diff --git a/lib/routes/misskey/user-timeline.ts b/lib/routes/misskey/user-timeline.ts new file mode 100644 index 00000000000000..bd6dcd71c0eb52 --- /dev/null +++ b/lib/routes/misskey/user-timeline.ts @@ -0,0 +1,73 @@ +import { config } from '@/config'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import { Data, Route, ViewType } from '@/types'; +import { fallback, queryToBoolean } from '@/utils/readable-social'; +import querystring from 'querystring'; +import utils from './utils'; + +export const route: Route = { + path: '/users/notes/:username/:routeParams?', + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, + example: '/misskey/users/notes/support@misskey.io', + parameters: { + username: 'Misskey username in the format of username@instance.domain', + routeParams: ` +| Key | Description | Accepted Values | Default | +| ----------------- | --------------------------------------- | --------------- | ------- | +| withRenotes | Include renotes in the timeline | 0/1/true/false | false | +| mediaOnly | Only return posts containing media | 0/1/true/false | false | +| simplifyAuthor | Simplify author field in feed items | 0/1/true/false | false | + +Note: \`withRenotes\` and \`mediaOnly\` are mutually exclusive and cannot both be set to true. + +Examples: +- /misskey/users/notes/mttb2ccp@misskey.io/withRenotes=true +- /misskey/users/notes/mttb2ccp@misskey.io/mediaOnly=true`, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'User timeline', + maintainers: ['siygle', 'SnowAgar25', 'HanaokaYuzu'], + handler, +}; + +async function handler(ctx): Promise<Data> { + const username = ctx.req.param('username'); + const [, pureUsername, site] = username.match(/@?(\w+)@(\w+\.\w+)/) || []; + if (!pureUsername || !site) { + throw new InvalidParameterError('Provide a valid Misskey username'); + } + if (!config.feature.allow_user_supply_unsafe_domain && !utils.allowSiteList.includes(site)) { + throw new ConfigNotFoundError(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`); + } + + const routeParams = querystring.parse(ctx.req.param('routeParams')); + const withRenotes = fallback(undefined, queryToBoolean(routeParams.withRenotes), false); + const mediaOnly = fallback(undefined, queryToBoolean(routeParams.mediaOnly), false); + const simplifyAuthor = fallback(undefined, queryToBoolean(routeParams.simplifyAuthor), false); + + // Check for conflicting parameters + if (withRenotes && mediaOnly) { + throw new InvalidParameterError('withRenotes and mediaOnly cannot both be true.'); + } + + const { accountData, avatarUrl } = await utils.getUserTimelineByUsername(pureUsername, site, { + withRenotes, + mediaOnly, + }); + + return { + title: `User timeline for ${username} on ${site}`, + link: `https://${site}/@${pureUsername}`, + image: avatarUrl ?? '', + item: utils.parseNotes(accountData, site, simplifyAuthor), + }; +} diff --git a/lib/routes/misskey/utils.ts b/lib/routes/misskey/utils.ts index 4c8bcfdf0f2ba7..e295da0749dfad 100644 --- a/lib/routes/misskey/utils.ts +++ b/lib/routes/misskey/utils.ts @@ -1,24 +1,64 @@ import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); -import { art } from '@/utils/render'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; import path from 'node:path'; +import { MisskeyNote, MisskeyUser } from './types'; + const allowSiteList = ['misskey.io', 'madost.one', 'mk.nixnet.social']; -// docs on: https://misskey-hub.net/docs/api/entity/note.html -const parseNotes = (data, site) => - data.map((item) => { - const host = item.user.host === null ? site : item.user.host; - const author = `${item.user.name} (@${item.user.username}@${host})`; +const parseNotes = (data: MisskeyNote[], site: string, simplifyAuthor: boolean = false) => + data.map((item: MisskeyNote) => { + const isRenote = item.renote && Object.keys(item.renote).length > 0; + const isReply = item.reply && Object.keys(item.reply).length > 0; + const noteToUse: MisskeyNote = isRenote ? (item.renote as MisskeyNote) : item; + + const host = noteToUse.user.host ?? site; + const author = simplifyAuthor ? String(noteToUse.user.name) : `${noteToUse.user.name} (${noteToUse.user.username}@${host})`; + const description = art(path.join(__dirname, 'templates/note.art'), { - text: item.text, - files: item.files, + text: noteToUse.text, + files: noteToUse.files, + reply: item.reply, + site, }); - const title = `${author}: "${description}"`; - const link = `https://${host}/notes/${item.id}`; - const pubDate = parseDate(item.createdAt); + + let title = ''; + if (isReply && item.reply) { + const replyToHost = item.reply.user.host ?? site; + const replyToAuthor = simplifyAuthor ? item.reply.user.name : `${item.reply.user.name} (${item.reply.user.username}@${replyToHost})`; + title = `Reply to ${replyToAuthor}: "${noteToUse.text ?? ''}"`; + } else if (isRenote) { + title = `Renote: ${author}: "${noteToUse.text ?? ''}"`; + } else { + title = `${author}: "${noteToUse.text ?? ''}"`; + } + + /** + * For renotes from non-Misskey instances (e.g. Mastodon, Pleroma), + * we can't use noteToUse.id to link to the original note since: + * 1. The URL format differs from Misskey's /notes/{id} pattern + * 2. Direct access to the original note may not be possible + * Therefore, we link to the renote itself in such cases + */ + let noteId = noteToUse.id; + + if (isRenote) { + const renoteHost = item.user.host ?? site; + const noteHost = noteToUse.user.host ?? site; + + // Use renote's ID if the note is from a different host or not in allowSiteList + if (renoteHost !== noteHost || !allowSiteList.includes(noteHost)) { + noteId = item.id; + } + } + const link = `https://${host}/notes/${noteId}`; + const pubDate = parseDate(noteToUse.createdAt); + return { title, description, @@ -27,5 +67,49 @@ const parseNotes = (data, site) => author, }; }); +async function getUserTimelineByUsername(username, site, { withRenotes = false, mediaOnly = false }) { + const searchUrl = `https://${site}/api/users/search-by-username-and-host`; + const cacheUid = `misskey_username/${site}/${username}`; + + const userData = (await cache.tryGet(cacheUid, async () => { + const searchResponse = await got({ + method: 'post', + url: searchUrl, + json: { + username, + host: site, + detail: true, + limit: 1, + }, + }); + const user = searchResponse.data.find((item) => item.username === username); + + if (!user) { + throw new Error(`username ${username} not found`); + } + return user; + })) as MisskeyUser; + + const accountId = userData.id; + const avatarUrl = userData.avatarUrl; + + // https://misskey.io/api-doc#tag/users/operation/users___notes + const usernotesUrl = `https://${site}/api/users/notes`; + const usernotesResponse = await got({ + method: 'post', + url: usernotesUrl, + json: { + userId: accountId, + withChannelNotes: true, + withRenotes, + withReplies: !mediaOnly, // Disable replies if mediaOnly is true + withFiles: mediaOnly, + limit: 10, + offset: 0, + }, + }); + const accountData = usernotesResponse.data; + return { site, accountId, accountData, avatarUrl }; +} -export default { parseNotes, allowSiteList }; +export default { parseNotes, getUserTimelineByUsername, allowSiteList }; diff --git a/lib/routes/misskon/namespace.ts b/lib/routes/misskon/namespace.ts new file mode 100644 index 00000000000000..ec718df3ca1aa7 --- /dev/null +++ b/lib/routes/misskon/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'MissKON', + url: 'misskon.com', + lang: 'en', +}; diff --git a/lib/routes/misskon/posts.ts b/lib/routes/misskon/posts.ts new file mode 100644 index 00000000000000..445bf6bc13af9c --- /dev/null +++ b/lib/routes/misskon/posts.ts @@ -0,0 +1,33 @@ +import { Route } from '@/types'; +import { ENDPOINT, getPosts } from './utils'; + +export const route: Route = { + path: '/posts/:routeParams?', + categories: ['picture'], + example: '/misskon/posts/search=video&tags_exclude=353,3100&per_page=5', + parameters: { routeParams: 'Additional parameters for filtering posts, refer to [WordPress API Reference](https://developer.wordpress.org/rest-api/reference/posts/#arguments) for details.' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['misskon.com/'], + target: '/posts', + }, + ], + name: 'Posts', + maintainers: ['Urabartin'], + handler: async (ctx) => { + const { routeParams = '' } = ctx.req.param(); + return { + title: `MissKON - ${routeParams || 'Posts'}`, + link: `${ENDPOINT}/posts` + (routeParams ? `?${routeParams}` : ''), + item: await getPosts(routeParams), + }; + }, +}; diff --git a/lib/routes/misskon/tag.ts b/lib/routes/misskon/tag.ts new file mode 100644 index 00000000000000..73b591fed3115c --- /dev/null +++ b/lib/routes/misskon/tag.ts @@ -0,0 +1,38 @@ +import { Route } from '@/types'; +import { getPosts, getTags } from './utils'; + +export const route: Route = { + path: '/tag/:tag', + categories: ['picture'], + example: '/misskon/tag/cosplay', + parameters: { tag: 'Any tag that exists in MissKon' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['misskon.com/tag/:tag/'], + target: '/tag/:tag', + }, + ], + name: 'Tag', + maintainers: ['Urabartin'], + handler: async (ctx) => { + const { tag } = ctx.req.param(); + const tagData = await getTags(tag); + const searchParams = new URLSearchParams(); + searchParams.set('tags', tagData.id); + const items = await getPosts(searchParams.toString()); + return { + title: `MissKON - ${tagData.name}`, + link: tagData.link, + description: tagData.description, + item: items, + }; + }, +}; diff --git a/lib/routes/misskon/top.ts b/lib/routes/misskon/top.ts new file mode 100644 index 00000000000000..542480a49f0722 --- /dev/null +++ b/lib/routes/misskon/top.ts @@ -0,0 +1,67 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { getPosts } from './utils'; + +export const route: Route = { + path: '/top/:k', + categories: ['picture'], + example: '/misskon/top/60', + parameters: { k: 'Top k days, can be 3, 7, 30 or 60' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + title: 'Top 3 days', + source: ['misskon.com/top3/'], + target: '/top/3', + }, + { + title: 'Top 7 days', + source: ['misskon.com/top7/'], + target: '/top/7', + }, + { + title: 'Top 30 days', + source: ['misskon.com/top30/'], + target: '/top/30', + }, + { + title: 'Top 60 days', + source: ['misskon.com/top60/'], + target: '/top/60', + }, + ], + name: 'Top k days', + maintainers: ['Urabartin'], + handler: async (ctx) => { + const { k } = ctx.req.param(); + if (!['3', '7', '30', '60'].includes(k)) { + throw new Error(`Invalid k: k=${k}`); + } + const topLink = `https://misskon.com/top${k}/`; + const response = await ofetch(topLink); + const $ = load(response); + + const feedTitle = $('.page-title').text(); + const feedDesc = $('.content > p').first().text(); + const itemSlugs = $('#main-content article.item-list > h2 a') + .toArray() + .map((link) => new URL($(link).attr('href') || '').pathname.slice(1, -1)); + const searchParams = new URLSearchParams(); + searchParams.set('slug', itemSlugs.join(',')); + searchParams.set('per_page', itemSlugs.length.toString()); + return { + title: `MissKON - ${feedTitle}`, + link: topLink, + description: feedDesc, + item: await getPosts(searchParams), + }; + }, +}; diff --git a/lib/routes/misskon/utils.ts b/lib/routes/misskon/utils.ts new file mode 100644 index 00000000000000..7445fa20a30c80 --- /dev/null +++ b/lib/routes/misskon/utils.ts @@ -0,0 +1,41 @@ +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const ENDPOINT = 'https://misskon.com/wp-json/wp/v2'; +const getPosts = async (searchParams) => { + const url = new URL(`${ENDPOINT}/posts?${searchParams}`); + url.searchParams.append('_embed', 'wp:term'); + const data = await ofetch(url.href); + return data.map((item) => { + const $ = load(item.content.rendered); + $('input').each(function () { + $(this).replaceWith($(this).attr('value') || ''); + }); + $('script').remove(); + return { + title: item.title.rendered, + link: item.link, + description: $.html(), + pubDate: timezone(parseDate(item.date_gmt), 0), + category: item._embedded['wp:term'] + .flat() + .filter((x) => x.taxonomy === 'post_tag') + .map((x) => x.name), + }; + }); +}; +const getTags = async (slug) => { + const data = await ofetch(`${ENDPOINT}/tags?slug=${slug}`); + if (data.length === 0) { + throw new Error(`Invalid tag slug: ${slug}`); + } + return { + id: data[0].id, + name: data[0].name, + link: data[0].link, + description: data[0].description, + }; +}; +export { ENDPOINT, getPosts, getTags }; diff --git a/lib/routes/mittrchina/index.ts b/lib/routes/mittrchina/index.ts index 2fcfb22fe87f1b..9a39fa15ed6941 100644 --- a/lib/routes/mittrchina/index.ts +++ b/lib/routes/mittrchina/index.ts @@ -10,7 +10,7 @@ import path from 'node:path'; export const route: Route = { path: '/:type?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/mittrchina/index', parameters: { type: '类型,见下表,默认为首页资讯' }, features: { @@ -25,8 +25,8 @@ export const route: Route = { maintainers: ['EsuRt', 'queensferryme'], handler, description: `| 快讯 | 本周热文 | 首页资讯 | 视频 | - | -------- | -------- | -------- | ----- | - | breaking | hot | index | video |`, +| -------- | -------- | -------- | ----- | +| breaking | hot | index | video |`, }; async function handler(ctx) { @@ -52,7 +52,7 @@ async function handler(ctx) { const { type = 'index' } = ctx.req.param(); const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; - const link = `https://apii.mittrchina.com${typeMap[type].apiPath}`; + const link = `https://apii.web.mittrchina.com${typeMap[type].apiPath}`; const { data: response } = type === 'breaking' ? await got.post(link, { @@ -82,20 +82,22 @@ async function handler(ctx) { type: article.address.split('.').pop(), }, }) - : article.summary, - pubDate: article.start_time ? parseDate(article.start_time, 'X') : undefined, + : (type === 'breaking' + ? article.content + : article.summary), + pubDate: article.start_time ? parseDate(article.start_time, 'X') : (article.push_time ? parseDate(article.push_time, 'X') : undefined), id: article.id, link: `https://www.mittrchina.com/news/detail/${article.id}`, })); let items = list; - if (type !== 'video') { + if (type !== 'video' && type !== 'breaking') { items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { const { data: { data: details }, - } = await got(`https://apii.mittrchina.com/information/details?id=${item.id}`); + } = await got(`https://apii.web.mittrchina.com/information/details?id=${item.id}`); item.description = details.content; diff --git a/lib/routes/mittrchina/namespace.ts b/lib/routes/mittrchina/namespace.ts index de5d9694d96dc1..e3dd40df5e64f0 100644 --- a/lib/routes/mittrchina/namespace.ts +++ b/lib/routes/mittrchina/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '麻省理工科技评论', url: 'mittrchina.com', + lang: 'zh-CN', }; diff --git a/lib/routes/miui/firmware/index.ts b/lib/routes/miui/firmware/index.ts index ffdeb734f61e6a..df911f54948ee5 100644 --- a/lib/routes/miui/firmware/index.ts +++ b/lib/routes/miui/firmware/index.ts @@ -10,13 +10,13 @@ export const route: Route = { name: 'New firmware', maintainers: ['Indexyz'], description: ` | stable | development | - | ------- | ----------- | - | release | dev | - - | region | region | - | ------ | ------ | - | China | cn | - | Global | global |`, +| ------- | ----------- | +| release | dev | + +| region | region | +| ------ | ------ | +| China | cn | +| Global | global |`, handler, }; diff --git a/lib/routes/miui/namespace.ts b/lib/routes/miui/namespace.ts index aafb64e596808d..2ec1b628f7027e 100644 --- a/lib/routes/miui/namespace.ts +++ b/lib/routes/miui/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MIUI', url: 'miui.com', + lang: 'zh-CN', }; diff --git a/lib/routes/mixcloud/index.ts b/lib/routes/mixcloud/index.ts index 59897d9e7ab77b..a4204d78979f1c 100644 --- a/lib/routes/mixcloud/index.ts +++ b/lib/routes/mixcloud/index.ts @@ -30,8 +30,8 @@ export const route: Route = { maintainers: ['Misaka13514'], handler, description: `| Shows | Reposts | Favorites | History | Stream | - | ------- | ------- | --------- | ------- | ------ | - | uploads | reposts | favorites | listens | stream |`, +| ------- | ------- | --------- | ------- | ------ | +| uploads | reposts | favorites | listens | stream |`, }; async function handler(ctx) { diff --git a/lib/routes/mixcloud/namespace.ts b/lib/routes/mixcloud/namespace.ts index fc6d82260a2081..025ee5a1edc2de 100644 --- a/lib/routes/mixcloud/namespace.ts +++ b/lib/routes/mixcloud/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Mixcloud', url: 'www.mixcloud.com', + lang: 'en', }; diff --git a/lib/routes/modb/namespace.ts b/lib/routes/modb/namespace.ts index 1cbe88ce5eb38e..701e45eeeba59c 100644 --- a/lib/routes/modb/namespace.ts +++ b/lib/routes/modb/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '墨天轮', url: 'modb.pro', + lang: 'zh-CN', }; diff --git a/lib/routes/modb/topic.ts b/lib/routes/modb/topic.ts index 69666942183927..d577d71bd0bdb2 100644 --- a/lib/routes/modb/topic.ts +++ b/lib/routes/modb/topic.ts @@ -1,7 +1,7 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import { load } from 'cheerio'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import logger from '@/utils/logger'; import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; @@ -27,14 +27,13 @@ export const route: Route = { async function handler(ctx) { const baseUrl = 'https://www.modb.pro'; const topicId = ctx.req.param('id'); - const response = await got({ - url: `${baseUrl}/api/columns/getKnowledge`, - searchParams: { + const response = await ofetch(`${baseUrl}/api/columns/getKnowledge`, { + query: { pageNum: 1, pageSize: 20, columnId: topicId, }, - }).json(); + }); const list = response.list.map((item) => { let doc = {}; let baseLink = {}; @@ -63,7 +62,7 @@ async function handler(ctx) { const items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { - const { data: response } = await got(item.link); + const response = await ofetch(item.link); const $ = load(response); item.description = $('div.editor-content-styl.article-style').first().html(); return item; diff --git a/lib/routes/modelscope/namespace.ts b/lib/routes/modelscope/namespace.ts index 1bd398f851c4c4..0eef487c19a84f 100644 --- a/lib/routes/modelscope/namespace.ts +++ b/lib/routes/modelscope/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ModelScope 魔搭社区', url: 'modelscope.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/modian/namespace.ts b/lib/routes/modian/namespace.ts index 16e1daa8d319d8..5444049c1409f7 100644 --- a/lib/routes/modian/namespace.ts +++ b/lib/routes/modian/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '摩点', url: 'modian.com', + lang: 'zh-CN', }; diff --git a/lib/routes/modian/zhongchou.ts b/lib/routes/modian/zhongchou.ts index 24d1d779b6ff84..701e37ebbc5b40 100644 --- a/lib/routes/modian/zhongchou.ts +++ b/lib/routes/modian/zhongchou.ts @@ -12,33 +12,33 @@ export const route: Route = { maintainers: ['nczitzk'], description: `分类 - | 全部 | 游戏 | 动漫 | 出版 | 桌游 | - | ---- | ----- | ------ | ---------- | ---------- | - | all | games | comics | publishing | tablegames | +| 全部 | 游戏 | 动漫 | 出版 | 桌游 | +| ---- | ----- | ------ | ---------- | ---------- | +| all | games | comics | publishing | tablegames | - | 卡牌 | 潮玩模型 | 影视 | 音乐 | 活动 | - | ----- | -------- | ---------- | ----- | ---------- | - | cards | toys | film-video | music | activities | +| 卡牌 | 潮玩模型 | 影视 | 音乐 | 活动 | +| ----- | -------- | ---------- | ----- | ---------- | +| cards | toys | film-video | music | activities | - | 设计 | 科技 | 食品 | 爱心通道 | 动物救助 | - | ------ | ---------- | ---- | -------- | -------- | - | design | technology | food | charity | animals | +| 设计 | 科技 | 食品 | 爱心通道 | 动物救助 | +| ------ | ---------- | ---- | -------- | -------- | +| design | technology | food | charity | animals | - | 个人愿望 | 其他 | - | -------- | ------ | - | wishes | others | +| 个人愿望 | 其他 | +| -------- | ------ | +| wishes | others | - 排序 + 排序 - | 最新上线 | 金额最高 | 评论最多 | - | --------- | ---------- | ------------ | - | top\_time | top\_money | top\_comment | +| 最新上线 | 金额最高 | 评论最多 | +| --------- | ---------- | ------------ | +| top\_time | top\_money | top\_comment | - 状态 + 状态 - | 全部 | 创意 | 预热 | 众筹中 | 众筹成功 | - | ---- | ---- | ------- | ------ | -------- | - | all | idea | preheat | going | success |`, +| 全部 | 创意 | 预热 | 众筹中 | 众筹成功 | +| ---- | ---- | ------- | ------ | -------- | +| all | idea | preheat | going | success |`, handler, radar: [ { diff --git a/lib/routes/modrinth/namespace.ts b/lib/routes/modrinth/namespace.ts index c85ca0d9f961df..f0c08a5aec8fba 100644 --- a/lib/routes/modrinth/namespace.ts +++ b/lib/routes/modrinth/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Modrinth', url: 'modrinth.com', + lang: 'en', }; diff --git a/lib/routes/mohw/namespace.ts b/lib/routes/mohw/namespace.ts index 9e456195cfae18..3cc9224b2e2371 100644 --- a/lib/routes/mohw/namespace.ts +++ b/lib/routes/mohw/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '台灣衛生福利部', url: 'mohw.gov.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/moodysmismicrosite/namespace.ts b/lib/routes/moodysmismicrosite/namespace.ts new file mode 100644 index 00000000000000..28787160da37e5 --- /dev/null +++ b/lib/routes/moodysmismicrosite/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '穆迪评级', + url: 'www.moodysmismicrosite.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/moodysmismicrosite/report.ts b/lib/routes/moodysmismicrosite/report.ts new file mode 100644 index 00000000000000..785b76f7df89ff --- /dev/null +++ b/lib/routes/moodysmismicrosite/report.ts @@ -0,0 +1,90 @@ +import { Route, ViewType } from '@/types'; +import { parseRelativeDate } from '@/utils/parse-date'; +import got from '@/utils/got'; + +export const route: Route = { + path: '/report/:industry?', + categories: ['finance'], + view: ViewType.Articles, + example: '/moodysmismicrosite/report/企业&金融机构', + parameters: { + industry: { + description: '可选参数,默认为全部行业。行业选择,支持使用&连接多个。', + options: [ + { value: '0', label: '企业' }, + { value: '1', label: '金融机构' }, + { value: '2', label: '主权' }, + { value: '3', label: '地方政府及城投公司' }, + { value: '4', label: '宏观经济' }, + { value: '5', label: '结构融资' }, + { value: '6', label: '基础设施及项目融资' }, + { value: '7', label: 'ESG' }, + { value: '8', label: '其他' }, + ], + default: '全部', + }, + }, + radar: [ + { + source: ['www.moodysmismicrosite.com/report'], + }, + ], + name: 'industry', + description: ` +| ID | Description | +| --- | --- | +| 0 | 企业 | +| 1 | 金融机构 | +| 2 | 主权 | +| 3 | 地方政府及城投公司 | +| 4 | 宏观经济 | +| 5 | 结构融资 | +| 6 | 基础设施项目融资 | +| 7 | ESG | +| 8 | 其他 | + `, + maintainers: ['FYLSen'], + handler, +}; + +async function handler(ctx) { + const industryMap: Record<number, string> = { + 0: '企业', + 1: '金融机构', + 2: '主权', + 3: '地方政府及城投公司', + 4: '宏观经济', + 5: '结构融资', + 6: '基础设施项目融资', + 7: 'ESG', + 8: '其他', + }; + + const reversedIndustry = Object.fromEntries(Object.entries(industryMap).map(([k, v]) => [v, k])); + + const industry = ctx.req.param('industry') || '行业'; + + const industryId = industry + .split('&') + .map((name) => reversedIndustry[name.trim()]) + .filter((key) => key !== undefined) + .join(','); + + const responseData = await got(`https://www.moodysmismicrosite.com/micro/v2/report/list?page_num=1&page_size=25&keyword=&industry_ids=${industryId}`); + + const items = responseData?.data?.data?.list || []; + + return { + title: `穆迪评级(${industry})`, + link: 'https://www.moodysmismicrosite.com/report', + allowEmpty: true, + item: items.map((x) => ({ + title: x.title, + pubDate: parseRelativeDate(x.release_time), + link: `https://www.moodysmismicrosite.com/details/${x.id}`, + description: x.content_profile, + category: x.classification, + guid: x.id, + })), + }; +} diff --git a/lib/routes/mox/index.ts b/lib/routes/mox/index.ts index bf42111205f84f..ce3afb137d8a4a 100644 --- a/lib/routes/mox/index.ts +++ b/lib/routes/mox/index.ts @@ -27,15 +27,15 @@ export const route: Route = { name: '首頁', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 在首页将分类参数选择确定后跳转到的分类页面 URL 中,\`/l/\` 后的字段即为分类参数。 如 [科幻 + 日語 + 日本 + 長篇 + 完結 + 最近更新](https://mox.moe/l/CAT%2A科幻,日本,完結,lastupdate,jpn,l,BL) 的 URL 为 [https://mox.moe/l/CAT%2A 科幻,日本,完結,lastupdate,jpn,l,BL](https://mox.moe/l/CAT%2A科幻,日本,完結,lastupdate,jpn,l,BL),此时 \`/l/\` 后的字段为 \`CAT%2A科幻,日本,完結,lastupdate,jpn,l,BL\`。最终获得路由为 [\`/mox/CAT%2A科幻,日本,完結,lastupdate,jpn,l,BL\`](https://rsshub.app/mox/CAT%2A科幻,日本,完結,lastupdate,jpn,l,BL) - ::: +::: - :::warning +::: warning 由于 mox.moe 对非登录用户屏蔽了部分漫画详情内容的获取,且极易触发反爬机制,导致访问ip被重定向至google.com,因此在未配置\`MOX_COOKIE\`参数的情况下路由只会返回漫画标题和封面,不会对详情内容进行抓取。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/mox/namespace.ts b/lib/routes/mox/namespace.ts index 4d329b35251bf8..b31a8d5d63cb8f 100644 --- a/lib/routes/mox/namespace.ts +++ b/lib/routes/mox/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Mox.moe', url: 'mox.moe', + lang: 'zh-TW', }; diff --git a/lib/routes/mpaypass/namespace.ts b/lib/routes/mpaypass/namespace.ts index e7112a1bcfb2ed..947971182d1e45 100644 --- a/lib/routes/mpaypass/namespace.ts +++ b/lib/routes/mpaypass/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '移动支付网', url: 'mpaypass.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/mrdx/namespace.ts b/lib/routes/mrdx/namespace.ts index 7a7e83bf431d37..fd44489e383737 100644 --- a/lib/routes/mrdx/namespace.ts +++ b/lib/routes/mrdx/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '新华每日电讯', url: 'mrdx.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/mrm/index.ts b/lib/routes/mrm/index.ts index 22ace8e8acc7f7..913ae0db879c9a 100644 --- a/lib/routes/mrm/index.ts +++ b/lib/routes/mrm/index.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| 交易通知 | 政策规定 | 业务通知 | - | ------------ | -------------------- | ----------------- | - | zonghezixun3 | zhengceguiding\_list | yewutongzhi\_list |`, +| ------------ | -------------------- | ----------------- | +| zonghezixun3 | zhengceguiding\_list | yewutongzhi\_list |`, }; async function handler(ctx) { diff --git a/lib/routes/mrm/namespace.ts b/lib/routes/mrm/namespace.ts index e3827f11913a49..d6ac0e021563bb 100644 --- a/lib/routes/mrm/namespace.ts +++ b/lib/routes/mrm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '华储网', url: 'mrm.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/msn/index.ts b/lib/routes/msn/index.ts new file mode 100644 index 00000000000000..aff863e1330fe9 --- /dev/null +++ b/lib/routes/msn/index.ts @@ -0,0 +1,86 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; + +const apiKey = '0QfOX3Vn51YCzitbLaRkTTBadtWpgTN8NZLW0C1SEM'; +const fetchedArticleContentHtmlImgRegex = /<img data-reference="image" data-document-id="cms\/api\/amp\/image\/([A-Za-z0-9]+)">/; + +export const route: Route = { + path: '/:market/:name/:id', + parameters: { + market: 'Market code. Find it in MSN url, e.g. zh-tw', + name: 'Name of the channel. Find it in MSN url, e.g. Bloomberg', + id: 'ID of the channel (always starts with sr-vid). Find it in MSN url, e.g. sr-vid-08gw7ky4u229xjsjvnf4n6n7v67gxm0pjmv9fr4y2x9jjmwcri4s', + }, + categories: ['traditional-media'], + example: '/zh-tw/Bloomberg/sr-vid-08gw7ky4u229xjsjvnf4n6n7v67gxm0pjmv9fr4y2x9jjmwcri4s', + description: `MSN News`, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + }, + radar: [ + { + source: ['www.msn.com/:market/channel/source/:name/:id'], + target: '/:market/:name/:id', + }, + ], + name: 'News', + maintainers: ['KTachibanaM'], + handler: async (ctx) => { + const { market, name, id } = ctx.req.param(); + let truncatedId = id; + if (truncatedId.startsWith('sr-')) { + truncatedId = truncatedId.substring(3); + } + + const pageData = await ofetch(`https://www.msn.com/${market}/channel/source/${name}/${id}`); + const $ = load(pageData); + const headElement = $('head'); + const dataClientSettings = headElement.attr('data-client-settings') ?? '{}'; + const parsedSettings = JSON.parse(dataClientSettings); + const requestMuid = parsedSettings.fd_muid; + + const jsonData = await ofetch(`https://assets.msn.com/service/news/feed/pages/providerfullpage?market=${market}&query=newest&CommunityProfileId=${truncatedId}&apikey=${apiKey}&user=m-${requestMuid}`); + const items = await Promise.all( + jsonData.sections[0].cards.map(async (card) => { + let articleContentHtml = ''; + + const articleUrl = card.url; + const parsedArticleUrl = URL.parse(articleUrl); + let articleId = parsedArticleUrl?.pathname.split('/').pop(); + if (articleId?.startsWith('ar-')) { + articleId = articleId.substring(3); + const fetchedArticleContentHtml = (await cache.tryGet(articleId, async () => { + const articleData = await ofetch(`https://assets.msn.com/content/view/v2/Detail/${market}/${articleId}`); + return articleData.body; + })) as string; + articleContentHtml = fetchedArticleContentHtml.replace(fetchedArticleContentHtmlImgRegex, '<img src="https://img-s-msn-com.akamaized.net/tenant/amp/entityid/$1.img">'); + } + + return { + title: card.title, + link: articleUrl, + description: card.abstract, + content: { + html: articleContentHtml, + }, + pubDate: parseDate(card.publishedDateTime), + category: [card.category], + }; + }) + ); + + const channelLink = `https://www.msn.com/${market}/channel/source/${name}/${id}`; + return { + title: name, + image: 'https://www.msn.com/favicon.ico', + link: channelLink, + item: items, + }; + }, +}; diff --git a/lib/routes/msn/namespace.ts b/lib/routes/msn/namespace.ts new file mode 100644 index 00000000000000..bb04523c2328f6 --- /dev/null +++ b/lib/routes/msn/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'MSN', + url: 'msn.com', + + zh: { + name: 'MSN', + }, +}; diff --git a/lib/routes/mvm/index.ts b/lib/routes/mwm/index.ts similarity index 94% rename from lib/routes/mvm/index.ts rename to lib/routes/mwm/index.ts index 3a62b8925fe6a2..3847b37081df67 100644 --- a/lib/routes/mvm/index.ts +++ b/lib/routes/mwm/index.ts @@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/:category?', categories: ['journal'], - example: '/mvm', + example: '/mwm', parameters: { category: '分类,见下表,默认为本期要目' }, features: { requireConfig: false, @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 本期要目 | 网络首发 | 学术活动 | 通知公告 | - | -------- | -------- | -------- | -------- | - | bqym | wlsf | xshd | tzgg |`, +| -------- | -------- | -------- | -------- | +| bqym | wlsf | xshd | tzgg |`, }; async function handler(ctx) { diff --git a/lib/routes/mvm/namespace.ts b/lib/routes/mwm/namespace.ts similarity index 87% rename from lib/routes/mvm/namespace.ts rename to lib/routes/mwm/namespace.ts index fcbb22cdcab6c0..b1b89c1babaeb8 100644 --- a/lib/routes/mvm/namespace.ts +++ b/lib/routes/mwm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '管理世界', url: 'mwm.net.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/my-formosa/index.ts b/lib/routes/my-formosa/index.ts new file mode 100644 index 00000000000000..231f414fb43b1a --- /dev/null +++ b/lib/routes/my-formosa/index.ts @@ -0,0 +1,78 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import timezone from '@/utils/timezone'; + +export const route: Route = { + path: '/', + categories: ['new-media'], + example: '/my-formosa', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['my-formosa.com/'], + }, + ], + name: '首页', + maintainers: ['dzx-dzx'], + handler, + url: 'my-formosa.com', +}; + +function fetch(url) { + return ofetch(url, { responseType: 'arrayBuffer' }).then((raw) => { + const decoder = new TextDecoder('big5'); + return decoder.decode(raw); + }); +} + +async function handler() { + const rootUrl = 'http://www.my-formosa.com/'; + + const res = await fetch(rootUrl); + + const $ = load(res); + + const items = await Promise.all( + $('#featured-news h3 a') + .toArray() + .map((item) => { + item = $(item); + + const title = item.text(); + const link = new URL(item.attr('href'), rootUrl).href; + + return cache.tryGet(link, async () => { + const res = await fetch(link); + const $ = load(res); + + const isTV = /^\/TV/.test(new URL(link).pathname); + + return { + title, + link, + author: $('.page-header~#featured-news h4').text(), + category: $("meta[name='keywords']").attr('content').split(',').filter(Boolean), + pubDate: timezone(parseDate((isTV ? $('.icon-calendar')[0].next.data : $('.date').text()).trim()), +8), + description: (isTV ? $('.post-item').html() : $('.body').html()).replaceAll(/\/News.*?\.jpg/g, (match) => `http://my-formosa.com${match}`), + }; + }); + }) + ); + + return { + title: $('title').text(), + link: rootUrl, + item: items, + }; +} diff --git a/lib/routes/my-formosa/namespace.ts b/lib/routes/my-formosa/namespace.ts new file mode 100644 index 00000000000000..8cd9c24c6c3596 --- /dev/null +++ b/lib/routes/my-formosa/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '美麗島電子報', + url: 'my-formosa.com', + lang: 'zh-TW', +}; diff --git a/lib/routes/mycard520/namespace.ts b/lib/routes/mycard520/namespace.ts new file mode 100644 index 00000000000000..66fb68d3d6c89e --- /dev/null +++ b/lib/routes/mycard520/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'MyCard娛樂中心', + url: 'mycard520.com.tw', + categories: ['game'], + description: '', + lang: 'zh-TW', +}; diff --git a/lib/routes/mycard520/news.ts b/lib/routes/mycard520/news.ts new file mode 100644 index 00000000000000..02c377ba79303a --- /dev/null +++ b/lib/routes/mycard520/news.ts @@ -0,0 +1,252 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +import cache from '@/utils/cache'; +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'; + +export const handler = async (ctx: Context): Promise<Data> => { + const { category = 'cardgame' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '18', 10); + + const baseUrl: string = 'https://app.mycard520.com.tw'; + const targetUrl: string = new URL(`category/${category.endsWith('/') ? category : `${category}/`}`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'zh-TW'; + + let items: DataItem[] = []; + + $('div.page_numbers').remove(); + + items = $('div#tab1 ul li') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio<Element> = $(el); + const $aEl: Cheerio<Element> = $el.find('a'); + + const title: string = $el.find('div.text_box p').text(); + const description: string | undefined = $aEl.html() ?? undefined; + const pubDateStr: string | undefined = $el.find('div.date').text().trim(); + const linkUrl: string | undefined = $aEl.attr('href'); + const image: string | undefined = $el.find('div.img_box img').attr('src'); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl, + content: { + html: description ?? '', + text: description ?? '', + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + const $$pageBox: Cheerio<Element> = $$('div.page_box'); + + const title: string = $$pageBox.find('h2').text(); + const pubDateStr: string | undefined = $$('div.date').first().text(); + const upDatedStr: string | undefined = pubDateStr; + + const clearIndex = $$pageBox + .children() + .toArray() + .map((el, index) => ({ el, index })) + .filter(({ el }) => el.tagName === 'div' && el.attributes.some((attr) => attr.name === 'style' && attr.value.split(';').some((prop) => prop.trim() === 'clear:both'))) + .reduce((_, { index }) => index, -1); + + if (clearIndex !== -1) { + $$pageBox.children().slice(0, clearIndex).remove(); + } + + const description: string | undefined = $$pageBox.html() ?? item.description; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + content: { + html: description ?? '', + text: description ?? '', + }, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[name="keywords"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('div.logo img').attr('src'), + author: $('title').text().split(/-/).pop()?.trim(), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/category/:category?', + name: '遊戲新聞', + url: 'app.mycard520.com.tw', + maintainers: ['nczitzk'], + handler, + example: '/mycard520/category/cardgame', + parameters: { + category: { + description: '分类,默认为 `cardgame`,即最新遊戲,可在对应分类页 URL 中找到', + options: [ + { + label: '最新遊戲', + value: 'cardgame', + }, + { + label: '手機遊戲', + value: 'cardgame-mobile', + }, + { + label: 'PC 遊戲', + value: 'cardgame-pc', + }, + { + label: '電競賽事', + value: 'cardgame-esports', + }, + { + label: '實況直播', + value: 'cardgame-live', + }, + ], + }, + }, + description: `:::tip +若订阅 [最新遊戲](https://app.mycard520.com.tw/category/cardgame/),网址为 \`https://app.mycard520.com.tw/category/cardgame/\`,请截取 \`https://app.mycard520.com.tw/category/\` 到末尾 \`/\` 的部分 \`cardgame\` 作为 \`category\` 参数填入,此时目标路由为 [\`/mycard520/category/cardgame\`](https://rsshub.app/mycard520/category/cardgame)。 +::: + +| [最新遊戲](https://app.mycard520.com.tw/category/cardgame/) | [手機遊戲](https://app.mycard520.com.tw/category/cardgame-mobile/) | [PC 遊戲](https://app.mycard520.com.tw/category/cardgame-pc/) | [電競賽事](https://app.mycard520.com.tw/category/cardgame-esports/) | [實況直播](https://app.mycard520.com.tw/category/cardgame-live/) | +| ----------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| [cardgame](https://rsshub.app/mycard520/category/cardgame) | [cardgame-mobile](https://rsshub.app/mycard520/category/cardgame-mobile) | [cardgame-pc](https://rsshub.app/mycard520/category/cardgame-pc) | [cardgame-esports](https://rsshub.app/mycard520/category/cardgame-esports) | [cardgame-live](https://rsshub.app/mycard520/category/cardgame-live) | +`, + categories: ['game'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['app.mycard520.com.tw/category/:category'], + target: (params) => { + const category: string = params.category; + + return `/mycard520${category ? `/${category}` : ''}`; + }, + }, + { + title: '最新遊戲', + source: ['app.mycard520.com.tw/category/cardgame'], + target: '/category/cardgame', + }, + { + title: '手機遊戲', + source: ['app.mycard520.com.tw/category/cardgame-mobile'], + target: '/category/cardgame-mobile', + }, + { + title: 'PC 遊戲', + source: ['app.mycard520.com.tw/category/cardgame-pc'], + target: '/category/cardgame-pc', + }, + { + title: '電競賽事', + source: ['app.mycard520.com.tw/category/cardgame-esports'], + target: '/category/cardgame-esports', + }, + { + title: '實況直播', + source: ['app.mycard520.com.tw/category/cardgame-live'], + target: '/category/cardgame-live', + }, + ], + view: ViewType.Articles, + + zh: { + path: '/category/:category?', + name: '游戏新闻', + url: 'app.mycard520.com.tw', + maintainers: ['nczitzk'], + handler, + example: '/mycard520/category/cardgame', + parameters: { + category: { + description: '分类,默认为 `cardgame`,即最新游戏,可在对应分类页 URL 中找到', + options: [ + { + label: '最新游戏', + value: 'cardgame', + }, + { + label: '手机游戏', + value: 'cardgame-mobile', + }, + { + label: 'PC 游戏', + value: 'cardgame-pc', + }, + { + label: '电竞赛事', + value: 'cardgame-esports', + }, + { + label: '实况直播', + value: 'cardgame-live', + }, + ], + }, + }, + description: `:::tip +若订阅 [最新游戏](https://app.mycard520.com.tw/category/cardgame/),网址为 \`https://app.mycard520.com.tw/category/cardgame/\`,请截取 \`https://app.mycard520.com.tw/category/\` 到末尾 \`/\` 的部分 \`cardgame\` 作为 \`category\` 参数填入,此时目标路由为 [\`/mycard520/category/cardgame\`](https://rsshub.app/mycard520/category/cardgame)。 +::: + +| [最新游戏](https://app.mycard520.com.tw/category/cardgame/) | [手机游戏](https://app.mycard520.com.tw/category/cardgame-mobile/) | [PC 游戏](https://app.mycard520.com.tw/category/cardgame-pc/) | [电竞赛事](https://app.mycard520.com.tw/category/cardgame-esports/) | [实况直播](https://app.mycard520.com.tw/category/cardgame-live/) | +| ----------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| [cardgame](https://rsshub.app/mycard520/category/cardgame) | [cardgame-mobile](https://rsshub.app/mycard520/category/cardgame-mobile) | [cardgame-pc](https://rsshub.app/mycard520/category/cardgame-pc) | [cardgame-esports](https://rsshub.app/mycard520/category/cardgame-esports) | [cardgame-live](https://rsshub.app/mycard520/category/cardgame-live) | +`, + }, +}; diff --git a/lib/routes/mydrivers/namespace.ts b/lib/routes/mydrivers/namespace.ts index 1bd863aea584db..47c4ade4b0ab3a 100644 --- a/lib/routes/mydrivers/namespace.ts +++ b/lib/routes/mydrivers/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '快科技', url: 'm.mydrivers.com', + lang: 'zh-CN', }; diff --git a/lib/routes/mydrivers/rank.ts b/lib/routes/mydrivers/rank.ts index 8f674dc9b97759..e769f6d98b4db0 100644 --- a/lib/routes/mydrivers/rank.ts +++ b/lib/routes/mydrivers/rank.ts @@ -29,8 +29,8 @@ export const route: Route = { handler, url: 'm.mydrivers.com/newsclass.aspx', description: `| 24 小时最热 | 本周最热 | 本月最热 | - | ----------- | -------- | -------- | - | 0 | 1 | 2 |`, +| ----------- | -------- | -------- | +| 0 | 1 | 2 |`, }; async function handler(ctx) { diff --git a/lib/routes/myfans/namespace.ts b/lib/routes/myfans/namespace.ts new file mode 100644 index 00000000000000..3e3b5ce62f7d9b --- /dev/null +++ b/lib/routes/myfans/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'myfans', + url: 'myfans.jp', + lang: 'ja', +}; diff --git a/lib/routes/myfans/post.ts b/lib/routes/myfans/post.ts new file mode 100644 index 00000000000000..fb1b6ea328eb1c --- /dev/null +++ b/lib/routes/myfans/post.ts @@ -0,0 +1,64 @@ +import { Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import { showByUsername, getPostByAccountId, baseUrl } from './utils'; +import path from 'node:path'; +import { art } from '@/utils/render'; +import { getCurrentPath } from '@/utils/helpers'; + +const __dirname = getCurrentPath(import.meta.url); + +export const route: Route = { + path: '/user/:username', + categories: ['multimedia'], + example: '/myfans/user/secret_japan', + parameters: { username: 'User handle' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['myfans.jp/:username', 'myfans.jp/:language/:username'], + }, + ], + name: 'User Posts', + maintainers: ['TonyRL'], + handler, +}; + +const render = (postImages, body) => + art(path.join(__dirname, 'templates', 'post.art'), { + postImages, + body, + }); + +async function handler(ctx) { + const { username } = ctx.req.param(); + + const account = await showByUsername(username); + const posts = await getPostByAccountId(account.id); + + const items = posts.map((p) => ({ + title: p.body?.replaceAll('\r\n', ' ').trim().split(' ')[0], + description: render(p.post_images, p.body?.replaceAll('\r\n', '<br>')), + pubDate: parseDate(p.published_at), + link: `${baseUrl}/posts/${p.id}`, + author: p.user.name, + })); + + return { + title: `${account.name} (@${account.username})`, + link: `${baseUrl}/${account.username}`, + description: `${account.posts_count} Post ${account.likes_count} Like ${account.followers_count} +Followers ${account.followings_count} Follow ${account.about.replaceAll('\r\n', ' ')}`, + image: account.avatar_url, + icon: account.avatar_url, + logo: account.avatar_url, + language: 'ja-JP', + item: items, + }; +} diff --git a/lib/routes/myfans/templates/post.art b/lib/routes/myfans/templates/post.art new file mode 100644 index 00000000000000..a4a1381694788c --- /dev/null +++ b/lib/routes/myfans/templates/post.art @@ -0,0 +1,8 @@ +{{ if postImages }} + {{ each postImages img }} + <img src="{{ img.file_url }}"><br> + {{ /each }} +{{ /if }} +{{ if body }} + {{@ body }} +{{ /if }} diff --git a/lib/routes/myfans/types.ts b/lib/routes/myfans/types.ts new file mode 100644 index 00000000000000..82863e5a27970d --- /dev/null +++ b/lib/routes/myfans/types.ts @@ -0,0 +1,104 @@ +export interface UserProfile { + about: string; + active: boolean; + avatar_url: string; + banner_url: string; + id: string | null; + is_following: boolean; + likes_count: number; + name: string; + username: string; + is_official_creator: boolean; + is_official: boolean; + label: null; + back_number_post_images_count: number; + back_number_post_videos_count: number; + cant_receive_message: boolean; + current_back_number_plan: null; + followers_count: number; + followings_count: number; + has_approved_user_identification: boolean; + is_bought_back_number: boolean; + is_followed: boolean; + is_subscribed: boolean; + limited_posts_count: number; + link_instagram_id: string; + link_instagram_url: null; + link_tiktok_id: string; + link_tiktok_url: null; + link_twitter_id: string; + link_twitter_url: string; + link_youtube_url: string; + post_images_count: number; + post_videos_count: number; + posts_count: number; + sns_link1: string; + sns_link2: string; +} + +export interface Post { + id: string; + kind: string; + status: string; + status_label: null; + body: string | null; + bookmarked: boolean; + humanized_publish_start_at: string; + deleted_at_i18n: null; + liked: boolean; + likes_count: number; + user: UserProfile; + post_images: { + file_url: string; + square_thumbnail_url: string; + raw_image_height: number; + raw_image_width: number; + }[]; + visible: boolean; + publish_end_at: null; + published_at: string; + publish_start_at: string | null; + pinned_at: string | null; + attachment: null; + plan: null; + current_single_plan: { + id: string; + amount: number; + auto_message_body: string; + } | null; + plans: { + id: string; + product_name: string; + monthly_price: number; + status: null; + is_limited_access: boolean; + disallow_new_subscriber: boolean; + active_discount: { + id: string; + discount_rate: number; + start_at: string | null; + end_at: string | null; + limited_number: null; + status: string; + } | null; + }[]; + video_processing: boolean | null; + video_duration: { + hours: string | null; + minutes: string; + seconds: string; + } | null; + free: boolean; + limited: boolean; + available: boolean; + metadata: { + video: { + duration: number; + resolutions: string[]; + }; + image: { + count: number; + }; + }; + thumbnail_url: string; +} diff --git a/lib/routes/myfans/utils.ts b/lib/routes/myfans/utils.ts new file mode 100644 index 00000000000000..df49d22a501a5a --- /dev/null +++ b/lib/routes/myfans/utils.ts @@ -0,0 +1,39 @@ +import ofetch from '@/utils/ofetch'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import cache from '@/utils/cache'; +import { Post, UserProfile } from './types'; + +const apiBaseUrl = 'https://api.myfans.jp'; +export const baseUrl = 'https://myfans.jp'; + +const headers = { + 'google-ga-data': 'event328', +}; + +export const showByUsername = (username: string) => + cache.tryGet(`myfans:account:${username}`, async () => { + const accountInfo = await ofetch<UserProfile>(`${apiBaseUrl}/api/v2/users/show_by_username`, { + headers, + query: { + username, + }, + }); + + if (!accountInfo.id) { + throw new InvalidParameterError('This creator does not exist.'); + } + + return accountInfo; + }) as Promise<UserProfile>; + +export const getPostByAccountId = async (accountId) => { + const post = await ofetch(`${apiBaseUrl}/api/v2/users/${accountId}/posts`, { + headers, + query: { + sort_key: 'publish_start_at', + page: 1, + }, + }); + + return post.data as Promise<Post[]>; +}; diff --git a/lib/routes/myfigurecollection/activity.ts b/lib/routes/myfigurecollection/activity.ts index 8bd33065f062df..6ccad02fac88e0 100644 --- a/lib/routes/myfigurecollection/activity.ts +++ b/lib/routes/myfigurecollection/activity.ts @@ -43,28 +43,28 @@ export const route: Route = { url: 'zh.myfigurecollection.net/browse', description: `Category - | Figures | Goods | Media | - | ------- | ----- | ----- | - | 0 | 1 | 2 | +| Figures | Goods | Media | +| ------- | ----- | ----- | +| 0 | 1 | 2 | Language - | Id | Language | - | -- | ---------- | - | | en | - | de | Deutsch | - | es | Español | - | fi | Suomeksi | - | fr | Français | - | it | Italiano | - | ja | 日本語 | - | nl | Nederlands | - | no | Norsk | - | pl | Polski | - | pt | Português | - | ru | Русский | - | sv | Svenska | - | zh | 中文 |`, +| Id | Language | +| -- | ---------- | +| | en | +| de | Deutsch | +| es | Español | +| fi | Suomeksi | +| fr | Français | +| it | Italiano | +| ja | 日本語 | +| nl | Nederlands | +| no | Norsk | +| pl | Polski | +| pt | Português | +| ru | Русский | +| sv | Svenska | +| zh | 中文 |`, }; async function handler(ctx) { diff --git a/lib/routes/myfigurecollection/index.ts b/lib/routes/myfigurecollection/index.ts index d0201640397719..5abdeaad52e11f 100644 --- a/lib/routes/myfigurecollection/index.ts +++ b/lib/routes/myfigurecollection/index.ts @@ -39,8 +39,8 @@ export const route: Route = { handler, url: 'zh.myfigurecollection.net/browse', description: `| 每日圖片 | 每週圖片 | 每月圖片 | - | -------- | -------- | -------- | - | potd | potw | potm |`, +| -------- | -------- | -------- | +| potd | potw | potm |`, }; async function handler(ctx) { diff --git a/lib/routes/myfigurecollection/namespace.ts b/lib/routes/myfigurecollection/namespace.ts index 9122016b8b98fb..6a16a739740cfd 100644 --- a/lib/routes/myfigurecollection/namespace.ts +++ b/lib/routes/myfigurecollection/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MyFigureCollection', url: 'myfigurecollection.net', + lang: 'en', }; diff --git a/lib/routes/mygopen/index.ts b/lib/routes/mygopen/index.ts index 8271f187b53057..7a45b7eacf11af 100644 --- a/lib/routes/mygopen/index.ts +++ b/lib/routes/mygopen/index.ts @@ -4,7 +4,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/:label?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/mygopen', parameters: { label: '分類,见下表,默认为首页' }, features: { @@ -24,7 +24,7 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 謠言 | 詐騙 | 真實資訊 | 教學 | - | ---- | ---- | -------- | ---- |`, +| ---- | ---- | -------- | ---- |`, }; async function handler(ctx) { diff --git a/lib/routes/mygopen/namespace.ts b/lib/routes/mygopen/namespace.ts index a88837a4022ecb..9340f203edca92 100644 --- a/lib/routes/mygopen/namespace.ts +++ b/lib/routes/mygopen/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MyGoPen', url: 'mygopen.com', + lang: 'zh-TW', }; diff --git a/lib/routes/mymusicsheet/namespace.ts b/lib/routes/mymusicsheet/namespace.ts index a08efb2df81335..199f03204ae04c 100644 --- a/lib/routes/mymusicsheet/namespace.ts +++ b/lib/routes/mymusicsheet/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MyMusicSheet', url: 'mymusicsheet.com', + lang: 'en', }; diff --git a/lib/routes/mysql/namespace.ts b/lib/routes/mysql/namespace.ts index 42e8810c4072aa..3af572b3bddb37 100644 --- a/lib/routes/mysql/namespace.ts +++ b/lib/routes/mysql/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'MySQL', url: 'dev.mysql.com', + lang: 'en', }; diff --git a/lib/routes/mysql/release.ts b/lib/routes/mysql/release.ts index b865c133f8906c..29bb810306b338 100644 --- a/lib/routes/mysql/release.ts +++ b/lib/routes/mysql/release.ts @@ -21,7 +21,7 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 8.0 | 5.7 | 5.6 | - | --- | --- | --- |`, +| --- | --- | --- |`, }; async function handler(ctx) { diff --git a/lib/routes/nasa/apod-cn.ts b/lib/routes/nasa/apod-cn.ts index 3d541d40385b8d..508168591180d5 100644 --- a/lib/routes/nasa/apod-cn.ts +++ b/lib/routes/nasa/apod-cn.ts @@ -24,9 +24,9 @@ export const route: Route = { maintainers: ['nczitzk', 'williamgateszhao'], handler, url: 'apod.nasa.govundefined', - description: `:::tip + description: `::: tip [NASA 中文](https://www.nasachina.cn/) 提供了每日天文图的中英双语图文说明,但在更新上偶尔略有一两天的延迟。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/nasa/apod.ts b/lib/routes/nasa/apod.ts index 391008c5f248fb..7f037fdcf44bec 100644 --- a/lib/routes/nasa/apod.ts +++ b/lib/routes/nasa/apod.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'; @@ -7,7 +7,8 @@ import timezone from '@/utils/timezone'; export const route: Route = { path: '/apod', - categories: ['picture'], + categories: ['picture', 'popular'], + view: ViewType.Pictures, example: '/nasa/apod', parameters: {}, features: { @@ -23,7 +24,7 @@ export const route: Route = { source: ['apod.nasa.govundefined'], }, ], - name: 'NASA', + name: 'Astronomy Picture of the Day', maintainers: ['nczitzk', 'williamgateszhao'], handler, url: 'apod.nasa.govundefined', diff --git a/lib/routes/nasa/namespace.ts b/lib/routes/nasa/namespace.ts index d53c8c1930b826..c1f062a2cabd65 100644 --- a/lib/routes/nasa/namespace.ts +++ b/lib/routes/nasa/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'NASA Astronomy Picture of the Day', + name: 'NASA', url: 'apod.nasa.gov', + lang: 'en', }; diff --git a/lib/routes/natgeo/dailyphoto.ts b/lib/routes/natgeo/dailyphoto.ts index cca15714b29675..88f2c5792d7b54 100644 --- a/lib/routes/natgeo/dailyphoto.ts +++ b/lib/routes/natgeo/dailyphoto.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,6 +13,7 @@ import { config } from '@/config'; export const route: Route = { path: '/dailyphoto', categories: ['picture'], + view: ViewType.Pictures, example: '/natgeo/dailyphoto', parameters: {}, features: { @@ -28,8 +29,8 @@ export const route: Route = { source: ['nationalgeographic.com/photo-of-the-day/*', 'nationalgeographic.com/'], }, ], - name: '每日一图', - maintainers: ['LogicJake', 'OrangeEd1t', 'TonyRL'], + name: 'Daily Photo', + maintainers: ['LogicJake', 'OrangeEd1t', 'TonyRL', 'pseudoyu'], handler, url: 'nationalgeographic.com/photo-of-the-day/*', }; diff --git a/lib/routes/natgeo/dailyselection.ts b/lib/routes/natgeo/dailyselection.ts index d7c0e89ff36a9a..49977d845b2abd 100644 --- a/lib/routes/natgeo/dailyselection.ts +++ b/lib/routes/natgeo/dailyselection.ts @@ -1,11 +1,26 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/dailyselection', - name: 'Unknown', + name: 'Daily Selection', + categories: ['picture'], + view: ViewType.Pictures, + example: '/natgeo/dailyselection', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + }, + radar: [ + { + source: ['nationalgeographic.com/'], + }, + ], maintainers: ['OrangeEd1t'], handler, }; diff --git a/lib/routes/natgeo/namespace.ts b/lib/routes/natgeo/namespace.ts index f7ab784082b324..613b82e745b1aa 100644 --- a/lib/routes/natgeo/namespace.ts +++ b/lib/routes/natgeo/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: '国家地理', + name: 'National Geographic', url: 'nationalgeographic.com', + lang: 'en', }; diff --git a/lib/routes/natgeo/natgeo.ts b/lib/routes/natgeo/natgeo.ts index 1960088d113684..72bcb226ac8ccd 100644 --- a/lib/routes/natgeo/natgeo.ts +++ b/lib/routes/natgeo/natgeo.ts @@ -1,29 +1,36 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import { load } from 'cheerio'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; -// https://www.natgeomedia.com//article/ +// https://www.natgeomedia.com/article/ async function loadContent(link) { - const { data } = await got(link); + const data = await ofetch(link); const $ = load(data); const dtStr = $('.content-title-area') .find('h6') .first() - .html() + .text() .replaceAll(/ /gi, ' ') .trim(); - let description = $('article').first().html() + $('article').eq(1).html(); - if (/photo/.test(link)) { + $('.splide__arrows, .slide-control').remove(); + + let description = ($('article').eq(0).html() ?? '') + ($('article').eq(1).html() ?? ''); + if (/photo|gallery/.test(link)) { description = $('#content-album').html() + description; } return { title: $('title').text(), - pubDate: parseDate(dtStr, 'MMM. DD YYYY'), + pubDate: parseDate(dtStr), description, + category: $('.content-tag a') + .toArray() + .map((i) => $(i).text()), + link, + image: $('link[rel="image_src"]').attr('href'), }; } @@ -42,8 +49,8 @@ export const route: Route = { }, radar: [ { - source: ['natgeomedia.com/:cat/:type', 'natgeomedia.com/'], - target: '/:cat/:type', + source: ['natgeomedia.com/:cat/:type', 'natgeomedia.com/:cat/', 'natgeomedia.com/'], + target: '/:cat/:type?', }, ], name: '分类', @@ -54,29 +61,23 @@ export const route: Route = { async function handler(ctx) { const type = ctx.req.param('type') ?? ''; const url = `https://www.natgeomedia.com/${ctx.req.param('cat')}/${type}`; - const res = await got(url); - const $ = load(res.data); + const res = await ofetch(url); + const $ = load(res); - const urlList = $('.article-link-w100') - .find('.read-btn') + const urlList = $('.article-link-content h4') .toArray() .map((i) => ({ link: $(i).find('a[href]').first().attr('href'), - })); + })) + .filter((i) => i.link); + + const out = await Promise.all(urlList.map((i) => cache.tryGet(i.link!, () => loadContent(i.link)))); - const out = await Promise.all( - urlList.map(async (i) => { - const link = i.link; - const single = { - link, - }; - const other = await cache.tryGet(link, () => loadContent(link)); - return { ...single, ...other }; - }) - ); return { title: $('title').text(), + description: $('meta[name="description"]').attr('content'), link: url, + image: 'https://www.natgeomedia.com/img/app_icon.png', item: out, }; } diff --git a/lib/routes/nationalgeographic/latest-stories.ts b/lib/routes/nationalgeographic/latest-stories.ts index 77608ebd338e81..b87a60e0156b6c 100644 --- a/lib/routes/nationalgeographic/latest-stories.ts +++ b/lib/routes/nationalgeographic/latest-stories.ts @@ -58,7 +58,7 @@ async function handler() { cache.tryGet(item.link, async () => { const response = await got(item.link); const $ = load(response.data); - const mods = findNatgeo($).page.content.article.frms.find((f) => f.cmsType === 'ArticleBodyFrame').mods; + const mods = findNatgeo($).page.content.prismarticle.frms.find((f) => f.cmsType === 'ArticleBodyFrame').mods; const bodyTile = mods.find((m) => m.edgs[0].cmsType === 'ArticleBodyTile').edgs[0]; item.author = bodyTile.cntrbGrp diff --git a/lib/routes/nationalgeographic/namespace.ts b/lib/routes/nationalgeographic/namespace.ts index 6b8cb85a79b607..942b46eea30e4e 100644 --- a/lib/routes/nationalgeographic/namespace.ts +++ b/lib/routes/nationalgeographic/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'National Geographic', url: 'www.nationalgeographic.com', + lang: 'en', }; diff --git a/lib/routes/nationalgeographic/templates/stories.art b/lib/routes/nationalgeographic/templates/stories.art index 6a6d842a8c90a0..039e7304711b5f 100644 --- a/lib/routes/nationalgeographic/templates/stories.art +++ b/lib/routes/nationalgeographic/templates/stories.art @@ -13,7 +13,7 @@ {{ if b.type === 'p' }} <p>{{@ b.cntnt.mrkup }}</p> {{ else if b.type === 'inline' }} - {{ if b.cntnt.cmsType === 'image' }} + {{ if b.cntnt.cmsType === 'image' && b.cntnt.image?.src }} <figure> <img src="{{ b.cntnt.image.src }}" alt="{{ b.cntnt.image.altText }}"> <figcaption>{{@ b.cntnt.caption }}</figcaption> diff --git a/lib/routes/nature/cover.ts b/lib/routes/nature/cover.ts index 5263dc21745df4..cb6458f49d6a57 100644 --- a/lib/routes/nature/cover.ts +++ b/lib/routes/nature/cover.ts @@ -41,7 +41,7 @@ export const route: Route = { }, ], name: 'Cover Story', - maintainers: ['y9c'], + maintainers: ['y9c', 'pseudoyu'], handler, url: 'nature.com/', description: `Subscribe to the cover images of the Nature journals, and get the latest publication updates in time.`, @@ -65,13 +65,15 @@ async function handler() { const { id, name, link } = journal; const response = await got(link, { cookieJar }); + const $ = load(response.data); + const ogUrl = $('meta[property="og:url"]').attr('content'); const capturingRegex = /volumes\/(?<volume>\d+)\/issues\/(?<issue>\d+)/; - const { volume, issue } = response.url.match(capturingRegex).groups; + const { volume, issue } = ogUrl?.match(capturingRegex)?.groups ?? {}; + const imageUrl = `https://media.springernature.com/full/springer-static/cover-hires/journal/${id}/${volume}/${issue}?as=webp`; const contents = `<div align="center"><img src="${imageUrl}" alt="Volume ${volume} Issue ${issue}"></div>`; - const $ = load(response.data); const date = $('title').text().split(',')[1].trim(); const issueDescription = $('div[data-test=issue-description]').html() ?? ''; diff --git a/lib/routes/nature/highlight.ts b/lib/routes/nature/highlight.ts index 4809bc0b511857..fe2b843ed7b0e2 100644 --- a/lib/routes/nature/highlight.ts +++ b/lib/routes/nature/highlight.ts @@ -25,9 +25,9 @@ export const route: Route = { name: 'Research Highlight', maintainers: [], handler, - description: `:::warning + description: `::: warning Only some journals are supported. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/nature/namespace.ts b/lib/routes/nature/namespace.ts index 1ac08accd3242a..d9a1465d12a1a0 100644 --- a/lib/routes/nature/namespace.ts +++ b/lib/routes/nature/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Nature Journal', url: 'nature.com', - description: `:::tip + description: `::: tip You can get all short name of a journal from [https://www.nature.com/siteindex](https://www.nature.com/siteindex) or [Journal List](#nature-journal-journal-list). :::`, + lang: 'en', }; diff --git a/lib/routes/nature/research.ts b/lib/routes/nature/research.ts index d72b0fd73c5afa..9a7b05fe9c81ed 100644 --- a/lib/routes/nature/research.ts +++ b/lib/routes/nature/research.ts @@ -38,19 +38,19 @@ export const route: Route = { }, ], name: 'Latest Research', - maintainers: ['y9c', 'TonyRL'], + maintainers: ['y9c', 'TonyRL', 'pseudoyu'], handler, description: `| \`:journal\` | Full Name of the Journal | Route | - | :-----------: | :-------------------------: | ---------------------------------------------------------------------------------- | - | nature | Nature | [/nature/research/nature](https://rsshub.app/nature/research/nature) | - | nbt | Nature Biotechnology | [/nature/research/nbt](https://rsshub.app/nature/research/nbt) | - | neuro | Nature Neuroscience | [/nature/research/neuro](https://rsshub.app/nature/research/neuro) | - | ng | Nature Genetics | [/nature/research/ng](https://rsshub.app/nature/research/ng) | - | ni | Nature Immunology | [/nature/research/ni](https://rsshub.app/nature/research/ni) | - | nmeth | Nature Method | [/nature/research/nmeth](https://rsshub.app/nature/research/nmeth) | - | nchem | Nature Chemistry | [/nature/research/nchem](https://rsshub.app/nature/research/nchem) | - | nmat | Nature Materials | [/nature/research/nmat](https://rsshub.app/nature/research/nmat) | - | natmachintell | Nature Machine Intelligence | [/nature/research/natmachintell](https://rsshub.app/nature/research/natmachintell) | +| :-----------: | :-------------------------: | ---------------------------------------------------------------------------------- | +| nature | Nature | [/nature/research/nature](https://rsshub.app/nature/research/nature) | +| nbt | Nature Biotechnology | [/nature/research/nbt](https://rsshub.app/nature/research/nbt) | +| neuro | Nature Neuroscience | [/nature/research/neuro](https://rsshub.app/nature/research/neuro) | +| ng | Nature Genetics | [/nature/research/ng](https://rsshub.app/nature/research/ng) | +| ni | Nature Immunology | [/nature/research/ni](https://rsshub.app/nature/research/ni) | +| nmeth | Nature Method | [/nature/research/nmeth](https://rsshub.app/nature/research/nmeth) | +| nchem | Nature Chemistry | [/nature/research/nchem](https://rsshub.app/nature/research/nchem) | +| nmat | Nature Materials | [/nature/research/nmat](https://rsshub.app/nature/research/nmat) | +| natmachintell | Nature Machine Intelligence | [/nature/research/natmachintell](https://rsshub.app/nature/research/natmachintell) | - Using router (\`/nature/research/\` + "short name for a journal") to query latest research paper for a certain journal of Nature Publishing Group. If the \`:journal\` parameter is blank, then latest research of Nature will return. diff --git a/lib/routes/nature/siteindex.ts b/lib/routes/nature/siteindex.ts index 99ec1d80957bf5..d927800dd87d7e 100644 --- a/lib/routes/nature/siteindex.ts +++ b/lib/routes/nature/siteindex.ts @@ -18,7 +18,7 @@ export const route: Route = { supportScihub: false, }, name: 'Journal List', - maintainers: ['TonyRL'], + maintainers: ['TonyRL', 'pseudoyu'], handler, }; @@ -40,19 +40,33 @@ async function handler(ctx) { items = await Promise.all( items.map((item) => cache.tryGet(`nature:siteindex:${item.title}`, async () => { - const response = await got(item.link, { cookieJar }); - const $ = load(response.data); - - delete item.link; try { - item.id = $('.app-latest-issue-row__image img') - .attr('src') - .match(/.*\/journal\/(\d{5})/)[1]; - item.description = item.id; + const response = await got(item.link, { cookieJar }); + const $ = load(response.data); + + const imgSrc = $('.app-latest-issue-row__image img').attr('src'); + if (imgSrc) { + const match = imgSrc.match(/.*\/journal\/(\d{5})/); + if (match) { + const id = match[1]; + return { + title: item.title, + name: item.name, + id, + description: id, + }; + } + } + return { + title: item.title, + name: item.name, + }; } catch { - // + return { + title: item.title, + name: item.name, + }; } - return item; }) ) ); diff --git a/lib/routes/nature/utils.ts b/lib/routes/nature/utils.ts index b32d7844a793ae..cf77094328723c 100644 --- a/lib/routes/nature/utils.ts +++ b/lib/routes/nature/utils.ts @@ -1,5 +1,5 @@ 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'; import { CookieJar } from 'tough-cookie'; @@ -52,13 +52,11 @@ const getArticleList = (html) => const getArticle = (item) => cache.tryGet(item.link, async () => { - const response = await got(item.link, { - cookieJar, - }); - const $ = load(response.data); - const responseUrl = new URL(response.url); + const response = await ofetch(item.link); + + const $ = load(response); - if (responseUrl.pathname.startsWith('/immersive/')) { + if (new URL(item.link).pathname.startsWith('/immersive/')) { const meta = getDataLayer($); item.doi = meta.content.article?.doi; item.author = meta.content.contentInfo.authors.join(', '); diff --git a/lib/routes/nautil/namespace.ts b/lib/routes/nautil/namespace.ts index 877f9173ca6795..4ef07654411813 100644 --- a/lib/routes/nautil/namespace.ts +++ b/lib/routes/nautil/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Nautilus', url: 'nautil.us', + lang: 'en', }; diff --git a/lib/routes/nautil/topics.ts b/lib/routes/nautil/topics.ts index 116ee9c1add915..82cecd704ef8ae 100644 --- a/lib/routes/nautil/topics.ts +++ b/lib/routes/nautil/topics.ts @@ -12,7 +12,7 @@ const baseUrl = 'https://nautil.us'; export const route: Route = { path: '/topic/:tid', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/nautil/topic/arts', parameters: { tid: 'topic' }, features: { diff --git a/lib/routes/nautiljon/manga-releases.ts b/lib/routes/nautiljon/manga-releases.ts new file mode 100644 index 00000000000000..7eb6974ef38976 --- /dev/null +++ b/lib/routes/nautiljon/manga-releases.ts @@ -0,0 +1,72 @@ +import { load } from 'cheerio'; +import { config } from '@/config'; +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +const host = 'https://www.nautiljon.com'; +export const route: Route = { + path: '/releases/manga', + categories: ['reading'], + example: '/nautiljon/releases/manga', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['nautiljon.com/'], + }, + ], + name: 'France manga releases', + maintainers: ['Fafnor'], + handler, + url: 'nautiljon.com', +}; + +const isVolumeReleased = (releaseDate: string) => { + const releaseDateToCheck = parseDate(releaseDate, 'DD/MM/YYYY'); + const todayDate = new Date(); + todayDate.setHours(0, 0, 0, 0); + + return releaseDateToCheck <= todayDate; +}; + +async function handler() { + const response = await ofetch(`${host}/planning/manga/`, { + headers: { + 'User-Agent': config.trueUA, + }, + }); + + const $ = load(response); + const list = $('table#planning tbody tr') + .toArray() + .filter((item) => isVolumeReleased($(item).find('td').first().text())); + const items = list.map((item) => { + item = $(item); + const releaseDate = item.find('td').first().text(); + + const a = item.find('td.p_titre').find('a.sim').first(); + const img = item.find('td:nth-child(2) a').first(); + + return { + title: a.text(), + link: `${host}${a.attr('href')}`, + pubDate: parseDate(releaseDate, 'DD/MM/YYYY'), + image: `${host}${img.attr('im')}`, + category: item.find('td.p_titre div.fl').first().text(), + }; + }); + + return { + title: 'Nautiljon France Manga Releases', + link: `${host}/planning/manga/`, + item: items, + }; +} diff --git a/lib/routes/nautiljon/namespace.ts b/lib/routes/nautiljon/namespace.ts new file mode 100644 index 00000000000000..596a86a6247389 --- /dev/null +++ b/lib/routes/nautiljon/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Nautiljon', + url: 'nautiljon.com', + lang: 'fr', +}; diff --git a/lib/routes/nbd/daily.ts b/lib/routes/nbd/daily.ts index a05b1060734c58..49c554967a6caa 100644 --- a/lib/routes/nbd/daily.ts +++ b/lib/routes/nbd/daily.ts @@ -24,5 +24,5 @@ export const route: Route = { }; function handler(ctx) { - ctx.redirect('/nbd/332'); + ctx.set('redirect', '/nbd/332'); } diff --git a/lib/routes/nbd/index.ts b/lib/routes/nbd/index.ts index 843fbff483b7b7..854ce8c4daa5eb 100644 --- a/lib/routes/nbd/index.ts +++ b/lib/routes/nbd/index.ts @@ -28,8 +28,8 @@ export const route: Route = { handler, url: 'nbd.com.cn/', description: `| 头条 | 要闻 | 图片新闻 | 推荐 | - | ---- | ---- | -------- | ---- | - | 2 | 3 | 4 | 5 |`, +| ---- | ---- | -------- | ---- | +| 2 | 3 | 4 | 5 |`, }; async function handler(ctx) { diff --git a/lib/routes/nbd/namespace.ts b/lib/routes/nbd/namespace.ts index 3856375eca18f8..8032771edd93b1 100644 --- a/lib/routes/nbd/namespace.ts +++ b/lib/routes/nbd/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '每经网', url: 'nbd.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nber/all.ts b/lib/routes/nber/all.ts new file mode 100644 index 00000000000000..93c438691e6fb2 --- /dev/null +++ b/lib/routes/nber/all.ts @@ -0,0 +1,19 @@ +import { Route } from '@/types'; +import { handler } from './common'; + +export const route: Route = { + name: 'All Papers', + maintainers: ['5upernova-heng'], + path: '/papers', + example: '/nber/papers', + features: { + supportScihub: true, + }, + radar: [ + { + source: ['nber.org/papers'], + }, + ], + handler, + url: 'nber.org/papers', +}; diff --git a/lib/routes/nber/index.ts b/lib/routes/nber/common.ts similarity index 69% rename from lib/routes/nber/index.ts rename to lib/routes/nber/common.ts index bcd5bfc74b9a66..fcf5c558c679e5 100644 --- a/lib/routes/nber/index.ts +++ b/lib/routes/nber/common.ts @@ -1,10 +1,9 @@ -import { Route } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); import { getSubPath } from '@/utils/common-utils'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import path from 'node:path'; import { art } from '@/utils/render'; @@ -12,37 +11,10 @@ import { parseDate } from '@/utils/parse-date'; import { config } from '@/config'; async function getData(url) { - const response = await got(url).json(); + const response = await ofetch(url); return response.results; } - -export const route: Route = { - path: ['/papers', '/news'], - categories: ['journal'], - example: '/nber/papers', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: true, - }, - radar: [ - { - source: ['nber.org/papers'], - }, - ], - name: 'All Papers', - maintainers: [], - handler, - url: 'nber.org/papers', - description: `Papers that are published in this week.`, - url: 'nber.org/papers', -}; - -async function handler(ctx) { +export async function handler(ctx) { const url = 'https://www.nber.org/api/v1/working_page_listing/contentType/working_paper/_/_/search'; const baseUrl = 'https://www.nber.org'; const data = await cache.tryGet(url, () => getData(url), config.cache.routeExpire, false); @@ -52,8 +24,8 @@ async function handler(ctx) { .map((article) => { const link = `${baseUrl}${article.url}`; return cache.tryGet(link, async () => { - const response = await got(link); - const $ = load(response.data); + const response = await ofetch(link); + const $ = load(response); const downloadLink = $('meta[name="citation_pdf_url"]').attr('content'); const fullAbstract = $('.page-header__intro-inner').html(); return { @@ -76,6 +48,5 @@ async function handler(ctx) { link: 'https://www.nber.org/papers', item: items, description: `National Bureau of Economic Research Working Papers articles`, - language: $('html').attr('lang'), }; } diff --git a/lib/routes/nber/namespace.ts b/lib/routes/nber/namespace.ts index 8de1aeb513b851..f0d1350fc57e36 100644 --- a/lib/routes/nber/namespace.ts +++ b/lib/routes/nber/namespace.ts @@ -3,4 +3,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'National Bureau of Economic Research', url: 'nber.org', + categories: ['journal'], + lang: 'en', }; diff --git a/lib/routes/nber/new.ts b/lib/routes/nber/new.ts new file mode 100644 index 00000000000000..0fe6d061b5a0a4 --- /dev/null +++ b/lib/routes/nber/new.ts @@ -0,0 +1,20 @@ +import { Route } from '@/types'; +import { handler } from './common'; + +export const route: Route = { + name: 'New Papers', + maintainers: ['5upernova-heng'], + path: '/new', + example: '/nber/new', + features: { + supportScihub: true, + }, + radar: [ + { + source: ['nber.org/papers'], + }, + ], + handler, + url: 'nber.org/papers', + description: 'Papers that are published in this week.', +}; diff --git a/lib/routes/ncc-cma/cmdp.ts b/lib/routes/ncc-cma/cmdp.ts index 62a14b523e4f07..9c0571a7d85e17 100644 --- a/lib/routes/ncc-cma/cmdp.ts +++ b/lib/routes/ncc-cma/cmdp.ts @@ -9,16 +9,9 @@ import { art } from '@/utils/render'; import path from 'node:path'; import iconv from 'iconv-lite'; -export const route: Route = { - path: '/cmdp/image/:id{.+}?', - name: 'Unknown', - maintainers: [], - handler, -}; - -async function handler(ctx) { - const id = ctx.req.param('id'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 50; +export const handler = async (ctx) => { + const { id = 'RPJQWQYZ' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; const ids = id?.split(/\//) ?? []; const titles = []; @@ -33,6 +26,7 @@ async function handler(ctx) { const $ = load(iconv.decode(response, 'gbk')); const author = '国家气候中心'; + const language = 'zh'; const items = $('ul.img-con-new-con li img[id]') .toArray() @@ -43,9 +37,9 @@ async function handler(ctx) { const id = item.prop('id'); const title = $(`li[data-id="${id}"]`).text() || undefined; - const src = new URL(item.prop('src'), currentUrl).href; + const image = new URL(item.prop('src'), currentUrl).href; const date = - src + image .match(/_(\d{4})(\d{2})(\d{2})_/) ?.slice(1, 4) .join('-') ?? new Date().toISOString().slice(0, 10); @@ -54,39 +48,302 @@ async function handler(ctx) { titles.push(title); } + const description = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: `${title} ${date}`, + }, + ] + : undefined, + }); + const guid = `ncc-cma#${id}#${date}`; + return { title: `${title} ${date}`, + description, + pubDate: parseDate(date), link: currentUrl, - description: art(path.join(__dirname, 'templates/description.art'), { - image: { - src, - alt: `${title} ${date}`, - }, - }), - author, category: [title], - guid: `ncc-cma#${id}#${date}`, - pubDate: parseDate(date), - enclosure_url: src, - enclosure_type: `image/${src.split(/\./).pop()}`, + author, + guid, + id: guid, + content: { + html: description, + text: description, + }, + image, + banner: image, + language, + enclosure_url: image, + enclosure_type: `image/${image.split(/\./).pop()}`, + enclosure_title: `${title} ${date}`, }; }); const subtitle = $('h1').last().text(); const image = $('img.logo').prop('src'); - const icon = new URL('favicon.ico', rootUrl).href; return { - item: items, title: `${author} - ${subtitle}${titles.length === 0 ? '' : ` - ${titles.join('|')}`}`, - link: currentUrl, description: $('title').text(), - language: 'zh', + link: currentUrl, + item: items, + allowEmpty: true, image, - icon, - logo: icon, - subtitle, author, - allowEmpty: true, + language, }; -} +}; + +export const route: Route = { + path: '/cmdp/image/:id{.+}?', + name: '最新监测', + url: 'cmdp.ncc-cma.net', + maintainers: ['nczitzk'], + handler, + example: '/ncc-cma/cmdp/image/RPJQWQYZ', + parameters: { category: '图片,默认为 RPJQWQYZ,即日平均气温距平,可在对应列表项 data-id 属性中找到' }, + description: `::: tip + 若订阅日平均气温距平,将其 data-id \`RPJQWQYZ\` 作为参数填入,此时路由为 [\`/ncc-cma/cmdp/image/RPJQWQYZ\`](https://rsshub.app/ncc-cma/cmdp/image/RPJQWQYZ)。 + + 若同时订阅日平均气温距平、近5天平均气温距和近10天平均气温距平,将其 data-id \`RPJQWQYZ\`、\`ZJ5TPJQWJP\` 和 \`ZJ10TQWJP\` 作为参数填入,此时路由为 [\`/ncc-cma/cmdp/image/RPJQWQYZ/ZJ5TPJQWJP/ZJ10TQWJP\`](https://rsshub.app/ncc-cma/cmdp/image/RPJQWQYZ/ZJ5TPJQWJP/ZJ10TQWJP)。 +::: + +| 日平均气温距平 | 近5天平均气温距平 | 近10天平均气温距平 | 近20天平均气温距平 | 近30天平均气温距平 | +| ----------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | +| [RPJQWQYZ](https://rsshub.app/ncc-cma/cmdp/image/RPJQWQYZ) | [ZJ5TPJQWJP](https://rsshub.app/ncc-cma/cmdp/image/ZJ5TPJQWJP) | [ZJ10TQWJP](https://rsshub.app/ncc-cma/cmdp/image/ZJ10TQWJP) | [ZJ20TQWJP](https://rsshub.app/ncc-cma/cmdp/image/ZJ20TQWJP) | [ZJ30TQWJP](https://rsshub.app/ncc-cma/cmdp/image/ZJ30TQWJP) | + +| 本月以来气温距平 | 本季以来气温距平 | 本年以来气温距平 | +| ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- | +| [BYYLQWJP](https://rsshub.app/ncc-cma/cmdp/image/BYYLQWJP) | [BJYLQWJP](https://rsshub.app/ncc-cma/cmdp/image/BJYLQWJP) | [BNYLQWJP](https://rsshub.app/ncc-cma/cmdp/image/BNYLQWJP) | + +| 日降水量分布 | 近5天降水量 | 近10天降水量 | 近20天降水量 | 近30天降水量 | +| ----------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- | +| [QGRJSLFBT0808S](https://rsshub.app/ncc-cma/cmdp/image/QGRJSLFBT0808S) | [ZJ5TJSLFBT](https://rsshub.app/ncc-cma/cmdp/image/ZJ5TJSLFBT) | [ZJ10TJSL](https://rsshub.app/ncc-cma/cmdp/image/ZJ10TJSL) | [ZJ20TJSL](https://rsshub.app/ncc-cma/cmdp/image/ZJ20TJSL) | [ZJ30TJSL](https://rsshub.app/ncc-cma/cmdp/image/ZJ30TJSL) | + +| 本月以来降水量 | 本季以来降水量 | 近10天降水量距平百分率 | 近20天降水量距平百分率 | 近30天降水量距平百分率 | +| --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | +| [BYYLJSL](https://rsshub.app/ncc-cma/cmdp/image/BYYLJSL) | [BJYLJSL](https://rsshub.app/ncc-cma/cmdp/image/BJYLJSL) | [ZJ10TJSLJP](https://rsshub.app/ncc-cma/cmdp/image/ZJ10TJSLJP) | [ZJ20TJSLJP](https://rsshub.app/ncc-cma/cmdp/image/ZJ20TJSLJP) | [ZJ30TJSLJP](https://rsshub.app/ncc-cma/cmdp/image/ZJ30TJSLJP) | + +| 本月以来降水量距平百分率 | 本季以来降水量距平百分率 | 本年以来降水量距平百分率 | +| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------- | +| [BYYLJSLJPZYQHZ](https://rsshub.app/ncc-cma/cmdp/image/BYYLJSLJPZYQHZ) | [BJYLJSLJPZJQHZ](https://rsshub.app/ncc-cma/cmdp/image/BJYLJSLJPZJQHZ) | [BNYLJSLJP](https://rsshub.app/ncc-cma/cmdp/image/BNYLJSLJP) | + +| 气温距平(最近10天) | 气温距平(最近20天) | 气温距平(最近30天) | 气温距平(最近90天) | 最低气温距平(最近30天) | +| -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [glbtmeana10\_](https://rsshub.app/ncc-cma/cmdp/image/glbtmeana10_) | [glbtmeana20\_](https://rsshub.app/ncc-cma/cmdp/image/glbtmeana20_) | [glbtmeana30\_](https://rsshub.app/ncc-cma/cmdp/image/glbtmeana30_) | [glbtmeana90\_](https://rsshub.app/ncc-cma/cmdp/image/glbtmeana90_) | [glbtmina30\_](https://rsshub.app/ncc-cma/cmdp/image/glbtmina30_) | + +| 最低气温距平(最近90天) | 最高气温距平(最近30天) | 最高气温距平(最近90天) | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| [glbtmina90\_](https://rsshub.app/ncc-cma/cmdp/image/glbtmina90_) | [glbtmaxa30\_](https://rsshub.app/ncc-cma/cmdp/image/glbtmaxa30_) | [glbtmaxa90\_](https://rsshub.app/ncc-cma/cmdp/image/glbtmaxa90_) | + +| 降水量(最近10天) | 降水量(最近20天) | 降水量(最近30天) | 降水量(最近90天) | 降水距平百分率(最近10天) | +| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------ | +| [glbrain10\_](https://rsshub.app/ncc-cma/cmdp/image/glbrain10_) | [glbrain20\_](https://rsshub.app/ncc-cma/cmdp/image/glbrain20_) | [glbrain30\_](https://rsshub.app/ncc-cma/cmdp/image/glbrain30_) | [glbrain90\_](https://rsshub.app/ncc-cma/cmdp/image/glbrain90_) | [glbraina10\_](https://rsshub.app/ncc-cma/cmdp/image/glbraina10_) | + +| 降水距平百分率(最近20天) | 降水距平百分率(最近30天) | 降水距平百分率(最近90天) | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| [glbraina20\_](https://rsshub.app/ncc-cma/cmdp/image/glbraina20_) | [glbraina30\_](https://rsshub.app/ncc-cma/cmdp/image/glbraina30_) | [glbraina90\_](https://rsshub.app/ncc-cma/cmdp/image/glbraina90_) | + + `, + categories: ['forecast'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + title: '日平均气温距平', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/RPJQWQYZ', + }, + { + title: '近5天平均气温距平', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ5TPJQWJP', + }, + { + title: '近10天平均气温距平', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ10TQWJP', + }, + { + title: '近20天平均气温距平', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ20TQWJP', + }, + { + title: '近30天平均气温距平', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ30TQWJP', + }, + { + title: '本月以来气温距平', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/BYYLQWJP', + }, + { + title: '本季以来气温距平', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/BJYLQWJP', + }, + { + title: '本年以来气温距平', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/BNYLQWJP', + }, + { + title: '日降水量分布', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/QGRJSLFBT0808S', + }, + { + title: '近5天降水量', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ5TJSLFBT', + }, + { + title: '近10天降水量', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ10TJSL', + }, + { + title: '近20天降水量', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ20TJSL', + }, + { + title: '近30天降水量', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ30TJSL', + }, + { + title: '本月以来降水量', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/ncc-cma/cmdp/image/BYYLJSL', + }, + { + title: '本季以来降水量', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/BJYLJSL', + }, + { + title: '近10天降水量距平百分率', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ10TJSLJP', + }, + { + title: '近20天降水量距平百分率', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ20TJSLJP', + }, + { + title: '近30天降水量距平百分率', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/ZJ30TJSLJP', + }, + { + title: '本月以来降水量距平百分率', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/BYYLJSLJPZYQHZ', + }, + { + title: '本季以来降水量距平百分率', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/BJYLJSLJPZJQHZ', + }, + { + title: '本年以来降水量距平百分率', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/BNYLJSLJP', + }, + { + title: '气温距平(最近10天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbtmeana10_', + }, + { + title: '气温距平(最近20天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbtmeana20_', + }, + { + title: '气温距平(最近30天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbtmeana30_', + }, + { + title: '气温距平(最近90天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbtmeana90_', + }, + { + title: '最低气温距平(最近30天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbtmina30_', + }, + { + title: '最低气温距平(最近90天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbtmina90_', + }, + { + title: '最高气温距平(最近30天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbtmaxa30_', + }, + { + title: '最高气温距平(最近90天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbtmaxa90_', + }, + { + title: '降水量(最近10天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbrain10_', + }, + { + title: '降水量(最近20天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbrain20_', + }, + { + title: '降水量(最近30天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbrain30_', + }, + { + title: '降水量(最近90天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbrain90_', + }, + { + title: '降水距平百分率(最近10天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbraina10_', + }, + { + title: '降水距平百分率(最近20天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbraina20_', + }, + { + title: '降水距平百分率(最近30天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbraina30_', + }, + { + title: '降水距平百分率(最近90天)', + source: ['cmdp.ncc-cma.net/cn/index.htm'], + target: '/cmdp/image/glbraina90_', + }, + ], +}; diff --git a/lib/routes/ncc-cma/namespace.ts b/lib/routes/ncc-cma/namespace.ts index befbc6ab91b8a2..e7640c6483f3d6 100644 --- a/lib/routes/ncc-cma/namespace.ts +++ b/lib/routes/ncc-cma/namespace.ts @@ -3,4 +3,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '国家气候中心', url: 'cmdp.ncc-cma.net', + categories: ['forecast'], + description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/ncepu/master/masterinfo.ts b/lib/routes/ncepu/master/masterinfo.ts index 6d83b9508f66a2..bbc35f9943ac8d 100644 --- a/lib/routes/ncepu/master/masterinfo.ts +++ b/lib/routes/ncepu/master/masterinfo.ts @@ -38,8 +38,8 @@ export const route: Route = { maintainers: ['nilleo'], handler, description: `| 类型 | 硕士招生信息 | 通知公告 | 研究生培养信息 | - | ---- | ------------ | -------- | -------------- | - | 参数 | zsxx | tzgg | pyxx |`, +| ---- | ------------ | -------- | -------------- | +| 参数 | zsxx | tzgg | pyxx |`, }; async function handler(ctx) { diff --git a/lib/routes/ncepu/namespace.ts b/lib/routes/ncepu/namespace.ts index 19f34a79229acc..dab7ccab469828 100644 --- a/lib/routes/ncepu/namespace.ts +++ b/lib/routes/ncepu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '华北电力大学', url: 'yjsy.ncepu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ncku/csie.ts b/lib/routes/ncku/csie.ts new file mode 100644 index 00000000000000..a8178d74af2e87 --- /dev/null +++ b/lib/routes/ncku/csie.ts @@ -0,0 +1,83 @@ +import type { Route } from '@/types'; +import { CheerioAPI, load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; + +const currentURL = (catagory: string, page: number) => (catagory === '_all' ? `https://www.csie.ncku.edu.tw/zh-hant/news?page=${page}` : `https://www.csie.ncku.edu.tw/zh-hant/news/${catagory}?page=${page}`); + +const catagories = { + _all: '全部資訊', + normal: '一般資訊', + bachelorAdmission: '大學部招生', + masterAdmission: '研究所招生', + speeches: '演講及活動資訊', + awards: '獲獎資訊', + scholarship: '獎助學金', + jobs: '徵人資訊', +}; + +export const route: Route = { + 'zh-TW': { + name: '國立成功大學資訊系公告', + description: '可用分類:_all, normal, bachelorAdmission, masterAdmission, speeches, awards, scholarship, jobs', + }, + name: 'CSIE News', + description: 'Availible catagories:_all, normal, bachelorAdmission, masterAdmission, speeches, awards, scholarship, jobs', + path: '/csie/:catagory?', + categories: ['university'], + example: '/ncku/csie/normal', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.csie.ncku.edu.tw/zh-hant/news/'], + target: '/csie/_all', + }, + { + source: ['www.csie.ncku.edu.tw/zh-hant/news/:catagory'], + target: '/csie/:catagory', + }, + ], + maintainers: ['simbafs'], + handler: async (ctx) => { + let catagory = ctx.req.param('catagory') ?? '_all'; + if (catagories[catagory] === undefined) { + catagory = 'normal'; + } + + const base = 1; // get from query + const limit = 3; // get from query + + const item = ( + await Promise.allSettled( + Array.from({ length: limit }).map(async (_, i) => { + const $ = await ofetch<CheerioAPI>(currentURL(catagory, base + i), { + parseResponse: load, + }); + + return $('.list-title > li') + .toArray() + .map((e) => ({ + title: $('a', e).text(), + pubDate: new Date($('small', e).text()), + link: `https://www.csie.ncku.edu.tw${$('a', e).attr('href')}`, + catagory: $('span:nth-child(2)', e).text(), + })); + }) + ) + ) + .filter((item) => item.status === 'fulfilled') + .flatMap((item) => item.value); + + return { + title: `成大資訊系公告 - ${catagories[catagory]}`, + link: currentURL(catagory, base), + item, + }; + }, +}; diff --git a/lib/routes/ncku/namespace.ts b/lib/routes/ncku/namespace.ts new file mode 100644 index 00000000000000..f1ab851f81bc59 --- /dev/null +++ b/lib/routes/ncku/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'National Cheng Kung University', + url: 'www.ncku.edu.tw', + lang: 'zh-TW', +}; diff --git a/lib/routes/ncku/phys.ts b/lib/routes/ncku/phys.ts new file mode 100644 index 00000000000000..1230d0cd789f6a --- /dev/null +++ b/lib/routes/ncku/phys.ts @@ -0,0 +1,96 @@ +import type { Route } from '@/types'; +import { CheerioAPI, load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; + +const currentURL = (catagory: string) => `https://phys.ncku.edu.tw/news/${catagory === '_all' ? '' : catagory}`; + +const catagories = { + '24': '物理系', + scholarship: '獎助學金', + admission: '招生與錄取報到', + 'course-announcement': '助教公告', + 'bachelor-announcement': '大學部', + 'master-announcement': '研究所', + graduation: '畢業離校', + 'student-guide': '學生手冊與新生入學', + honor: '榮譽榜', + career: '求才公告', + others: '其他', + _all: '所有訊息', +}; + +export const route: Route = { + 'zh-TW': { + name: '國立成功大學物理系公告', + }, + name: 'Phys News', + description: `| 分類 | catagory | +| ---- | ---- | +| 物理系 | 24 | +| 獎助學金 | scholarship | +| 招生與錄取報到 | admission | +| 助教公告 | course-announcement | +| 大學部 | bachelor-announcement | +| 研究所 | master-announcement | +| 畢業離校 | graduation | +| 學生手冊與新生入學 | student-guide | +| 榮譽榜 | honor | +| 求才公告 | career | +| 其他 | others | +| 所有訊息 | _all | +`, + path: '/phys/:catagory?', + parameters: { + catagory: 'catagory, default is _all', + }, + categories: ['university'], + example: '/ncku/phys/_all', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['phys.ncku.edu.tw/news/'], + target: '/phys/_all', + }, + { + source: ['phys.ncku.edu.tw/news/:catagory/'], + target: '/phys/:catagory', + }, + ], + maintainers: ['simbafs'], + handler: async (ctx) => { + let catagory = ctx.req.param('catagory') ?? '_all'; + if (catagories[catagory] === undefined) { + catagory = '_all'; + } + + const $ = await ofetch<CheerioAPI>(currentURL(catagory), { + parseResponse: load, + }); + + const item = $('.newsList .Txt') + .toArray() + .map((e) => ({ + title: $('a', e).text(), + pubDate: new Date( + $('.newsDate', e) + .text() + .match(/\d{4}(?: \/ \d{2}){2}/)?.[0] || '' + ), + link: $('a', e).attr('href'), + catagory: $('.newIcon', e).text(), + })); + + return { + title: `成大物理系公告 - ${catagories[catagory]}`, + link: currentURL(catagory), + item, + }; + }, +}; diff --git a/lib/routes/ncpssd/namespace.ts b/lib/routes/ncpssd/namespace.ts index 317113c1830f37..19bf511f9c5cbe 100644 --- a/lib/routes/ncpssd/namespace.ts +++ b/lib/routes/ncpssd/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '国家哲学社会科学文献中心', url: 'ncpssd.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ncu/namespace.ts b/lib/routes/ncu/namespace.ts index b50b43a80e941c..0d1dc484f9ba72 100644 --- a/lib/routes/ncu/namespace.ts +++ b/lib/routes/ncu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南昌大学', url: 'jwc.ncu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ncwu/namespace.ts b/lib/routes/ncwu/namespace.ts index 97bce4e5d0f62f..bf1bf0c53010ab 100644 --- a/lib/routes/ncwu/namespace.ts +++ b/lib/routes/ncwu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '华北水利水电大学', url: 'ncwu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ndss-symposium/namespace.ts b/lib/routes/ndss-symposium/namespace.ts index ab01400d65459c..83f0c197879cc8 100644 --- a/lib/routes/ndss-symposium/namespace.ts +++ b/lib/routes/ndss-symposium/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Network and Distributed System Security (NDSS) Symposium', url: 'ndss-symposium.org', + lang: 'en', }; diff --git a/lib/routes/neatdownloadmanager/namespace.ts b/lib/routes/neatdownloadmanager/namespace.ts index 0614af9ac05991..578771d013cc2d 100644 --- a/lib/routes/neatdownloadmanager/namespace.ts +++ b/lib/routes/neatdownloadmanager/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Neat Download Manager', url: 'neatdownloadmanager.com', + lang: 'en', }; diff --git a/lib/routes/neea/index.ts b/lib/routes/neea/index.ts index c02d90c33837b7..fa0570ce745f23 100644 --- a/lib/routes/neea/index.ts +++ b/lib/routes/neea/index.ts @@ -2,6 +2,9 @@ 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'; + function loadContent(link) { return cache.tryGet(link, async () => { // 开始加载页面 @@ -20,16 +23,9 @@ function loadContent(link) { }); } -export const route: Route = { - path: '/:type?', - name: 'Unknown', - maintainers: ['SunShinenny'], - handler, -}; - async function handler(ctx) { const type = ctx.req.param('type'); - const host = `http://${type}.neea.edu.cn${typeDic[type].url}`; + const host = `https://${type}.neea.edu.cn${typeDic[type].url}`; const response = await got({ method: 'get', url: host, @@ -43,14 +39,13 @@ async function handler(ctx) { list.map(async (item) => { const ReportIDname = $(item).find('#ReportIDname > a'); const ReportIDIssueTime = $(item).find('#ReportIDIssueTime'); - const itemUrl = `http://${type}.neea.edu.cn` + $(ReportIDname).attr('href'); - let time = new Date(ReportIDIssueTime.text()).getTime(); - time += new Date().getTimezoneOffset() * 60 * 1000 + 8 * 60 * 60 * 1000; // beijing timezone + const itemUrl = `https://${type}.neea.edu.cn` + $(ReportIDname).attr('href'); + const time = ReportIDIssueTime.text(); const single = { title: $(ReportIDname).text(), link: itemUrl, guid: itemUrl, - pubDate: new Date(time).toUTCString(), + pubDate: timezone(parseDate(time), +8), }; const other = await loadContent(String(itemUrl)); return Object.assign({}, single, other); @@ -86,10 +81,14 @@ const typeDic = { url: '/html1/category/1507/1148-1.htm', title: '中小学教师资格考试', }, + tdxl: { + url: '/html1/category/2210/313-1.htm', + title: '同等学力申请硕士学位考试', + }, // 社会证书考试 cet: { url: '/html1/category/16093/1124-1.htm', - title: '全国四六级(CET)', + title: '全国四六级考试(CET)', }, ncre: { url: '/html1/category/1507/872-1.htm', @@ -102,18 +101,47 @@ const typeDic = { pets: { url: '/html1/category/1507/1570-1.htm', - title: '全国英语等级考试 (PETS)', + title: '全国英语等级考试(PETS)', }, wsk: { url: '/html1/category/1507/1646-1.htm', - title: '全国外语水平考试 (WSK)', + title: '全国外语水平考试(WSK)', }, ccpt: { url: '/html1/category/1507/2035-1.htm', - title: '书画等级考试 (CCPT)', + title: '书画等级考试(CCPT)', }, - mets: { - url: '/html1/category/1507/2065-1.htm', - title: '医护英语水平考试 (METS)', +}; + +export const route: Route = { + path: '/local/:type', + name: '国内考试动态', + url: 'www.neea.edu.cn', + maintainers: ['SunShinenny'], + example: '/neea/local/cet', + parameters: { type: '考试项目,见下表' }, + categories: ['study'], + features: { + supportRadar: true, }, + radar: Object.entries(typeDic).map(([type, value]) => ({ + title: `${value.title}动态`, + source: [`${type}.neea.edu.cn`, `${type}.neea.cn`], + target: `/local/${type}`, + })), + handler, + description: `| | 考试项目 | type | +| ------------ | ----------------------------- | -------- | +| 国家教育考试 | 普通高考 | gaokao | +| | 成人高考 | chengkao | +| | 研究生考试 | yankao | +| | 自学考试 | zikao | +| | 中小学教师资格考试 | ntce | +| | 同等学力申请硕士学位考试 | tdxl | +| 社会证书考试 | 全国四六级考试(CET) | cet | +| | 全国计算机等级考试(NCRE) | ncre | +| | 全国计算机应用水平考试(NIT) | nit | +| | 全国英语等级考试(PETS) | pets | +| | 全国外语水平考试(WSK) | wsk | +| | 书画等级考试(CCPT) | ccpt |`, }; diff --git a/lib/routes/neea/jlpt.ts b/lib/routes/neea/jlpt.ts index a016476222a9dc..ea5214c52c88e6 100644 --- a/lib/routes/neea/jlpt.ts +++ b/lib/routes/neea/jlpt.ts @@ -1,78 +1,113 @@ -import { Route } from '@/types'; +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; -export const route: Route = { - path: '/jlpt', - categories: ['study'], - example: '/neea/jlpt', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['jlpt.neea.cn/'], - }, - ], - name: '教育部考试中心日本语能力测试重要通知', - maintainers: ['nczitzk'], - handler, - url: 'jlpt.neea.cn/', -}; +import { type CheerioAPI, type Cheerio, type Element, load } from 'cheerio'; +import { type Context } from 'hono'; + +export const handler = async (ctx: Context): Promise<Data> => { + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); -async function handler() { - const rootUrl = 'https://news.neea.cn'; - const currentUrl = `${rootUrl}/JLPT/1/newslist.htm`; + const baseUrl: string = 'https://jlpt.neea.cn'; + const targetUrl: string = new URL('index.do', baseUrl).href; - const response = await got({ - method: 'get', - url: currentUrl, - }); + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'zh-CN'; - const $ = load(response.data); + let items: DataItem[] = []; - let items = $('a') + items = $('div.indexcontent a') + .slice(0, limit) .toArray() - .map((item) => { - item = $(item); + .map((el): Element => { + const $el: Cheerio<Element> = $(el); - const matches = item.text().match(/(\d{4}-\d{2}-\d{2})/); + const title: string = $el.text(); + const pubDateStr: string | undefined = title.match(/(\d{4}-\d{2}-\d{2})/)?.[1]; + const linkUrl: string | undefined = $el.attr('href'); + const upDatedStr: string | undefined = pubDateStr; - return { - title: item.text(), - link: `${rootUrl}/JLPT/1/${item.attr('href')}`, - pubDate: matches ? parseDate(matches[1]) : '', + const processedItem: DataItem = { + title, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, }; + + return processedItem; }); - items = await Promise.all( - items.map((item) => - cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); - const content = load(detailResponse.data); + const title: string = $$('div.dvTitle').text(); + const description: string = $$('div.dvContent').html() ?? ''; - item.description = content('.dvContent').html(); + const processedItem: DataItem = { + title, + description, + }; - return item; + return { + ...item, + ...processedItem, + }; + }); }) ) - ); + ).filter((_): _ is DataItem => true); + + const title: string = $('title').text(); return { - title: '重要通知 - 教育部考试中心日本语能力测试', - link: currentUrl, + title: `${title.split(/-/).pop()} - ${$('div.indexcontent h1').text()}`, + description: title, + link: targetUrl, item: items, + allowEmpty: true, + image: $('div.header img').attr('arc') ? new URL($('div.header img').attr('arc') as string, baseUrl).href : undefined, + author: title.split(/-/)[0], + language, + id: targetUrl, }; -} +}; + +export const route: Route = { + path: '/jlpt', + name: '日本语能力测试JLPT通知', + url: 'jlpt.neea.cn', + maintainers: ['nczitzk'], + handler, + example: '/neea/jlpt', + parameters: undefined, + description: undefined, + categories: ['study'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['jlpt.neea.cn'], + target: '/jlpt', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/neea/namespace.ts b/lib/routes/neea/namespace.ts index d3aeb74d214dea..272cd338a8df3b 100644 --- a/lib/routes/neea/namespace.ts +++ b/lib/routes/neea/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'NEEA 中国教育考试网', - url: 'jlpt.neea.cn', + name: '中国教育考试网', + url: 'www.neea.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nenu/namespace.ts b/lib/routes/nenu/namespace.ts index ec8774201a1dda..090c3e05a3d115 100644 --- a/lib/routes/nenu/namespace.ts +++ b/lib/routes/nenu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '东北师范大学', url: 'sohac.nenu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/netflav/namespace.ts b/lib/routes/netflav/namespace.ts index 6438e6fcf5578a..7e4073381c282b 100644 --- a/lib/routes/netflav/namespace.ts +++ b/lib/routes/netflav/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Netflav', url: 'netflav.com', + lang: 'en', }; diff --git a/lib/routes/neu/bmie.ts b/lib/routes/neu/bmie.ts index f6bb3212a43169..0a1df43f0012a0 100644 --- a/lib/routes/neu/bmie.ts +++ b/lib/routes/neu/bmie.ts @@ -41,22 +41,22 @@ export const route: Route = { maintainers: ['tennousuathena'], handler, description: `| Id | 名称 | - | ----------------------- | ---------- | - | news | 学院新闻 | - | academic | 学术科研 | - | talent\_development | 人才培养 | - | international\_exchange | 国际交流 | - | announcement | 通知公告 | - | undergraduate\_dev | 本科生培养 | - | postgraduate\_dev | 研究生培养 | - | undergraduate\_recruit | 本科生招募 | - | postgraduate\_recruit | 研究生招募 | - | CPC\_build | 党的建设 | - | CPC\_work | 党委工作 | - | union\_work | 工会工作 | - | CYL\_work | 共青团工作 | - | security\_management | 安全管理 | - | alumni\_style | 校友风采 |`, +| ----------------------- | ---------- | +| news | 学院新闻 | +| academic | 学术科研 | +| talent\_development | 人才培养 | +| international\_exchange | 国际交流 | +| announcement | 通知公告 | +| undergraduate\_dev | 本科生培养 | +| postgraduate\_dev | 研究生培养 | +| undergraduate\_recruit | 本科生招募 | +| postgraduate\_recruit | 研究生招募 | +| CPC\_build | 党的建设 | +| CPC\_work | 党委工作 | +| union\_work | 工会工作 | +| CYL\_work | 共青团工作 | +| security\_management | 安全管理 | +| alumni\_style | 校友风采 |`, }; async function handler(ctx) { diff --git a/lib/routes/neu/namespace.ts b/lib/routes/neu/namespace.ts index 20251e59881a8a..9806092f24465d 100644 --- a/lib/routes/neu/namespace.ts +++ b/lib/routes/neu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '东北大学', url: 'neunews.neu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/neu/news.ts b/lib/routes/neu/news.ts index 401ec142d5ec69..9948c64d08f9a7 100644 --- a/lib/routes/neu/news.ts +++ b/lib/routes/neu/news.ts @@ -27,22 +27,22 @@ export const route: Route = { maintainers: ['JeasonLau'], handler, description: `| 种类名 | 参数 | - | -------- | ---- | - | 东大要闻 | ddyw | - | 媒体东大 | mtdd | - | 通知公告 | tzgg | - | 新闻纵横 | xwzh | - | 人才培养 | rcpy | - | 学术科研 | xsky | - | 英文新闻 | 217 | - | 招生就业 | zsjy | - | 考研出国 | kycg | - | 校园文学 | xywx | - | 校友风采 | xyfc | - | 时事热点 | ssrd | - | 教育前沿 | jyqy | - | 文化体育 | whty | - | 最新科技 | zxkj |`, +| -------- | ---- | +| 东大要闻 | ddyw | +| 媒体东大 | mtdd | +| 通知公告 | tzgg | +| 新闻纵横 | xwzh | +| 人才培养 | rcpy | +| 学术科研 | xsky | +| 英文新闻 | 217 | +| 招生就业 | zsjy | +| 考研出国 | kycg | +| 校园文学 | xywx | +| 校友风采 | xyfc | +| 时事热点 | ssrd | +| 教育前沿 | jyqy | +| 文化体育 | whty | +| 最新科技 | zxkj |`, }; async function handler(ctx) { diff --git a/lib/routes/newmuseum/namespace.ts b/lib/routes/newmuseum/namespace.ts index e135dfc83b5b6d..7d18c04808d87f 100644 --- a/lib/routes/newmuseum/namespace.ts +++ b/lib/routes/newmuseum/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'New Museum 纽约新美术馆', + name: 'New Museum', url: 'www.newmuseum.org', + lang: 'en', }; diff --git a/lib/routes/newrank/douyin.ts b/lib/routes/newrank/douyin.ts index ed2f38a65af7b5..6e799e77e5497c 100644 --- a/lib/routes/newrank/douyin.ts +++ b/lib/routes/newrank/douyin.ts @@ -25,7 +25,7 @@ export const route: Route = { name: '抖音短视频', maintainers: ['lessmoe'], handler, - description: `:::warning + description: `::: warning 免费版账户抖音每天查询次数 20 次,如需增加次数可购买新榜会员或等待未来多账户支持 :::`, }; diff --git a/lib/routes/newrank/namespace.ts b/lib/routes/newrank/namespace.ts index 5dc50c11d36f9e..3be235f00936eb 100644 --- a/lib/routes/newrank/namespace.ts +++ b/lib/routes/newrank/namespace.ts @@ -3,8 +3,9 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '新榜', url: 'newrank.cn', - description: `:::warning + description: `::: warning 部署时需要配置 NEWRANK\_COOKIE,具体见部署文档 请勿过高频抓取,新榜疑似对每天调用 token 总次数进行了限制,超限会报错 :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/newrank/utils.ts b/lib/routes/newrank/utils.ts index 3b4001309eac65..066cd5b73c521c 100644 --- a/lib/routes/newrank/utils.ts +++ b/lib/routes/newrank/utils.ts @@ -42,7 +42,7 @@ const decrypt_douyin_detail_xyz = (nonce) => { return md5(str); }; -const flatten = (arr) => arr.reduce((acc, val) => [...acc, ...(Array.isArray(val) ? flatten(val) : val)], []); +const flatten = (arr) => arr.reduce((acc, val) => (Array.isArray(val) ? [...acc, ...flatten(val)] : [...acc, val]), []); function shouldUpdateCookie(forcedUpdate = false) { if (forcedUpdate) { diff --git a/lib/routes/newrank/wechat.ts b/lib/routes/newrank/wechat.ts index dd7db0c947f52d..a87822aef724c7 100644 --- a/lib/routes/newrank/wechat.ts +++ b/lib/routes/newrank/wechat.ts @@ -25,7 +25,7 @@ export const route: Route = { supportScihub: false, }, name: '微信公众号', - maintainers: ['lessmoe'], + maintainers: ['lessmoe', 'pseudoyu'], handler, }; @@ -70,16 +70,21 @@ async function handler(ctx) { xyz: utils.decrypt_wechat_detail_xyz(uid, nonce), }, }); + const name = response.data.value.user.name; const realTimeArticles = utils.flatten(response.data.value.realTimeArticles); const articles = utils.flatten(response.data.value.articles); const newArticles = [...realTimeArticles, ...articles]; + const items = newArticles.map((item) => ({ + id: item.id, title: item.title, description: '', link: item.url, pubDate: item.publicTime, })); + + // TODO: link is empty await Promise.all(items.map((item) => finishArticleItem(item))); return { diff --git a/lib/routes/news/namespace.ts b/lib/routes/news/namespace.ts index 8dfc1cc0409ede..35aacd36813089 100644 --- a/lib/routes/news/namespace.ts +++ b/lib/routes/news/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: '小黑盒', + name: '新华社', url: 'news.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/newseed/index.ts b/lib/routes/newseed/index.ts new file mode 100644 index 00000000000000..03b4906d61e989 --- /dev/null +++ b/lib/routes/newseed/index.ts @@ -0,0 +1,73 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/latest', + categories: ['new-media'], + example: '/newseed/latest', + url: 'news.newseed.cn', + name: '最新新闻', + maintainers: ['p3psi-boo'], + handler, +}; + +async function handler() { + const baseUrl = 'https://news.newseed.cn/'; + const response = await got({ + method: 'get', + url: baseUrl, + }); + + const $ = load(response.data); + + const list = $('#news-list li') + .toArray() + .map((item) => { + const element = $(item); + const a = element.find('h3 a'); + const link = a.attr('href') || ''; + const title = a.text(); + const image = element.find('.img img').attr('src'); + const info = element.find('.info'); + const author = info.find('.author a').text(); + const pubDate = info.find('.date').text(); + const tags = element + .find('.tag a') + .toArray() + .map((el) => $(el).text()) + .filter((tag) => tag !== author); + + return { + title, + link, + author, + pubDate, + category: tags, + description: image ? `<img src="${image}"><br>${title}` : title, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await got({ + method: 'get', + url: item.link, + }); + + const $ = load(response.data); + item.description = $('.news-content').html() || item.description; + return item; + }) + ) + ); + + return { + title: '新芽 - 最新新闻', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/newseed/namespace.ts b/lib/routes/newseed/namespace.ts new file mode 100644 index 00000000000000..9ae79a97b8ca34 --- /dev/null +++ b/lib/routes/newseed/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '新芽', + url: 'newseed.cn', + description: '新芽是专注于互联网创业的媒体平台,提供创业资讯、投融资信息、创业活动、创业服务等。', + lang: 'zh-CN', +}; diff --git a/lib/routes/newsmarket/index.ts b/lib/routes/newsmarket/index.ts index c64dd3229ddb35..ddae997667f895 100644 --- a/lib/routes/newsmarket/index.ts +++ b/lib/routes/newsmarket/index.ts @@ -26,12 +26,12 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 時事。政策 | 食安 | 新知 | 愛地方 | 種好田 | 好吃。好玩 | - | ----------- | ----------- | --------- | ------------ | ------------ | ------------- | - | news-policy | food-safety | knowledge | country-life | good-farming | good-food-fun | +| ----------- | ----------- | --------- | ------------ | ------------ | ------------- | +| news-policy | food-safety | knowledge | country-life | good-farming | good-food-fun | - | 食農教育 | 人物 | 漁業。畜牧 | 綠生活。國際 | 評論 | - | -------------- | ------------------ | -------------------- | ------------------- | ------- | - | food-education | people-and-history | raising-and-breeding | living-green-travel | opinion |`, +| 食農教育 | 人物 | 漁業。畜牧 | 綠生活。國際 | 評論 | +| -------------- | ------------------ | -------------------- | ------------------- | ------- | +| food-education | people-and-history | raising-and-breeding | living-green-travel | opinion |`, }; async function handler(ctx) { diff --git a/lib/routes/newsmarket/namespace.ts b/lib/routes/newsmarket/namespace.ts index 62811b182da4bc..e1b1f673d78220 100644 --- a/lib/routes/newsmarket/namespace.ts +++ b/lib/routes/newsmarket/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上下游 News&Market', url: 'newsmarket.com.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/newyorker/namespace.ts b/lib/routes/newyorker/namespace.ts index 2046e8fb5a3f5d..cfb89952048a45 100644 --- a/lib/routes/newyorker/namespace.ts +++ b/lib/routes/newyorker/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'New Yorker', url: 'newyorker.com', + lang: 'en', }; diff --git a/lib/routes/newyorker/news.ts b/lib/routes/newyorker/news.ts index 1f185f0092bfbe..daf1221ba48047 100644 --- a/lib/routes/newyorker/news.ts +++ b/lib/routes/newyorker/news.ts @@ -1,14 +1,15 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; -import parser from '@/utils/rss-parser'; import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; + const host = 'https://www.newyorker.com'; export const route: Route = { - path: '/:category/:subCategory?', - categories: ['traditional-media'], - example: '/newyorker/everything', - parameters: { category: 'rss category. can be found at `https://www.newyorker.com/about/feeds`' }, + path: '/:category', + categories: ['traditional-media', 'popular'], + view: ViewType.Articles, + example: '/newyorker/latest', + parameters: { category: 'tab name. can be found at url' }, features: { requireConfig: false, requirePuppeteer: false, @@ -19,22 +20,30 @@ export const route: Route = { }, radar: [ { - source: ['newyorker.com/feed/:category/:subCategory?'], + source: ['newyorker.com/:category?'], }, ], - name: 'The New Yorker', - maintainers: ['EthanWng97'], + name: 'Articles', + maintainers: ['EthanWng97', 'pseudoyu'], handler, }; async function handler(ctx) { - const { category, subCategory } = ctx.req.param(); - const rssUrl = subCategory ? `${host}/feed/${category}/${subCategory}` : `${host}/feed/${category}`; - const feed = await parser.parseURL(rssUrl); + const { category } = ctx.req.param(); + const link = `${host}/${category}`; + const response = await ofetch(link); + const $ = load(response); + const preloadedState = JSON.parse( + $('script:contains("window.__PRELOADED_STATE__")') + .text() + .match(/window\.__PRELOADED_STATE__ = (.*);/)?.[1] ?? '{}' + ); + const list = preloadedState.transformed.bundle.containers[0].items; const items = await Promise.all( - feed.items.map((item) => - cache.tryGet(item.link, async () => { - const data = await ofetch(item.link); + list.map((item) => { + const url = `${host}${item.url}`; + return cache.tryGet(url, async () => { + const data = await ofetch(url); const $ = load(data); const description = $('#main-content'); description.find('h1').remove(); @@ -43,18 +52,17 @@ async function handler(ctx) { description.find('div[class^="ActionBarWrapperContent-"]').remove(); description.find('div[class^="ContentHeaderByline-"]').remove(); return { - title: item.title, + title: item.dangerousHed, pubDate: item.pubDate, - link: item.link, - category: item.categories, + link: url, description: description.html(), }; - }) - ) + }); + }) ); return { - title: `The New Yorker - ${feed.title}`, + title: `The New Yorker - ${category}`, link: host, description: 'Reporting, Profiles, breaking news, cultural coverage, podcasts, videos, and cartoons from The New Yorker.', item: items, diff --git a/lib/routes/newzmz/index.ts b/lib/routes/newzmz/index.ts index 5850206fd4485c..8d91f0e1bb2cd4 100644 --- a/lib/routes/newzmz/index.ts +++ b/lib/routes/newzmz/index.ts @@ -28,9 +28,9 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'newzmz.com/', - description: `:::tip + description: `::: tip [雪国列车 (剧版)](https://nzmz.xyz/details-qEzRyY3v.html) 的下载页 URL 为 \`https://v.ys99.xyz/view/qEzRyY3v.html\`,即剧集 id 为 \`qEzRyY3v\` - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/newzmz/namespace.ts b/lib/routes/newzmz/namespace.ts index 30d940e8398688..dd11466dda72de 100644 --- a/lib/routes/newzmz/namespace.ts +++ b/lib/routes/newzmz/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'NEW 字幕组', url: 'newzmz.com', + lang: 'zh-CN', }; diff --git a/lib/routes/newzmz/util.ts b/lib/routes/newzmz/util.ts index 3d301187ffaab7..64ccce324bd129 100644 --- a/lib/routes/newzmz/util.ts +++ b/lib/routes/newzmz/util.ts @@ -154,7 +154,7 @@ const processItems = async (i, downLinkType, itemSelector, categorySelector, dow author: i.author, category: [...i.category, ...categories].filter(Boolean), pubDate: i.pubDate, - enclosure_url: downLinks.filter((l) => l.title === downLinkType).pop()?.link ?? downLinks[0].link, + enclosure_url: downLinks.findLast((l) => l.title === downLinkType)?.link ?? downLinks[0].link, enclosure_type: 'application/x-bittorrent', }; }); diff --git a/lib/routes/nextapple/namespace.ts b/lib/routes/nextapple/namespace.ts index e2630f4c91f535..405ad6b0607e9f 100644 --- a/lib/routes/nextapple/namespace.ts +++ b/lib/routes/nextapple/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '壹蘋新聞網', url: 'tw.nextapple.com', + lang: 'zh-TW', }; diff --git a/lib/routes/nextapple/realtime.ts b/lib/routes/nextapple/realtime.ts index 27a324104a872d..169267ddb6a741 100644 --- a/lib/routes/nextapple/realtime.ts +++ b/lib/routes/nextapple/realtime.ts @@ -28,12 +28,12 @@ export const route: Route = { handler, url: 'tw.nextapple.com/', description: `| 首頁 | 焦點 | 熱門 | 娛樂 | 生活 | 女神 | 社會 | - | ------ | --------- | ---- | ------------- | ---- | -------- | ----- | - | latest | recommend | hit | entertainment | life | gorgeous | local | +| ------ | --------- | ---- | ------------- | ---- | -------- | ----- | +| latest | recommend | hit | entertainment | life | gorgeous | local | - | 政治 | 國際 | 財經 | 體育 | 旅遊美食 | 3C 車市 | - | -------- | ------------- | ------- | ------ | --------- | ------- | - | politics | international | finance | sports | lifestyle | gadget |`, +| 政治 | 國際 | 財經 | 體育 | 旅遊美食 | 3C 車市 | +| -------- | ------------- | ------- | ------ | --------- | ------- | +| politics | international | finance | sports | lifestyle | gadget |`, }; async function handler(ctx) { diff --git a/lib/routes/nextjs/blog.ts b/lib/routes/nextjs/blog.ts new file mode 100644 index 00000000000000..00b5c6ffe9c187 --- /dev/null +++ b/lib/routes/nextjs/blog.ts @@ -0,0 +1,63 @@ +import type { DataItem, Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; + +const handler: Route['handler'] = async () => { + const data = await ofetch('https://nextjs.org/blog'); + + const $ = load(data); + + const item = (await Promise.all( + $('article') + .toArray() + .slice(0, 20) + .map((item) => { + const $ = load(item); + const link = `https://nextjs.org${$('a[href^="/blog"]').attr('href')}`; + + return cache.tryGet(`nextjs:blog:${link}`, async () => { + const data = await ofetch(link); + + const $ = load(data); + + return { + title: $('h1').first().text().trim(), + link, + description: $('div.prose').html() ?? '', + pubDate: parseDate( + $('p[data-version="v1"]') + .first() + .text() + .replace(/st|nd|rd|th/, '') + ), + }; + }); + }) + )) as DataItem[]; + + return { + title: 'Next.js Blog', + link: 'https://nextjs.org/blog', + language: 'en-US', + item, + }; +}; + +export const route: Route = { + path: '/blog', + name: 'Blog', + categories: ['program-update'], + maintainers: ['equt'], + example: '/nextjs/blog', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + handler, +}; diff --git a/lib/routes/nextjs/namespace.ts b/lib/routes/nextjs/namespace.ts new file mode 100644 index 00000000000000..24f8453659f458 --- /dev/null +++ b/lib/routes/nextjs/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Next.js', + url: 'nextjs.org', + lang: 'en', +}; diff --git a/lib/routes/nga/forum.ts b/lib/routes/nga/forum.ts index efdde7c5cee5ac..6b7f4928143fd0 100644 --- a/lib/routes/nga/forum.ts +++ b/lib/routes/nga/forum.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 { config } from '@/config'; @@ -8,7 +8,8 @@ const X_UA = 'NGA_skull/6.0.5(iPhone10,3;iOS 12.0.1)'; export const route: Route = { path: '/forum/:fid/:recommend?', - categories: ['bbs'], + categories: ['bbs', 'popular'], + view: ViewType.Articles, example: '/nga/forum/489', parameters: { fid: '分区 id, 可在分区主页 URL 找到, 没有 fid 时 stid 同样适用', recommend: '是否只显示精华主题, 留空为否, 任意值为是' }, features: { diff --git a/lib/routes/nga/namespace.ts b/lib/routes/nga/namespace.ts index 3f47ca99fa4d86..4aa77cc3595d7c 100644 --- a/lib/routes/nga/namespace.ts +++ b/lib/routes/nga/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'NGA', url: 'bbs.nga.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ngocn2/index.ts b/lib/routes/ngocn2/index.ts index d95557f1ce69b0..ec5ed99fafef00 100644 --- a/lib/routes/ngocn2/index.ts +++ b/lib/routes/ngocn2/index.ts @@ -27,8 +27,8 @@ export const route: Route = { handler, url: 'ngocn2.org/', description: `| 所有文章 | 早报 | 热点 | - | -------- | ----------- | -------- | - | article | daily-brief | trending |`, +| -------- | ----------- | -------- | +| article | daily-brief | trending |`, }; async function handler(ctx) { diff --git a/lib/routes/ngocn2/namespace.ts b/lib/routes/ngocn2/namespace.ts index 879cb18a94bd5b..642d3272f1e606 100644 --- a/lib/routes/ngocn2/namespace.ts +++ b/lib/routes/ngocn2/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'NGOCN', url: 'ngocn2.org', + lang: 'zh-TW', }; diff --git a/lib/routes/nhentai/other.ts b/lib/routes/nhentai/index.ts similarity index 86% rename from lib/routes/nhentai/other.ts rename to lib/routes/nhentai/index.ts index d7fe5273b3fe75..dcacfac6f681b9 100644 --- a/lib/routes/nhentai/other.ts +++ b/lib/routes/nhentai/index.ts @@ -6,26 +6,21 @@ import InvalidParameterError from '@/errors/types/invalid-parameter'; const supportedKeys = new Set(['parody', 'character', 'tag', 'artist', 'group', 'language', 'category']); export const route: Route = { - path: '/:key/:keyword/:mode?', - categories: ['anime'], - example: '/nhentai/language/chinese', + path: '/index/:key/:keyword/:mode?', + example: '/nhentai/index/language/chinese', parameters: { key: 'Filter term, can be: `parody`, `character`, `tag`, `artist`, `group`, `language` or `category`', keyword: 'Filter value', mode: 'mode, `simple` to only show cover, `detail` to show all pages, `torrent` to include Magnet URI, need login, refer to [Route-specific Configurations](https://docs.rsshub.app/deploy/config#route-specific-configurations), default to `simple`', }, features: { - requireConfig: false, - requirePuppeteer: false, antiCrawler: true, supportBT: true, - supportPodcast: false, - supportScihub: false, }, radar: [ { source: ['nhentai.net/:key/:keyword'], - target: '/:key/:keyword', + target: '/index/:key/:keyword', }, ], name: 'Filter', diff --git a/lib/routes/nhentai/namespace.ts b/lib/routes/nhentai/namespace.ts index 3d10d8736d2ecd..8307086ab5f46f 100644 --- a/lib/routes/nhentai/namespace.ts +++ b/lib/routes/nhentai/namespace.ts @@ -3,4 +3,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'nhentai', url: 'nhentai.net', + categories: ['anime'], + lang: 'en', }; diff --git a/lib/routes/nhentai/search.ts b/lib/routes/nhentai/search.ts index cb26cf29e63134..d719758bccad60 100644 --- a/lib/routes/nhentai/search.ts +++ b/lib/routes/nhentai/search.ts @@ -4,19 +4,14 @@ import { getSimple, getDetails, getTorrents } from './util'; export const route: Route = { path: '/search/:keyword/:mode?', - categories: ['anime'], example: '/nhentai/search/language%3Ajapanese+-scat+-yaoi+-guro+-"mosaic+censorship"', parameters: { keyword: 'Keywords for search. You can copy the content after `q=` after searching on the original website, or you can enter it directly. See the [official website](https://nhentai.net/info/) for details', mode: 'mode, `simple` to only show cover, `detail` to show all pages, `torrent` to include Magnet URI, need login, refer to [Route-specific Configurations](https://docs.rsshub.app/deploy/config#route-specific-configurations), default to `simple`', }, features: { - requireConfig: false, - requirePuppeteer: false, antiCrawler: true, supportBT: true, - supportPodcast: false, - supportScihub: false, }, radar: [ { diff --git a/lib/routes/nhentai/util.ts b/lib/routes/nhentai/util.ts index 9783450b1010d7..5cdd3478382d77 100644 --- a/lib/routes/nhentai/util.ts +++ b/lib/routes/nhentai/util.ts @@ -3,6 +3,7 @@ const __dirname = getCurrentPath(import.meta.url); import { load } from 'cheerio'; import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { config } from '@/config'; import { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; @@ -67,9 +68,8 @@ const getCookie = async (username, password, cache) => { return userTokenCookie; }; -const gotByIp = (url, ...options) => - // credit: https://github.com/sinkaroid/jandapress/pull/23 - got(url.replace(/^https:\/\/nhentai.net\//, 'http://129.150.63.211:3002/'), { +const oFetch = (url, ...options) => + ofetch(url, { ...options, headers: { host: 'nhentai.net', @@ -77,7 +77,7 @@ const gotByIp = (url, ...options) => }); const getSimple = async (url) => { - const { data } = await gotByIp(url); + const data = await oFetch(url); const $ = load(data); return $('.gallery a.cover') @@ -103,7 +103,10 @@ const parseSimpleDetail = ($ele) => { const link = new URL($ele.attr('href'), baseUrl).href; const thumb = $ele.children('img'); const thumbSrc = thumb.attr('data-src') || thumb.attr('src'); - const highResoThumbSrc = thumbSrc.replace('thumb', '1').replace(/t(\d+)\.nhentai\.net/, 'i$1.nhentai.net'); + const highResoThumbSrc = thumbSrc + .replace('thumb', '1') + .replace(/t(\d+)\.nhentai\.net/, 'i$1.nhentai.net') + .replace('.webp.webp', '.webp'); return { title: $ele.children('.caption').text(), link, @@ -113,17 +116,17 @@ const parseSimpleDetail = ($ele) => { const getTorrent = async (simple, cookie) => { const { link } = simple; - const response = await gotByIp(link + 'download', { followRedirect: false, responseType: 'buffer', headers: { Cookie: cookie } }); + const response = await oFetch(link + 'download', { followRedirect: false, responseType: 'buffer', headers: { Cookie: cookie } }); return { ...simple, - enclosure_url: response.data, + enclosure_url: response, enclosure_type: 'application/x-bittorrent', }; }; const getDetail = async (simple) => { const { link } = simple; - const { data } = await gotByIp(link); + const data = await oFetch(link); const $ = load(data); const galleryImgs = $('.gallerythumb img') diff --git a/lib/routes/nhk/namespace.ts b/lib/routes/nhk/namespace.ts index d1c485f1a8a936..f6fac0f194fd4d 100644 --- a/lib/routes/nhk/namespace.ts +++ b/lib/routes/nhk/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'NHK', url: 'www3.nhk.or.jp', + lang: 'ja', }; diff --git a/lib/routes/nhk/news.ts b/lib/routes/nhk/news.ts index 1747220a113f64..7ab05259f44768 100644 --- a/lib/routes/nhk/news.ts +++ b/lib/routes/nhk/news.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); @@ -12,9 +12,37 @@ const apiUrl = 'https://nwapi.nhk.jp'; export const route: Route = { path: '/news/:lang?', - categories: ['traditional-media'], + categories: ['traditional-media', 'popular'], + view: ViewType.Articles, example: '/nhk/news/en', - parameters: { lang: 'Language, see below, `en` by default' }, + parameters: { + lang: { + description: 'Language, see below', + options: [ + { value: 'ar', label: 'العربية' }, + { value: 'bn', label: 'বাংলা' }, + { value: 'my', label: 'မြန်မာဘာသာစကား' }, + { value: 'zh', label: '中文(简体)' }, + { value: 'zt', label: '中文(繁體)' }, + { value: 'en', label: 'English' }, + { value: 'fr', label: 'Français' }, + { value: 'hi', label: 'हिन्दी' }, + { value: 'id', label: 'Bahasa Indonesia' }, + { value: 'ko', label: '코리언' }, + { value: 'fa', label: 'فارسی' }, + { value: 'pt', label: 'Português' }, + { value: 'ru', label: 'Русский' }, + { value: 'es', label: 'Español' }, + { value: 'sw', label: 'Kiswahili' }, + { value: 'th', label: 'ภาษาไทย' }, + { value: 'tr', label: 'Türkçe' }, + { value: 'uk', label: 'Українська' }, + { value: 'ur', label: 'اردو' }, + { value: 'vi', label: 'Tiếng Việt' }, + ], + default: 'en', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -30,19 +58,8 @@ export const route: Route = { }, ], name: 'WORLD-JAPAN - Top Stories', - maintainers: ['TonyRL'], + maintainers: ['TonyRL', 'pseudoyu', 'cscnk52'], handler, - description: `| العربية | বাংলা | မြန်မာဘာသာစကား | 中文(简体) | 中文(繁體) | English | Français | - | ------- | -- | ------------ | ------------ | ------------ | ------- | -------- | - | ar | bn | my | zh | zt | en | fr | - - | हिन्दी | Bahasa Indonesia | 코리언 | فارسی | Português | Русский | Español | - | -- | ---------------- | ------ | ----- | --------- | ------- | ------- | - | hi | id | ko | fa | pt | ru | es | - - | Kiswahili | ภาษาไทย | Türkçe | Українська | اردو | Tiếng Việt | - | --------- | ------- | ------ | ---------- | ---- | ---------- | - | sw | th | tr | uk | ur | vi |`, }; async function handler(ctx) { @@ -65,7 +82,7 @@ async function handler(ctx) { item.category = Object.values(data.data.categories); item.description = art(path.join(__dirname, 'templates/news.art'), { img: data.data.thumbnails, - description: data.data.detail.replace('\n\n', '<br><br>'), + description: data.data.detail.replaceAll('\n\n', '<br><br>'), }); delete item.id; return item; diff --git a/lib/routes/niaogebiji/namespace.ts b/lib/routes/niaogebiji/namespace.ts index 3a259c592c6a2f..c2fddda2b185d9 100644 --- a/lib/routes/niaogebiji/namespace.ts +++ b/lib/routes/niaogebiji/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '鸟哥笔记', url: 'niaogebiji.com', + lang: 'zh-CN', }; diff --git a/lib/routes/nicovideo/namespace.ts b/lib/routes/nicovideo/namespace.ts new file mode 100644 index 00000000000000..34f247fcc31b06 --- /dev/null +++ b/lib/routes/nicovideo/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Niconico', + url: 'www.nicovideo.jp', + categories: ['multimedia'], + lang: 'ja', +}; diff --git a/lib/routes/nicovideo/templates/video.art b/lib/routes/nicovideo/templates/video.art new file mode 100644 index 00000000000000..457b2e399a478c --- /dev/null +++ b/lib/routes/nicovideo/templates/video.art @@ -0,0 +1,7 @@ +{{ if embed }} + <iframe src="https://embed.nicovideo.jp/watch/{{ video.id }}" style="top: 0; left: 0; width: 100%; height: 100%; position: absolute; border: 0;" allowfullscreen></iframe> +{{ else }} + <img src="{{ video.thumbnail.nHdUrl || video.thumbnail.largeUrl || video.thumbnail.middleUrl }}"> +{{ /if }} +<br> +{{@ video.shortDescription }} diff --git a/lib/routes/nicovideo/types.ts b/lib/routes/nicovideo/types.ts new file mode 100644 index 00000000000000..f8690c625efdd6 --- /dev/null +++ b/lib/routes/nicovideo/types.ts @@ -0,0 +1,54 @@ +export interface UserInfo { + id: number; + nickname: string; + icon: string; + smallIcon: string; +} + +interface Count { + view: number; + comment: number; + mylist: number; + like: number; +} + +interface Thumbnail { + url: string; + middleUrl: string | null; + largeUrl: string | null; + listingUrl: string; + nHdUrl: string; +} + +interface Owner { + ownerType: string; + type: string; + visibility: string; + id: string; + name: string; + iconUrl: string; +} + +export interface Essential { + type: string; + id: string; + title: string; + registeredAt: string; + count: Count; + thumbnail: Thumbnail; + duration: number; + shortDescription: string; + latestCommentSummary: string; + isChannelVideo: boolean; + isPaymentRequired: boolean; + playbackPosition: null; + owner: Owner; + requireSensitiveMasking: boolean; + videoLive: null; + isMuted: boolean; +} + +export interface VideoItem { + series: null; + essential: Essential; +} diff --git a/lib/routes/nicovideo/utils.ts b/lib/routes/nicovideo/utils.ts new file mode 100644 index 00000000000000..10b92a6971937a --- /dev/null +++ b/lib/routes/nicovideo/utils.ts @@ -0,0 +1,37 @@ +import { Essential, UserInfo, VideoItem } from './types'; + +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { config } from '@/config'; +import path from 'node:path'; +import { art } from '@/utils/render'; +import { getCurrentPath } from '@/utils/helpers'; + +const __dirname = getCurrentPath(import.meta.url); + +export const getUserInfoById = (id: string) => cache.tryGet(`nicovideo:user:${id}`, () => ofetch<UserInfo>(`https://embed.nicovideo.jp/users/${id}`)) as Promise<UserInfo>; + +export const getUserVideosById = (id: string) => + cache.tryGet( + `nicovideo:user:${id}:videos`, + async () => { + const { data } = await ofetch(`https://nvapi.nicovideo.jp/v3/users/${id}/videos`, { + headers: { + 'X-Frontend-Id': '6', + }, + query: { + sortKey: 'registeredAt', + sortOrder: 'desc', + sensitiveContents: 'mask', + pageSize: 100, + page: 1, + }, + }); + + return data.items; + }, + config.cache.routeExpire, + false + ) as Promise<VideoItem[]>; + +export const renderVideo = (video: Essential, embed: boolean) => art(path.join(__dirname, 'templates', 'video.art'), { video, embed }); diff --git a/lib/routes/nicovideo/video.ts b/lib/routes/nicovideo/video.ts new file mode 100644 index 00000000000000..d09959386fd9ec --- /dev/null +++ b/lib/routes/nicovideo/video.ts @@ -0,0 +1,47 @@ +import type { Context } from 'hono'; +import { DataItem, Route } from '@/types'; +import { UserInfo, VideoItem } from './types'; + +import { parseDate } from '@/utils/parse-date'; +import { getUserInfoById, getUserVideosById, renderVideo } from './utils'; + +const handler = async (ctx: Context) => { + const { id } = ctx.req.param(); + const embed = !ctx.req.param('embed'); + + const userInfo: UserInfo = await getUserInfoById(id); + const videos: VideoItem[] = await getUserVideosById(id); + + const items = videos.map(({ essential: video }) => ({ + title: video.title, + link: `https://www.nicovideo.jp/watch/${video.id}`, + pubDate: parseDate(video.registeredAt), + author: [{ name: video.owner.name, avatar: video.owner.iconUrl, url: `https://www.nicovideo.jp/user/${video.owner.id}` }], + description: renderVideo(video, embed), + image: video.thumbnail.nHdUrl || video.thumbnail.largeUrl || video.thumbnail.middleUrl, + upvotes: video.count.like, + comments: video.count.comment, + })) as DataItem[]; + + return { + title: `${userInfo.nickname} - ニコニコ`, + link: `https://www.nicovideo.jp/user/${id}/video`, + image: userInfo.icon, + item: items, + }; +}; + +export const route: Route = { + name: 'User Videos', + path: '/user/:id/video/:embed?', + parameters: { id: 'User ID', embed: 'Default to embed the video, set to any value to disable embedding' }, + example: '/nicovideo/user/16690815/video', + maintainers: ['TonyRL'], + radar: [ + { + source: ['www.nicovideo.jp/user/:id', 'www.nicovideo.jp/user/:id/video'], + target: '/user/:id/video', + }, + ], + handler, +}; diff --git a/lib/routes/nielsberglund/index.ts b/lib/routes/nielsberglund/index.ts new file mode 100644 index 00000000000000..5f4122948a34cf --- /dev/null +++ b/lib/routes/nielsberglund/index.ts @@ -0,0 +1,78 @@ +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: '/nielsberglund/blog', + radar: [ + { + source: ['nielsberglund.com/'], + }, + ], + url: 'nielsberglund.com/', + name: 'Blog', + maintainers: ['liyaozhong'], + handler, + description: 'Niels Berglund Blog Posts', +}; + +async function handler() { + const rootUrl = 'https://nielsberglund.com'; + const currentUrl = rootUrl; + + const response = await got(currentUrl); + const $ = load(response.data); + + let items = $('.post-preview') + .toArray() + .map((item) => { + const $item = $(item); + const $link = $item.find('a').first(); + const href = $link.attr('href'); + const title = $item.find('.post-title').first().text().trim(); + const dateStr = $item.find('.post-meta').text().trim(); + + if (!href || !title) { + return null; + } + + const link = new URL(href, rootUrl).href; + const pubDate = parseDate(dateStr); + + 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); + + item.description = $detail('.post-container').html() || ''; + + return item; + } catch { + return item; + } + }) + ) + ) + ).filter((item): item is DataItem => item !== null); + + return { + title: 'Niels Berglund Blog', + link: rootUrl, + item: items, + }; +} diff --git a/lib/routes/nielsberglund/namespace.ts b/lib/routes/nielsberglund/namespace.ts new file mode 100644 index 00000000000000..4f221a7f49a1e0 --- /dev/null +++ b/lib/routes/nielsberglund/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Niels Berglund Blog', + url: 'nielsberglund.com', + lang: 'en', +}; diff --git a/lib/routes/nifd/namespace.ts b/lib/routes/nifd/namespace.ts index ed6a0e8893397a..df90764e93a5c6 100644 --- a/lib/routes/nifd/namespace.ts +++ b/lib/routes/nifd/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '国家金融与发展实验室', url: 'www.nifd.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nikkei/asia/index.ts b/lib/routes/nikkei/asia/index.ts index 75d29f5fcd11ba..0f36c8b82e1e47 100644 --- a/lib/routes/nikkei/asia/index.ts +++ b/lib/routes/nikkei/asia/index.ts @@ -8,15 +8,6 @@ export const route: Route = { path: '/asia', categories: ['traditional-media'], example: '/nikkei/asia', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, radar: [ { source: ['asia.nikkei.com/'], @@ -25,7 +16,7 @@ export const route: Route = { name: 'Nikkei Asia Latest News', maintainers: ['rainrdx'], handler, - url: 'asia.nikkei.com/', + url: 'asia.nikkei.com', }; async function handler() { diff --git a/lib/routes/nikkei/cn/index.ts b/lib/routes/nikkei/cn/index.ts index 8437b838aa2196..50a96f572d1a8a 100644 --- a/lib/routes/nikkei/cn/index.ts +++ b/lib/routes/nikkei/cn/index.ts @@ -5,13 +5,60 @@ import got from '@/utils/got'; import { load } from 'cheerio'; import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; -import parser from '@/utils/rss-parser'; +import { config } from '@/config'; +import Parser from 'rss-parser'; + +const parser = new Parser({ + customFields: { + item: ['magnet'], + }, + headers: { + 'User-Agent': config.ua, + }, + defaultRSS: 0.9, +}); export const route: Route = { path: '/cn/*', - name: 'Unknown', - maintainers: [], + name: '中文版新闻', + example: '/nikkei/cn', + maintainers: ['nczitzk'], handler, + description: `::: tip + 如 [中国 经济 日经中文网](https://cn.nikkei.com/china/ceconomy.html) 的 URL 为 \`https://cn.nikkei.com/china/ceconomy.html\` 对应路由为 [\`/nikkei/cn/cn/china/ceconomy\`](https://rsshub.app/nikkei/cn/cn/china/ceconomy) + + 如 [中國 經濟 日經中文網](https://zh.cn.nikkei.com/china/ceconomy.html) 的 URL 为 \`https://zh.cn.nikkei.com/china/ceconomy.html\` 对应路由为 [\`/nikkei/cn/zh/china/ceconomy\`](https://rsshub.app/nikkei/cn/zh/china/ceconomy) + + 特别地,当 \`path\` 填入 \`rss\` 后(如路由为 [\`/nikkei/cn/cn/rss\`](https://rsshub.app/nikkei/cn/cn/rss)),此时返回的是 [官方 RSS 的内容](https://cn.nikkei.com/rss.html) +:::`, + radar: [ + { + title: '中文版新闻', + source: ['cn.nikkei.com/:category/:type', 'cn.nikkei.com/:category', 'cn.nikkei.com/'], + target: (params) => { + if (params.category && params.type) { + return `/nikkei/cn/cn/${params.category}/${params.type.replace('.html', '')}`; + } else if (params.category && !params.type) { + return `/nikkei/cn/cn/${params.category.replace('.html', '')}`; + } else { + return `/nikkei/cn/cn`; + } + }, + }, + { + title: '中文版新聞', + source: ['zh.cn.nikkei.com/:category/:type', 'zh.cn.nikkei.com/:category', 'zh.cn.nikkei.com/'], + target: (params) => { + if (params.category && params.type) { + return `/nikkei/cn/zh/${params.category}/${params.type.replace('.html', '')}`; + } else if (params.category && !params.type) { + return `/nikkei/cn/zh/${params.category.replace('.html', '')}`; + } else { + return `/nikkei/cn/zh`; + } + }, + }, + ], }; async function handler(ctx) { @@ -20,7 +67,7 @@ async function handler(ctx) { if (/^\/cn\/(cn|zh)/.test(path)) { language = path.match(/^\/cn\/(cn|zh)/)[1]; - path = path.match(new RegExp('\\/cn\\/' + language + '(.*)'))[1]; + path = path.match(new RegExp(String.raw`\/cn\/` + language + '(.*)'))[1]; } else { language = 'cn'; } @@ -40,7 +87,7 @@ async function handler(ctx) { officialFeed = await parser.parseURL(currentUrl); items = officialFeed.items.slice(0, limit).map((item) => ({ title: item.title, - link: new URL(item.attr('href'), rootUrl).href, + link: new URL(item.link, rootUrl).href, })); } else { const response = await got({ diff --git a/lib/routes/nikkei/index.ts b/lib/routes/nikkei/index.ts index 3767f2a55ebfdf..ae0cb2f5769dfe 100644 --- a/lib/routes/nikkei/index.ts +++ b/lib/routes/nikkei/index.ts @@ -3,11 +3,12 @@ import got from '@/utils/got'; import { load } from 'cheerio'; export const route: Route = { - path: ['/', '/index'], - name: 'Unknown', - maintainers: [], + path: '/index', + name: 'Home', + example: '/nikkei/index', + maintainers: ['zjysdhr'], handler, - url: 'www.nikkei.com/', + url: 'www.nikkei.com', }; async function handler() { diff --git a/lib/routes/nikkei/namespace.ts b/lib/routes/nikkei/namespace.ts index ea3cef38390233..3ff282901d0626 100644 --- a/lib/routes/nikkei/namespace.ts +++ b/lib/routes/nikkei/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The Nikkei 日本経済新聞', - url: 'asia.nikkei.com', + url: 'nikkei.com', + lang: 'ja', }; diff --git a/lib/routes/nikkei/news.ts b/lib/routes/nikkei/news.ts index 757dd8d999922a..c9e8a672234505 100644 --- a/lib/routes/nikkei/news.ts +++ b/lib/routes/nikkei/news.ts @@ -10,9 +10,9 @@ import { art } from '@/utils/render'; import path from 'node:path'; export const route: Route = { - path: '/:category/:article_type?', + path: '/news/:category/:article_type?', categories: ['traditional-media'], - example: '/nikkei/news', + example: '/nikkei/news/news', parameters: { category: 'Category, see table below', article_type: 'Only includes free articles, set `free` to enable, disabled by default' }, radar: [ { @@ -24,8 +24,8 @@ export const route: Route = { maintainers: ['Arracc', 'ladeng07'], handler, description: `| 総合 | オピニオン | 経済 | 政治 | 金融 | マーケット | ビジネス | マネーのまなび | テック | 国際 | スポーツ | 社会・調査 | 地域 | 文化 | ライフスタイル | - | ---- | ---------- | ------- | -------- | --------- | ---------- | -------- | -------------- | ---------- | ------------- | -------- | ---------- | ----- | ------- | -------------- | - | news | opinion | economy | politics | financial | business | 不支持 | 不支持 | technology | international | sports | society | local | culture | lifestyle |`, +| ---- | ---------- | ------- | -------- | --------- | ---------- | -------- | -------------- | ---------- | ------------- | -------- | ---------- | ----- | ------- | -------------- | +| news | opinion | economy | politics | financial | business | 不支持 | 不支持 | technology | international | sports | society | local | culture | lifestyle |`, }; async function handler(ctx) { diff --git a/lib/routes/nintendo/namespace.ts b/lib/routes/nintendo/namespace.ts index 99783728bf0ad4..592294664592b2 100644 --- a/lib/routes/nintendo/namespace.ts +++ b/lib/routes/nintendo/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Nintendo', url: 'nintendo.com', + lang: 'en', }; diff --git a/lib/routes/nippon/index.ts b/lib/routes/nippon/index.ts index 3e0e9485aa7833..f66fe927f3d7b3 100644 --- a/lib/routes/nippon/index.ts +++ b/lib/routes/nippon/index.ts @@ -23,8 +23,8 @@ export const route: Route = { ], name: '政治外交', description: `| 政治 | 经济 | 社会 | 展览预告 | 焦点专题 | 深度报道 | 话题 | 日本信息库 | 日本一蹩 | 人物访谈 | 编辑部通告 | - | -------- | ------- | ------- | -------- | ------------------ | -------- | ------------ | ---------- | ------------- | -------- | ------------- | - | Politics | Economy | Society | Culture | Science,Technology | In-depth | japan-topics | japan-data | japan-glances | People | Announcements |`, +| -------- | ------- | ------- | -------- | ------------------ | -------- | ------------ | ---------- | ------------- | -------- | ------------- | +| Politics | Economy | Society | Culture | Science,Technology | In-depth | japan-topics | japan-data | japan-glances | People | Announcements |`, maintainers: ['laampui'], handler, }; diff --git a/lib/routes/nippon/namespace.ts b/lib/routes/nippon/namespace.ts index 43e0269f972c26..fbfdb56682aaa8 100644 --- a/lib/routes/nippon/namespace.ts +++ b/lib/routes/nippon/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '走进日本', url: 'www.nippon.com', + lang: 'zh-CN', }; diff --git a/lib/routes/njglyy/namespace.ts b/lib/routes/njglyy/namespace.ts index 242cb9f1c1d3d1..5ea5a3cf68ae9e 100644 --- a/lib/routes/njglyy/namespace.ts +++ b/lib/routes/njglyy/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京鼓楼医院', url: 'njglyy.com', + lang: 'zh-CN', }; diff --git a/lib/routes/njit/namespace.ts b/lib/routes/njit/namespace.ts index 08f0e64bb7f004..d1cdfe0ebe9099 100644 --- a/lib/routes/njit/namespace.ts +++ b/lib/routes/njit/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京工程学院', url: 'jwc.njit.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/njnu/ceai/ceai.ts b/lib/routes/njnu/ceai/ceai.ts index 8e059bb9b6804b..031be1dd3dead2 100644 --- a/lib/routes/njnu/ceai/ceai.ts +++ b/lib/routes/njnu/ceai/ceai.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['Shujakuinkuraudo'], handler, description: `| 学院公告 | 学院新闻 | 学生资讯 | - | -------- | -------- | -------- | - | xygg | xyxw | xszx |`, +| -------- | -------- | -------- | +| xygg | xyxw | xszx |`, }; async function handler(ctx) { diff --git a/lib/routes/njnu/jwc/jwc.ts b/lib/routes/njnu/jwc/jwc.ts index e67b8aed71ef27..00b65edc37a647 100644 --- a/lib/routes/njnu/jwc/jwc.ts +++ b/lib/routes/njnu/jwc/jwc.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['Shujakuinkuraudo'], handler, description: `| 教师通知 | 新闻动态 | 学生通知 | - | -------- | -------- | -------- | - | jstz | xwdt | xstz |`, +| -------- | -------- | -------- | +| jstz | xwdt | xstz |`, }; async function handler(ctx) { diff --git a/lib/routes/njnu/namespace.ts b/lib/routes/njnu/namespace.ts index 2f4434beacbb75..a42fcf4f911e07 100644 --- a/lib/routes/njnu/namespace.ts +++ b/lib/routes/njnu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京师范大学', url: 'ceai.njnu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nju/exchangesys.ts b/lib/routes/nju/exchangesys.ts index baabbf874780a7..8583be71bfc830 100644 --- a/lib/routes/nju/exchangesys.ts +++ b/lib/routes/nju/exchangesys.ts @@ -19,8 +19,8 @@ export const route: Route = { maintainers: [], handler, description: `| 新闻通知 | 交换生项目 | - | -------- | ---------- | - | news | proj |`, +| -------- | ---------- | +| news | proj |`, }; async function handler(ctx) { diff --git a/lib/routes/nju/jw.ts b/lib/routes/nju/jw.ts index 341ec261b133e4..c5f200ce28c239 100644 --- a/lib/routes/nju/jw.ts +++ b/lib/routes/nju/jw.ts @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['cqjjjzr'], handler, description: `| 公告通知 | 教学动态 | - | -------- | -------- | - | ggtz | jxdt |`, +| -------- | -------- | +| ggtz | jxdt |`, }; async function handler(ctx) { diff --git a/lib/routes/nju/namespace.ts b/lib/routes/nju/namespace.ts index 187fdd60c797bb..be6d6ef49d304a 100644 --- a/lib/routes/nju/namespace.ts +++ b/lib/routes/nju/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京大学', url: 'admission.nju.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nju/rczp.ts b/lib/routes/nju/rczp.ts index bbfe922b8faf70..74f3762e5d276f 100644 --- a/lib/routes/nju/rczp.ts +++ b/lib/routes/nju/rczp.ts @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['ret-1'], handler, description: `| 信息发布 | 教研类岗位 | 管理岗位及其他 | - | -------- | ---------- | -------------- | - | xxfb | jylgw | gllgw |`, +| -------- | ---------- | -------------- | +| xxfb | jylgw | gllgw |`, }; async function handler(ctx) { diff --git a/lib/routes/nju/scit.ts b/lib/routes/nju/scit.ts index a31b97116ce6c8..9d944dc549c591 100644 --- a/lib/routes/nju/scit.ts +++ b/lib/routes/nju/scit.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['ret-1'], handler, description: `| 通知公告 | 科研动态 | - | -------- | -------- | - | tzgg | kydt |`, +| -------- | -------- | +| tzgg | kydt |`, }; async function handler(ctx) { diff --git a/lib/routes/nju/zbb.ts b/lib/routes/nju/zbb.ts index d8fdf49f7b8762..5ed81ad745a2b3 100644 --- a/lib/routes/nju/zbb.ts +++ b/lib/routes/nju/zbb.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['ret-1'], handler, description: `| 采购信息 | 成交公示 | 政府采购意向公开 | - | -------- | -------- | ---------------- | - | cgxx | cjgs | zfcgyxgk |`, +| -------- | -------- | ---------------- | +| cgxx | cjgs | zfcgyxgk |`, }; async function handler(ctx) { diff --git a/lib/routes/njucm/namespace.ts b/lib/routes/njucm/namespace.ts index c777e9830d084f..f6bcaa33454647 100644 --- a/lib/routes/njucm/namespace.ts +++ b/lib/routes/njucm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京中医药大学', url: 'lib.njucm.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/njuferret/namespace.ts b/lib/routes/njuferret/namespace.ts index b566c270318267..91e940de52330d 100644 --- a/lib/routes/njuferret/namespace.ts +++ b/lib/routes/njuferret/namespace.ts @@ -7,4 +7,5 @@ export const namespace: Namespace = { zh: { name: '有点博客', }, + lang: 'zh-CN', }; diff --git a/lib/routes/njupt/jwc.ts b/lib/routes/njupt/jwc.ts index 73d0e7a91f57d4..4e9cd0329facf5 100644 --- a/lib/routes/njupt/jwc.ts +++ b/lib/routes/njupt/jwc.ts @@ -28,8 +28,8 @@ export const route: Route = { maintainers: ['shaoye'], handler, description: `| 通知公告 | 教务快讯 | - | -------- | -------- | - | notice | news |`, +| -------- | -------- | +| notice | news |`, }; async function handler(ctx) { diff --git a/lib/routes/njupt/namespace.ts b/lib/routes/njupt/namespace.ts index 2316f3939d6195..02175fda0479dc 100644 --- a/lib/routes/njupt/namespace.ts +++ b/lib/routes/njupt/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京邮电大学', url: 'jwc.njupt.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/njust/cwc.ts b/lib/routes/njust/cwc.ts index 6d211c8bbf52cb..85f80effc1c685 100644 --- a/lib/routes/njust/cwc.ts +++ b/lib/routes/njust/cwc.ts @@ -29,8 +29,8 @@ export const route: Route = { maintainers: ['MilkShakeYoung', 'jasongzy'], handler, description: `| 通知公告 | 办事流程 | - | -------- | -------- | - | tzgg | bslc |`, +| -------- | -------- | +| tzgg | bslc |`, }; async function handler(ctx) { diff --git a/lib/routes/njust/dgxg.ts b/lib/routes/njust/dgxg.ts index c568ba9877cf2e..d93459b3f67091 100644 --- a/lib/routes/njust/dgxg.ts +++ b/lib/routes/njust/dgxg.ts @@ -30,8 +30,8 @@ export const route: Route = { maintainers: ['jasongzy'], handler, description: `| 公示通知 | 学术文化 | 就业指导 | - | -------- | -------- | -------- | - | gstz | xswh | jyzd |`, +| -------- | -------- | -------- | +| gstz | xswh | jyzd |`, }; async function handler(ctx) { diff --git a/lib/routes/njust/eo.ts b/lib/routes/njust/eo.ts index 3039cc44110667..3cf4a52056478d 100644 --- a/lib/routes/njust/eo.ts +++ b/lib/routes/njust/eo.ts @@ -38,15 +38,15 @@ export const route: Route = { handler, description: `\`grade\` 列表: - | 本科 2016 级 | 本科 2017 级 | 本科 2018 级 | 本科 2019 级 | - | ------------ | ------------ | ------------ | ------------ | - | 16 | 17 | 18 | 19 | +| 本科 2016 级 | 本科 2017 级 | 本科 2018 级 | 本科 2019 级 | +| ------------ | ------------ | ------------ | ------------ | +| 16 | 17 | 18 | 19 | \`type\` 列表: - | 年级通知(通知公告) | 每日动态(主任寄语) | - | -------------------- | -------------------- | - | tz | dt |`, +| 年级通知(通知公告) | 每日动态(主任寄语) | +| -------------------- | -------------------- | +| tz | dt |`, }; async function handler(ctx) { diff --git a/lib/routes/njust/eoe.ts b/lib/routes/njust/eoe.ts index 231f92263e84ac..a20455b93cb4da 100644 --- a/lib/routes/njust/eoe.ts +++ b/lib/routes/njust/eoe.ts @@ -29,8 +29,8 @@ export const route: Route = { maintainers: ['jasongzy'], handler, description: `| 通知公告 | 新闻动态 | - | -------- | -------- | - | tzgg | xwdt |`, +| -------- | -------- | +| tzgg | xwdt |`, }; async function handler(ctx) { diff --git a/lib/routes/njust/gs.ts b/lib/routes/njust/gs.ts index 122657b46651d8..40fd55027fb5a8 100644 --- a/lib/routes/njust/gs.ts +++ b/lib/routes/njust/gs.ts @@ -29,8 +29,8 @@ export const route: Route = { maintainers: ['MilkShakeYoung', 'jasongzy'], handler, description: `| 首页通知公告 | 首页新闻动态 | 最新通知 | 招生信息 | 培养信息 | 学术活动 | - | ------------ | ------------ | -------- | -------- | -------- | -------- | - | sytzgg\_4568 | sytzgg | 14686 | 14687 | 14688 | xshdggl |`, +| ------------ | ------------ | -------- | -------- | -------- | -------- | +| sytzgg\_4568 | sytzgg | 14686 | 14687 | 14688 | xshdggl |`, }; async function handler(ctx) { diff --git a/lib/routes/njust/jwc.ts b/lib/routes/njust/jwc.ts index bbe175fe0631b8..b096502080626b 100644 --- a/lib/routes/njust/jwc.ts +++ b/lib/routes/njust/jwc.ts @@ -31,8 +31,8 @@ export const route: Route = { maintainers: ['MilkShakeYoung', 'jasongzy'], handler, description: `| 教师通知 | 学生通知 | 新闻 | 学院动态 | - | -------- | -------- | ---- | -------- | - | jstz | xstz | xw | xydt |`, +| -------- | -------- | ---- | -------- | +| jstz | xstz | xw | xydt |`, }; async function handler(ctx) { diff --git a/lib/routes/njust/namespace.ts b/lib/routes/njust/namespace.ts index 39ef6dab434d25..c0ec205488426e 100644 --- a/lib/routes/njust/namespace.ts +++ b/lib/routes/njust/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京理工大学', url: 'jwc.njust.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/njust/utils.ts b/lib/routes/njust/utils.ts index 29371a7d62c510..059210f6668b2c 100644 --- a/lib/routes/njust/utils.ts +++ b/lib/routes/njust/utils.ts @@ -23,7 +23,7 @@ async function getContent(url, pptr = false) { const content = await page.content(); return content; } finally { - browser.close(); + await browser.close(); } } else { const response = await got(url); diff --git a/lib/routes/njxzc/namespace.ts b/lib/routes/njxzc/namespace.ts index 7d8b373051838d..98e099e9e9b07c 100644 --- a/lib/routes/njxzc/namespace.ts +++ b/lib/routes/njxzc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京晓庄学院', url: 'lib.njxzc.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nlc/namespace.ts b/lib/routes/nlc/namespace.ts index 7165ef0e7b0c15..cb3e2e662bc5f4 100644 --- a/lib/routes/nlc/namespace.ts +++ b/lib/routes/nlc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国国家图书馆', url: 'read.nlc.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nlc/read.ts b/lib/routes/nlc/read.ts index a7430d10e8de41..1e4483211dbc8a 100644 --- a/lib/routes/nlc/read.ts +++ b/lib/routes/nlc/read.ts @@ -21,10 +21,10 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| [电子图书](http://read.nlc.cn/outRes/outResList?type=电子图书) | [电子期刊](http://read.nlc.cn/outRes/outResList?type=电子期刊) | [电子论文](http://read.nlc.cn/outRes/outResList?type=电子论文) | [电子报纸](http://read.nlc.cn/outRes/outResList?type=电子报纸) | [音视频](http://read.nlc.cn/outRes/outResList?type=音视频) | - | -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | +| -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | - | [标准专利](http://read.nlc.cn/outRes/outResList?type=标准专利) | [工具书](http://read.nlc.cn/outRes/outResList?type=工具书) | [少儿资源](http://read.nlc.cn/outRes/outResList?type=少儿资源) | - | -------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------- |`, +| [标准专利](http://read.nlc.cn/outRes/outResList?type=标准专利) | [工具书](http://read.nlc.cn/outRes/outResList?type=工具书) | [少儿资源](http://read.nlc.cn/outRes/outResList?type=少儿资源) | +| -------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------- |`, }; async function handler(ctx) { diff --git a/lib/routes/nltimes/namespace.ts b/lib/routes/nltimes/namespace.ts index 39a69ecc1170f7..1fb782e8d9fe6a 100644 --- a/lib/routes/nltimes/namespace.ts +++ b/lib/routes/nltimes/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'NL Times', url: 'nltimes.nl', + lang: 'en', }; diff --git a/lib/routes/nltimes/news.ts b/lib/routes/nltimes/news.ts index bad23e78393cee..6e722a98178127 100644 --- a/lib/routes/nltimes/news.ts +++ b/lib/routes/nltimes/news.ts @@ -41,8 +41,8 @@ export const route: Route = { maintainers: ['Hivol'], handler, description: `| Top Stories (default) | Health | Crime | Politics | Business | Tech | Culture | Sports | Weird | 1-1-2 | - | --------------------- | ------ | ----- | -------- | -------- | ---- | ------- | ------ | ----- | ----- | - | top-stories | health | crime | politics | business | tech | culture | sports | weird | 1-1-2 |`, +| --------------------- | ------ | ----- | -------- | -------- | ---- | ------- | ------ | ----- | ----- | +| top-stories | health | crime | politics | business | tech | culture | sports | weird | 1-1-2 |`, }; async function handler(ctx) { diff --git a/lib/routes/nmc/namespace.ts b/lib/routes/nmc/namespace.ts index 492740c8b067c1..0b96a0384ef3c8 100644 --- a/lib/routes/nmc/namespace.ts +++ b/lib/routes/nmc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中央气象台', url: 'nmc.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nmc/weatheralarm.ts b/lib/routes/nmc/weatheralarm.ts index f56b5bb97393e7..e568606b4059ca 100644 --- a/lib/routes/nmc/weatheralarm.ts +++ b/lib/routes/nmc/weatheralarm.ts @@ -69,6 +69,7 @@ async function handler(ctx) { return { title: '中央气象台全国气象预警', link: 'http://www.nmc.cn/publish/alarm.html', + allowEmpty: true, item: items, }; } diff --git a/lib/routes/nmtv/column.ts b/lib/routes/nmtv/column.ts index 9521802aef00d6..5a9c8589a79da3 100644 --- a/lib/routes/nmtv/column.ts +++ b/lib/routes/nmtv/column.ts @@ -24,9 +24,9 @@ export const route: Route = { name: '点播', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 如 [蒙古语卫视新闻联播](http://www.nmtv.cn/folder292/folder663/folder301/folder830/folder877) 的 URL 为 \`http://www.nmtv.cn/folder292/folder663/folder301/folder830/folder877\`,其栏目 id 为末尾数字编号,即 \`877\`。可以得到其对应路由为 [\`/nmtv/column/877\`](https://rsshub.app/nmtv/column/877) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/nmtv/namespace.ts b/lib/routes/nmtv/namespace.ts index 097d9fc4cb0038..be61a9222d9bb4 100644 --- a/lib/routes/nmtv/namespace.ts +++ b/lib/routes/nmtv/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '内蒙古广播电视台', url: 'nmtv.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nodejs/blog.ts b/lib/routes/nodejs/blog.ts index 4153209e3fea13..54b5340d133837 100644 --- a/lib/routes/nodejs/blog.ts +++ b/lib/routes/nodejs/blog.ts @@ -25,21 +25,23 @@ export const route: Route = { name: 'News', maintainers: ['nczitzk'], handler, - description: `| العربية | Catalan | Deutsch | Español | زبان فارسی | - | ------- | ------- | ------- | ------- | ---------- | - | ar | ca | de | es | fa | + description: `Official RSS Source: https://nodejs.org/en/feed/blog.xml - | Français | Galego | Italiano | 日本語 | 한국어 | - | -------- | ------ | -------- | ------ | ------ | - | fr | gl | it | ja | ko | +| العربية | Catalan | Deutsch | Español | زبان فارسی | +| ------- | ------- | ------- | ------- | ---------- | +| ar | ca | de | es | fa | - | Português do Brasil | limba română | Русский | Türkçe | Українська | - | ------------------- | ------------ | ------- | ------ | ---------- | - | pt-br | ro | ru | tr | uk | +| Français | Galego | Italiano | 日本語 | 한국어 | +| -------- | ------ | -------- | ------ | ------ | +| fr | gl | it | ja | ko | - | 简体中文 | 繁體中文 | - | -------- | -------- | - | zh-cn | zh-tw |`, +| Português do Brasil | limba română | Русский | Türkçe | Українська | +| ------------------- | ------------ | ------- | ------ | ---------- | +| pt-br | ro | ru | tr | uk | + +| 简体中文 | 繁體中文 | +| -------- | -------- | +| zh-cn | zh-tw |`, }; async function handler(ctx) { @@ -55,16 +57,22 @@ async function handler(ctx) { const $ = load(response.data); - $('.summary').remove(); - - let items = $('ul.blog-index li a') + let items = $('article') .toArray() - .map((item) => { - item = $(item); + .map((article) => { + const $article = load(article); + + const author = $article('footer p').text(); + const pubDate = parseDate($article('footer time').attr('datetime')); + + const title = $article('a[aria-label]').prop('aria-label'); + const href = $article('a[aria-label]').attr('href'); return { - title: item.text(), - link: `${rootUrl}${item.attr('href')}`, + title, + link: `${rootUrl}${href}`, + author, + pubDate, }; }); @@ -76,17 +84,9 @@ async function handler(ctx) { url: item.link, }); - const content = load(detailResponse.data); - - item.pubDate = parseDate(content('time').attr('datetime')); - item.author = content('.blogpost-meta') - .text() - .match(/by (.*), /)?.[1]; - - content('.blogpost-header').remove(); - - item.description = content('article').html(); + const $content = load(detailResponse.data); + item.description = $content('main').html(); return item; }) ) diff --git a/lib/routes/nodejs/namespace.ts b/lib/routes/nodejs/namespace.ts index 8a845ce4708304..4a5df9be0159e1 100644 --- a/lib/routes/nodejs/namespace.ts +++ b/lib/routes/nodejs/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Node.js', url: 'nodejs.org', + lang: 'en', }; diff --git a/lib/routes/nogizaka46/blog.ts b/lib/routes/nogizaka46/blog.ts index fd320f85911a32..1d42cad8c7658b 100644 --- a/lib/routes/nogizaka46/blog.ts +++ b/lib/routes/nogizaka46/blog.ts @@ -29,48 +29,48 @@ export const route: Route = { url: 'blog.nogizaka46.com/s/n46/diary/MEMBER', description: `Member ID - | Member ID | Name | - | --------- | --------------------- | - | 55401 | 岡本 姫奈 | - | 55400 | 川﨑 桜 | - | 55397 | 池田 瑛紗 | - | 55396 | 五百城 茉央 | - | 55395 | 中西 アルノ | - | 55394 | 奥田 いろは | - | 55393 | 冨里 奈央 | - | 55392 | 小川 彩 | - | 55391 | 菅原 咲月 | - | 55390 | 一ノ瀬 美空 | - | 55389 | 井上 和 | - | 55387 | 弓木 奈於 | - | 55386 | 松尾 美佑 | - | 55385 | 林 瑠奈 | - | 55384 | 佐藤 璃果 | - | 55383 | 黒見 明香 | - | 48014 | 清宮 レイ | - | 48012 | 北川 悠理 | - | 48010 | 金川 紗耶 | - | 48019 | 矢久保 美緒 | - | 48018 | 早川 聖来 | - | 48009 | 掛橋 沙耶香 | - | 48008 | 賀喜 遥香 | - | 48017 | 筒井 あやめ | - | 48015 | 田村 真佑 | - | 48013 | 柴田 柚菜 | - | 48006 | 遠藤 さくら | - | 36760 | 与田 祐希 | - | 36759 | 吉田 綾乃クリスティー | - | 36758 | 山下 美月 | - | 36757 | 向井 葉月 | - | 36756 | 中村 麗乃 | - | 36755 | 佐藤 楓 | - | 36754 | 阪口 珠美 | - | 36753 | 久保 史緒里 | - | 36752 | 大園 桃子 | - | 36751 | 梅澤 美波 | - | 36750 | 岩本 蓮加 | - | 36749 | 伊藤 理々杏 | - | 264 | 齋藤 飛鳥 |`, +| Member ID | Name | +| --------- | --------------------- | +| 55401 | 岡本 姫奈 | +| 55400 | 川﨑 桜 | +| 55397 | 池田 瑛紗 | +| 55396 | 五百城 茉央 | +| 55395 | 中西 アルノ | +| 55394 | 奥田 いろは | +| 55393 | 冨里 奈央 | +| 55392 | 小川 彩 | +| 55391 | 菅原 咲月 | +| 55390 | 一ノ瀬 美空 | +| 55389 | 井上 和 | +| 55387 | 弓木 奈於 | +| 55386 | 松尾 美佑 | +| 55385 | 林 瑠奈 | +| 55384 | 佐藤 璃果 | +| 55383 | 黒見 明香 | +| 48014 | 清宮 レイ | +| 48012 | 北川 悠理 | +| 48010 | 金川 紗耶 | +| 48019 | 矢久保 美緒 | +| 48018 | 早川 聖来 | +| 48009 | 掛橋 沙耶香 | +| 48008 | 賀喜 遥香 | +| 48017 | 筒井 あやめ | +| 48015 | 田村 真佑 | +| 48013 | 柴田 柚菜 | +| 48006 | 遠藤 さくら | +| 36760 | 与田 祐希 | +| 36759 | 吉田 綾乃クリスティー | +| 36758 | 山下 美月 | +| 36757 | 向井 葉月 | +| 36756 | 中村 麗乃 | +| 36755 | 佐藤 楓 | +| 36754 | 阪口 珠美 | +| 36753 | 久保 史緒里 | +| 36752 | 大園 桃子 | +| 36751 | 梅澤 美波 | +| 36750 | 岩本 蓮加 | +| 36749 | 伊藤 理々杏 | +| 264 | 齋藤 飛鳥 |`, }; async function handler(ctx) { diff --git a/lib/routes/nogizaka46/namespace.ts b/lib/routes/nogizaka46/namespace.ts index c22f9f1c020057..6ed98591cfe57f 100644 --- a/lib/routes/nogizaka46/namespace.ts +++ b/lib/routes/nogizaka46/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Sakamichi Series 坂道系列官网资讯', url: 'news.nogizaka46.com', + lang: 'zh-CN', }; diff --git a/lib/routes/nosec/index.ts b/lib/routes/nosec/index.ts index 10e0b6bab7a4d3..4b69098a536aef 100644 --- a/lib/routes/nosec/index.ts +++ b/lib/routes/nosec/index.ts @@ -21,15 +21,15 @@ export const route: Route = { parameters: { keykind: '对应文章分类' }, name: 'Posts', maintainers: ['hellodword'], - description: ` | 分类 | 标识 | - | :------- | :--------- | - | 威胁情报 | \`threaten\` | - | 安全动态 | \`security\` | - | 漏洞预警 | \`hole\` | - | 数据泄露 | \`leakage\` | - | 专题报告 | \`speech\` | - | 技术分析 | \`skill\` | - | 安全工具 | \`tool\` |`, + description: `| 分类 | 标识 | +| :------- | :--------- | +| 威胁情报 | \`threaten\` | +| 安全动态 | \`security\` | +| 漏洞预警 | \`hole\` | +| 数据泄露 | \`leakage\` | +| 专题报告 | \`speech\` | +| 技术分析 | \`skill\` | +| 安全工具 | \`tool\` |`, handler, radar: [ { diff --git a/lib/routes/nosec/namespace.ts b/lib/routes/nosec/namespace.ts index d00b7bf400a40c..a057fcb2a5bca6 100644 --- a/lib/routes/nosec/namespace.ts +++ b/lib/routes/nosec/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'NOSEC 安全讯息平台', url: 'nosec.org', + lang: 'zh-CN', }; diff --git a/lib/routes/notateslaapp/namespace.ts b/lib/routes/notateslaapp/namespace.ts index 383dc416ba48b7..a45cfe8d7148f0 100644 --- a/lib/routes/notateslaapp/namespace.ts +++ b/lib/routes/notateslaapp/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Not a Tesla App', url: 'notateslaapp.com', + lang: 'en', }; diff --git a/lib/routes/notefolio/namespace.ts b/lib/routes/notefolio/namespace.ts index 4c6d914709f66a..3568fce38434da 100644 --- a/lib/routes/notefolio/namespace.ts +++ b/lib/routes/notefolio/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Notefolio', url: 'notefolio.net', + lang: 'en', }; diff --git a/lib/routes/notefolio/search.ts b/lib/routes/notefolio/search.ts index 6114db87408084..f9bcbb70059aa4 100644 --- a/lib/routes/notefolio/search.ts +++ b/lib/routes/notefolio/search.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); @@ -70,12 +70,49 @@ const categoryMap = [ */ export const route: Route = { path: '/search/:category?/:order?/:time?/:query?', - categories: ['design'], + categories: ['design', 'popular'], + view: ViewType.Pictures, example: '/notefolio/search/1/pick/all/life', parameters: { - category: 'Category, see below, `all` by default', - order: 'Order, `pick` as Notefolio Pick, `published` as Newest, `like` as like, `pick` by default', - time: 'Time, `all` as All the time, `one-day` as Latest 24 hours, `week` as Latest week, `month` as Latest month, `three-month` as Latest 3 months, `all` by default', + category: { + description: 'Category, see below', + options: [ + { value: 'all', label: 'All (전체)' }, + { value: '1', label: 'Video / Motion Graphics (영상/모션그래픽)' }, + { value: '2', label: 'Graphic Design (그래픽 디자인)' }, + { value: '3', label: 'Branding / Editing (브랜딩/편집)' }, + { value: '4', label: 'UI/UX (UI/UX)' }, + { value: '5', label: 'Illustration (일러스트레이션)' }, + { value: '6', label: 'Digital Art (디지털 아트)' }, + { value: '7', label: 'Character Design (캐릭터 디자인)' }, + { value: '8', label: 'Product Package Design (제품/패키지 디자인)' }, + { value: '9', label: 'Photography (포토그래피)' }, + { value: '10', label: 'Typography (타이포그래피)' }, + { value: '11', label: 'Crafts (공예)' }, + { value: '12', label: 'Fine Art (파인아트)' }, + ], + default: 'all', + }, + order: { + description: 'Order, `pick` as Notefolio Pick, `published` as Newest, `like` as like, `pick` by default', + options: [ + { value: 'pick', label: 'Notefolio Pick' }, + { value: 'published', label: 'Newest' }, + { value: 'like', label: 'Like' }, + ], + default: 'pick', + }, + time: { + description: 'Time', + options: [ + { value: 'all', label: 'All the time' }, + { value: 'one-day', label: 'Latest 24 hours' }, + { value: 'week', label: 'Latest week' }, + { value: 'month', label: 'Latest month' }, + { value: 'three-month', label: 'Latest 3 months' }, + ], + default: 'all', + }, query: 'Keyword, empty by default', }, features: { @@ -96,20 +133,20 @@ export const route: Route = { handler, url: 'notefolio.net/search', description: `| Category | Name in Korean | Name in English | - | -------- | ------------------ | ----------------------- | - | all | 전체 | All | - | 1 | 영상/모션그래픽 | Video / Motion Graphics | - | 2 | 그래픽 디자인 | Graphic Design | - | 3 | 브랜딩/편집 | Branding / Editing | - | 4 | UI/UX | UI/UX | - | 5 | 일러스트레이션 | Illustration | - | 6 | 디지털 아트 | Digital Art | - | 7 | 캐릭터 디자인 | Character Design | - | 8 | 제품/패키지 디자인 | Product Package Design | - | 9 | 포토그래피 | Photography | - | 10 | 타이포그래피 | Typography | - | 11 | 공예 | Crafts | - | 12 | 파인아트 | Fine Art |`, +| -------- | ------------------ | ----------------------- | +| all | 전체 | All | +| 1 | 영상/모션그래픽 | Video / Motion Graphics | +| 2 | 그래픽 디자인 | Graphic Design | +| 3 | 브랜딩/편집 | Branding / Editing | +| 4 | UI/UX | UI/UX | +| 5 | 일러스트레이션 | Illustration | +| 6 | 디지털 아트 | Digital Art | +| 7 | 캐릭터 디자인 | Character Design | +| 8 | 제품/패키지 디자인 | Product Package Design | +| 9 | 포토그래피 | Photography | +| 10 | 타이포그래피 | Typography | +| 11 | 공예 | Crafts | +| 12 | 파인아트 | Fine Art |`, }; async function handler(ctx) { diff --git a/lib/routes/notion/namespace.ts b/lib/routes/notion/namespace.ts index 2910f91ad7956a..9b7ab25f0594da 100644 --- a/lib/routes/notion/namespace.ts +++ b/lib/routes/notion/namespace.ts @@ -10,4 +10,5 @@ Need to set up Notion integration, please refer to [Route-specific Configuration :::tip Recommendation It is recommended to use with clipping tools such as Notion Web Clipper. :::`, + lang: 'en', }; diff --git a/lib/routes/notion/release.ts b/lib/routes/notion/release.ts new file mode 100644 index 00000000000000..840afbc68b495b --- /dev/null +++ b/lib/routes/notion/release.ts @@ -0,0 +1,81 @@ +import type { DataItem, Route } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import day from 'dayjs'; + +const handler: Route['handler'] = async () => { + const data = await ofetch('https://notion.so/releases', { + headers: { + 'Accept-Language': 'en-US', // TODO accept param + }, + }); + + const $ = load(data); + + // the first post, do not cache + const title = $('h2').first().text() ?? ''; + const pubDate = parseDate($('time').first().text()); + const description = $('article.release article').first().html() ?? ''; + const link = `https://notion.so/releases/${day(pubDate).format('YYYY-MM-DD')}`; + + // archive + const item = (await Promise.all( + $('div[class^="releasePreviewsSection"] h3 a[href^="/releases/"]') + .toArray() + .slice(0, 5) + .map((item) => { + const link = `https://notion.so${item.attribs.href}`; + + return cache.tryGet(`notion:release:${link}`, async () => { + const data = await ofetch(link, { + headers: { + 'Accept-Language': 'en-US', // Notion will adjust returned content based on this header + }, + }); + + const $ = load(data); + + return { + title: $('h2').first().text() ?? '', + pubDate: parseDate($('time').first().text()), + description: $('article.release article').first().html() ?? '', + link, + }; + }); + }) + )) as DataItem[]; + + return { + title: 'Notion Releases', + link: 'https://notion.so/releases', + item: [ + { + title, + description, + pubDate, + link, + }, + ...item, + ], + }; +}; + +export const route: Route = { + name: 'Release', + path: '/release', + url: 'notion.so/releases', + example: '/notion/release', + categories: ['program-update'], + maintainers: ['equt'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + handler, +}; diff --git a/lib/routes/now/namespace.ts b/lib/routes/now/namespace.ts index f38a66d6b34615..001d3fddb1b5e5 100644 --- a/lib/routes/now/namespace.ts +++ b/lib/routes/now/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Now 新聞', url: 'news.now.com', + lang: 'zh-TW', }; diff --git a/lib/routes/now/news.ts b/lib/routes/now/news.ts index d2bad419a6776f..cef675f489a4b9 100644 --- a/lib/routes/now/news.ts +++ b/lib/routes/now/news.ts @@ -32,23 +32,23 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'news.now.com/', - description: `:::tip + description: `::: tip **编号** 仅对事件追蹤、評論節目、新聞專題三个分类起作用,例子如下: 对于 [事件追蹤](https://news.now.com/home/tracker) 中的 [塔利班奪權](https://news.now.com/home/tracker/detail?catCode=123\&topicId=1056) 话题,其网址为 \`https://news.now.com/home/tracker/detail?catCode=123&topicId=1056\`,其中 \`topicId\` 为 1056,则对应路由为 [\`/now/news/tracker/1056\`](https://rsshub.app/now/news/tracker/1056) - ::: +::: - | 首頁 | 港聞 | 兩岸國際 | 娛樂 | - | ---- | ----- | ------------- | ------------- | - | | local | international | entertainment | +| 首頁 | 港聞 | 兩岸國際 | 娛樂 | +| ---- | ----- | ------------- | ------------- | +| | local | international | entertainment | - | 生活 | 科技 | 財經 | 體育 | - | ---- | ---------- | ------- | ------ | - | life | technology | finance | sports | +| 生活 | 科技 | 財經 | 體育 | +| ---- | ---------- | ------- | ------ | +| life | technology | finance | sports | - | 事件追蹤 | 評論節目 | 新聞專題 | - | -------- | -------- | -------- | - | tracker | feature | opinion |`, +| 事件追蹤 | 評論節目 | 新聞專題 | +| -------- | -------- | -------- | +| tracker | feature | opinion |`, }; async function handler(ctx) { diff --git a/lib/routes/nowcoder/discuss.ts b/lib/routes/nowcoder/discuss.ts index 58e5bedd77cb35..1e9e82040910fb 100644 --- a/lib/routes/nowcoder/discuss.ts +++ b/lib/routes/nowcoder/discuss.ts @@ -24,8 +24,8 @@ export const route: Route = { maintainers: ['LogicJake'], handler, description: `| 最新回复 | 最新发表 | 最新 | 精华 | - | -------- | -------- | ---- | ---- | - | 0 | 3 | 1 | 4 |`, +| -------- | -------- | ---- | ---- | +| 0 | 3 | 1 | 4 |`, }; async function handler(ctx) { diff --git a/lib/routes/nowcoder/jobcenter.ts b/lib/routes/nowcoder/jobcenter.ts index cb66b9e23de4cb..9a7b953f4ec2a4 100644 --- a/lib/routes/nowcoder/jobcenter.ts +++ b/lib/routes/nowcoder/jobcenter.ts @@ -37,15 +37,15 @@ export const route: Route = { 职位类型代码见下表: - | 研发 | 测试 | 数据 | 算法 | 前端 | 产品 | 运营 | 其他 | - | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | - | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 0 | +| 研发 | 测试 | 数据 | 算法 | 前端 | 产品 | 运营 | 其他 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 0 | 排序参数见下表: - | 最新发布 | 最快处理 | 处理率最高 | - | -------- | -------- | ---------- | - | 1 | 2 | 3 |`, +| 最新发布 | 最快处理 | 处理率最高 | +| -------- | -------- | ---------- | +| 1 | 2 | 3 |`, }; async function handler(ctx) { diff --git a/lib/routes/nowcoder/namespace.ts b/lib/routes/nowcoder/namespace.ts index bf41b34e76cc1f..5c4c15f4c0b2a7 100644 --- a/lib/routes/nowcoder/namespace.ts +++ b/lib/routes/nowcoder/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '牛客网', url: 'nowcoder.com', + lang: 'zh-CN', }; diff --git a/lib/routes/npm/namespace.ts b/lib/routes/npm/namespace.ts index f4b4fc058890b8..527022cfca7073 100644 --- a/lib/routes/npm/namespace.ts +++ b/lib/routes/npm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'NPM', url: 'npmjs.com', + lang: 'en', }; diff --git a/lib/routes/npm/package.ts b/lib/routes/npm/package.ts index 06191502475d66..9f2395ebede574 100644 --- a/lib/routes/npm/package.ts +++ b/lib/routes/npm/package.ts @@ -2,14 +2,21 @@ 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 { art } from '@/utils/render'; import path from 'node:path'; export const route: Route = { - path: 'package/:name{(@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*}', - name: 'Unknown', - maintainers: [], + path: '/package/:name{(@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*}', + name: 'Package', + maintainers: ['Fatpandac'], + categories: ['program-update'], + example: '/npm/package/rsshub', + radar: [ + { + source: ['www.npmjs.com/package/:name'], + }, + ], handler, }; @@ -20,10 +27,10 @@ async function handler(ctx) { const packageDownloadLastDayAPI = `https://api.npmjs.org/downloads/point/last-day/${name}`; // 按天统计 const packageVersionAPI = `https://registry.npmjs.org/${name}`; // 包基本信息 - const downloadCountLastMonthRes = await got(packageDownloadLastMonthAPI).json(); - const downloadCountLastWeekRes = await got(packageDownloadLastWeekAPI).json(); - const downloadCountLastDayRes = await got(packageDownloadLastDayAPI).json(); - const packageVersionRes = await got(packageVersionAPI).json(); + const downloadCountLastMonthRes = await ofetch(packageDownloadLastMonthAPI); + const downloadCountLastWeekRes = await ofetch(packageDownloadLastWeekAPI); + const downloadCountLastDayRes = await ofetch(packageDownloadLastDayAPI); + const packageVersionRes = await ofetch(packageVersionAPI); const packageVersion = packageVersionRes.time; const packageVersionList = Object.keys(packageVersion) diff --git a/lib/routes/npr/namespace.ts b/lib/routes/npr/namespace.ts index c996fcb0acd63e..b4c93afd9d7e76 100644 --- a/lib/routes/npr/namespace.ts +++ b/lib/routes/npr/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'National Public Radio', url: 'npr.org', + lang: 'en', }; diff --git a/lib/routes/ntdm/namespace.ts b/lib/routes/ntdm/namespace.ts index a831c050405574..3eed8cc9659431 100644 --- a/lib/routes/ntdm/namespace.ts +++ b/lib/routes/ntdm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'NT动漫', url: 'www.ntdm9.com', + lang: 'zh-CN', }; diff --git a/lib/routes/ntdtv/namespace.ts b/lib/routes/ntdtv/namespace.ts index deff9f98c5692c..01de33319cce14 100644 --- a/lib/routes/ntdtv/namespace.ts +++ b/lib/routes/ntdtv/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '新唐人电视台', url: 'www.ntdtv.com', + lang: 'zh-CN', }; diff --git a/lib/routes/nua/dc.ts b/lib/routes/nua/dc.ts index 37614bbc9a5536..e7ab373d222860 100644 --- a/lib/routes/nua/dc.ts +++ b/lib/routes/nua/dc.ts @@ -24,13 +24,13 @@ export const route: Route = { maintainers: ['evnydd0sf'], handler, description: `| News Type | Parameters | - | ------------------------ | ---------- | - | 学院新闻 NEWS | news | - | 展览 EXHIBITION | exhibition | - | 研创 RESEARCH & CREATION | rc | - | 项目 PROJECT | project | - | 党团 PARTY | party | - | 后浪 YOUTH | youth |`, +| ------------------------ | ---------- | +| 学院新闻 NEWS | news | +| 展览 EXHIBITION | exhibition | +| 研创 RESEARCH & CREATION | rc | +| 项目 PROJECT | project | +| 党团 PARTY | party | +| 后浪 YOUTH | youth |`, }; async function handler(ctx) { diff --git a/lib/routes/nua/gra.ts b/lib/routes/nua/gra.ts index bebf29ad8525e0..c7e48e7cd9cab2 100644 --- a/lib/routes/nua/gra.ts +++ b/lib/routes/nua/gra.ts @@ -24,10 +24,10 @@ export const route: Route = { maintainers: ['evnydd0sf'], handler, description: `| News Type | Parameters | - | --------- | ---------- | - | 招生工作 | 1959 | - | 培养工作 | 1962 | - | 学位工作 | 1958 |`, +| --------- | ---------- | +| 招生工作 | 1959 | +| 培养工作 | 1962 | +| 学位工作 | 1958 |`, }; async function handler(ctx) { diff --git a/lib/routes/nua/index.ts b/lib/routes/nua/index.ts index 22b65876d37362..48fffe9c4d685f 100644 --- a/lib/routes/nua/index.ts +++ b/lib/routes/nua/index.ts @@ -23,9 +23,9 @@ export const route: Route = { maintainers: ['evnydd0sf'], handler, description: `| News Type | Parameters | - | --------- | ---------- | - | 公告 | 346 | - | 南艺要闻 | 332 |`, +| --------- | ---------- | +| 公告 | 346 | +| 南艺要闻 | 332 |`, }; async function handler(ctx) { diff --git a/lib/routes/nua/lib.ts b/lib/routes/nua/lib.ts index 8c6254ff9df9e4..12711f07348ad8 100644 --- a/lib/routes/nua/lib.ts +++ b/lib/routes/nua/lib.ts @@ -25,11 +25,11 @@ export const route: Route = { maintainers: ['evnydd0sf'], handler, description: `| News Type | Parameters | - | --------- | ---------- | - | 新闻动态 | xwdt | - | 党建动态 | djdt | - | 资源动态 | zydt | - | 服务动态 | fwdt |`, +| --------- | ---------- | +| 新闻动态 | xwdt | +| 党建动态 | djdt | +| 资源动态 | zydt | +| 服务动态 | fwdt |`, }; async function handler(ctx) { diff --git a/lib/routes/nua/namespace.ts b/lib/routes/nua/namespace.ts index cab64c21198e52..5583cb8cf96304 100644 --- a/lib/routes/nua/namespace.ts +++ b/lib/routes/nua/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Nanjing University of the Arts 南京艺术学院', url: 'index.nua.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nua/sxw.ts b/lib/routes/nua/sxw.ts index 5937c67cec8c70..e5cf7f81a03d69 100644 --- a/lib/routes/nua/sxw.ts +++ b/lib/routes/nua/sxw.ts @@ -23,12 +23,12 @@ export const route: Route = { maintainers: ['evnydd0sf'], handler, description: `| News Type | Parameters | - | --------- | ---------- | - | 校园电视 | 230 | - | 院部动态 | 232 | - | 动感校园 | 233 | - | 招就指南 | 234 | - | 南艺院报 | 236 |`, +| --------- | ---------- | +| 校园电视 | 230 | +| 院部动态 | 232 | +| 动感校园 | 233 | +| 招就指南 | 234 | +| 南艺院报 | 236 |`, }; async function handler(ctx) { diff --git a/lib/routes/nuaa/college/cs.ts b/lib/routes/nuaa/college/cs.ts index bb521288c7d0ee..be076db17b4554 100644 --- a/lib/routes/nuaa/college/cs.ts +++ b/lib/routes/nuaa/college/cs.ts @@ -33,8 +33,8 @@ export const route: Route = { maintainers: ['LogicJake', 'Seiry', 'qrzbing', 'Xm798'], handler, description: `| 通知公告 | 热点新闻 | 学科科研 | 教学动态 | 本科生培养 | 研究生培养 | 学生工作 | - | -------- | -------- | -------- | -------- | ---------- | ---------- | -------- | - | tzgg | rdxw | xkky | jxdt | be | me | xsgz |`, +| -------- | -------- | -------- | -------- | ---------- | ---------- | -------- | +| tzgg | rdxw | xkky | jxdt | be | me | xsgz |`, }; async function handler(ctx) { diff --git a/lib/routes/nuaa/jwc/jwc.ts b/lib/routes/nuaa/jwc/jwc.ts index 459e455e8d3ed0..bf856cf9d911b9 100644 --- a/lib/routes/nuaa/jwc/jwc.ts +++ b/lib/routes/nuaa/jwc/jwc.ts @@ -31,8 +31,8 @@ export const route: Route = { maintainers: ['arcosx', 'Seiry', 'qrzbing', 'Xm798'], handler, description: `| 通知公告 | 教学服务 | 教学建设 | 学生培养 | 教学资源 | - | -------- | -------- | -------- | -------- | -------- | - | tzgg | jxfw | jxjs | xspy | jxzy |`, +| -------- | -------- | -------- | -------- | -------- | +| tzgg | jxfw | jxjs | xspy | jxzy |`, }; async function handler(ctx) { diff --git a/lib/routes/nuaa/namespace.ts b/lib/routes/nuaa/namespace.ts index cbdc87595480b2..0026e6d843df79 100644 --- a/lib/routes/nuaa/namespace.ts +++ b/lib/routes/nuaa/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京航空航天大学', url: 'aao.nuaa.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nuaa/yjsy/yjsy.ts b/lib/routes/nuaa/yjsy/yjsy.ts index 0275aa9aff421e..431f4cfe762bf4 100644 --- a/lib/routes/nuaa/yjsy/yjsy.ts +++ b/lib/routes/nuaa/yjsy/yjsy.ts @@ -30,8 +30,8 @@ export const route: Route = { maintainers: ['junfengP', 'Seiry', 'Xm798'], handler, description: `| 通知公告 | 新闻动态 | 学术信息 | 师生风采 | - | -------- | -------- | -------- | -------- | - | tzgg | xwdt | xsxx | ssfc |`, +| -------- | -------- | -------- | -------- | +| tzgg | xwdt | xsxx | ssfc |`, }; async function handler(ctx) { diff --git a/lib/routes/nudt/namespace.ts b/lib/routes/nudt/namespace.ts new file mode 100644 index 00000000000000..2ee0b20153037d --- /dev/null +++ b/lib/routes/nudt/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国人民解放军国防科技大学', + url: 'www.nudt.edu.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/nudt/yjszs.ts b/lib/routes/nudt/yjszs.ts new file mode 100644 index 00000000000000..db2eb0ce10b20d --- /dev/null +++ b/lib/routes/nudt/yjszs.ts @@ -0,0 +1,88 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import timezone from '@/utils/timezone'; + +/* 研究生院 */ +const host = 'http://yjszs.nudt.edu.cn'; + +// 目前研究生院最近仍在更新的链接 +const yjszs = new Map([ + // http://yjszs.nudt.edu.cn/pubweb/homePageList/recruitStudents.view?keyId=2 + // http://yjszs.nudt.edu.cn//pubweb/homePageList/searchContent.view + ['2', { title: '国防科技大学研究生院 - 通知公告' }], + // http://yjszs.nudt.edu.cn/pubweb/homePageList/recruitStudents.view?keyId=1 + ['1', { title: '国防科技大学研究生院 - 首页' }], + // http://yjszs.nudt.edu.cn/pubweb/homePageList/recruitStudents.view?keyId=8 + ['8', { title: '国防科技大学研究生院 - 招生简章' }], + // http://yjszs.nudt.edu.cn/pubweb/homePageList/recruitStudents.view?keyId=12 + ['12', { title: '国防科技大学研究生院 - 学校政策' }], + // http://yjszs.nudt.edu.cn//pubweb/homePageList/recruitStudents.view?keyId=16 + ['16', { title: '国防科技大学研究生院 - 硕士招生' }], + // http://yjszs.nudt.edu.cn/pubweb/homePageList/recruitStudents.view?keyId=17 + ['17', { title: '国防科技大学研究生院 - 博士招生' }], + // http://yjszs.nudt.edu.cn/pubweb/homePageList/recruitStudents.view?keyId=23 + ['23', { title: '国防科技大学研究生院 - 院所发文' }], + // http://yjszs.nudt.edu.cn/pubweb/homePageList/recruitStudents.view?keyId=25 + ['25', { title: '国防科技大学研究生院 - 数据统计' }], +]); + +export const route: Route = { + path: '/yjszs/:keyId?', + categories: ['university'], + example: '/nudt/yjszs/2', + parameters: { keyId: '分类,见下表,默认为通知公告' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['yjszs.nudt.edu.cn'], + }, + ], + name: '研究生院', + maintainers: ['Blank0120'], + handler, + url: 'yjszs.nudt.edu.cn/', + description: `| 通知公告 | 首页 | 招生简章 | 学校政策 | 硕士招生 | 博士招生 | 院所发文 | 数据统计 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| 2 | 1 | 8 | 12 | 16 | 17 | 23 | 25 |`, +}; + +async function handler(ctx) { + const keyId = ctx.req.param('keyId') ?? '2'; + const info = yjszs.get(keyId); + if (!info) { + throw new InvalidParameterError('invalid keyId'); + } + let link = `${host}/pubweb/homePageList`; + link += keyId === '2' ? `/searchContent.view` : `/recruitStudents.view?keyId=${keyId}`; + const response = await got({ + method: 'get', + url: link, + }); + + const $ = load(response.data); + const content = $('.news-list li'); + const items = content.toArray().map((elem) => { + elem = $(elem); + return { + link: new URL(elem.find('a').attr('href'), host).href, + title: elem.find('h3').text().trim(), + pubDate: timezone(parseDate(elem.find('.time').text(), 'YYYY-MM-DD'), -8), + }; + }); + + return { + title: info.title, + link, + item: items, + }; +} diff --git a/lib/routes/nuist/bulletin.ts b/lib/routes/nuist/bulletin.ts index 2348a631211b52..21b32605bfa999 100644 --- a/lib/routes/nuist/bulletin.ts +++ b/lib/routes/nuist/bulletin.ts @@ -45,16 +45,16 @@ export const route: Route = { maintainers: ['gylidian'], handler, description: `| 全部 | 文件公告 | 学术报告 | 招标信息 | 会议通知 | 党政事务 | 组织人事 | - | ---- | -------- | -------- | -------- | -------- | -------- | -------- | - | 791 | 792 | xsbgw | 779 | 780 | 781 | 782 | +| ---- | -------- | -------- | -------- | -------- | -------- | -------- | +| 791 | 792 | xsbgw | 779 | 780 | 781 | 782 | - | 科研信息 | 招生就业 | 教学考试 | 专题讲座 | 校园活动 | 学院动态 | 其他 | - | -------- | -------- | -------- | -------- | -------- | -------- | ---- | - | 783 | 784 | 785 | 786 | 788 | 789 | qt | +| 科研信息 | 招生就业 | 教学考试 | 专题讲座 | 校园活动 | 学院动态 | 其他 | +| -------- | -------- | -------- | -------- | -------- | -------- | ---- | +| 783 | 784 | 785 | 786 | 788 | 789 | qt | - :::warning +::: warning 全文内容需使用 校园网或[VPN](http://vpn.nuist.edu.cn) 获取 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/nuist/cas.ts b/lib/routes/nuist/cas.ts index 835f8c74cc57dc..a45054916520c9 100644 --- a/lib/routes/nuist/cas.ts +++ b/lib/routes/nuist/cas.ts @@ -25,8 +25,8 @@ export const route: Route = { maintainers: ['gylidian'], handler, description: `| 信息公告 | 新闻快讯 | 科学研究 | 网上公示 | 本科教育 | 研究生教育 | - | -------- | -------- | -------- | -------- | -------- | ---------- | - | xxgg | xwkx | kxyj | wsgs | bkjy | yjsjy |`, +| -------- | -------- | -------- | -------- | -------- | ---------- | +| xxgg | xwkx | kxyj | wsgs | bkjy | yjsjy |`, }; async function handler(ctx) { diff --git a/lib/routes/nuist/jwc.ts b/lib/routes/nuist/jwc.ts index be2550c62913a1..c0101b4592129f 100644 --- a/lib/routes/nuist/jwc.ts +++ b/lib/routes/nuist/jwc.ts @@ -24,8 +24,8 @@ export const route: Route = { maintainers: ['gylidian'], handler, description: `| 教学要闻 | 学院教学 | 教务管理 | 教学研究 | 教务管理 | 教材建设 | 考试中心 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | jxyw | xyjx | jwgl | jxyj | sjjx | jcjs | kszx |`, +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| jxyw | xyjx | jwgl | jxyj | sjjx | jcjs | kszx |`, }; async function handler(ctx) { diff --git a/lib/routes/nuist/namespace.ts b/lib/routes/nuist/namespace.ts index 0b77a2e992d952..461660d655da64 100644 --- a/lib/routes/nuist/namespace.ts +++ b/lib/routes/nuist/namespace.ts @@ -3,7 +3,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京信息工程大学', url: 'bulletin.nuist.edu.cn', - description: `:::tip + description: `::: tip 路由地址全部按照 **学校官网域名和栏目编号** 设计 使用方法: @@ -16,4 +16,5 @@ export const namespace: Namespace = { [https://rsshub.app/**nuist**/\`bulletin\`](https://rsshub.app/nuist/bulletin) 或 [https://rsshub.app/**nuist**/\`bulletin\`/\`791\`](https://rsshub.app/nuist/bulletin) :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/nuist/scs.ts b/lib/routes/nuist/scs.ts index f347f153cb9e2e..9981cfd9fb5e9f 100644 --- a/lib/routes/nuist/scs.ts +++ b/lib/routes/nuist/scs.ts @@ -29,8 +29,8 @@ export const route: Route = { maintainers: ['gylidian'], handler, description: `| 新闻快讯 | 通知公告 | 教务信息 | 科研动态 | 学子风采 | - | -------- | -------- | -------- | -------- | -------- | - | xwkx | tzgg | jwxx | kydt | xzfc |`, +| -------- | -------- | -------- | -------- | -------- | +| xwkx | tzgg | jwxx | kydt | xzfc |`, }; async function handler(ctx) { diff --git a/lib/routes/nuist/sese.ts b/lib/routes/nuist/sese.ts index 8915e7d93b80db..b2e3956a55e3ea 100644 --- a/lib/routes/nuist/sese.ts +++ b/lib/routes/nuist/sese.ts @@ -24,8 +24,8 @@ export const route: Route = { maintainers: ['gylidian'], handler, description: `| 通知公告 | 新闻快讯 | 学术动态 | 学生工作 | 研究生教育 | 本科教育 | - | -------- | -------- | -------- | -------- | ---------- | -------- | - | tzgg1 | xwkx | xsdt1 | xsgz1 | yjsjy1 | bkjy1 |`, +| -------- | -------- | -------- | -------- | ---------- | -------- | +| tzgg1 | xwkx | xsdt1 | xsgz1 | yjsjy1 | bkjy1 |`, }; async function handler(ctx) { diff --git a/lib/routes/nwafu/all.ts b/lib/routes/nwafu/all.ts index 4bf13ada0841ec..4c49e5db9dd46b 100644 --- a/lib/routes/nwafu/all.ts +++ b/lib/routes/nwafu/all.ts @@ -23,9 +23,9 @@ export const route: Route = { handler, description: `通知类别 - | 图书馆 | 共青团团委 | 信工学院 | 后勤管理处 | 计划财务处 | 教务处 | 新闻网 | 信息化管理处 | 研究生院 | 农业科学院 | 机械与电子工程学院 | 学术活动 | 生命科学学院 | - | ------ | ---------- | -------- | ---------- | ---------- | ------ | ------ | ------------ | -------- | ---------- | ------------------ | -------- | ------------ | - | lib | youth | cie | gs | jcc | jiaowu | news | nic | yjshy | nxy | cmee | xshd | sm |`, +| 图书馆 | 共青团团委 | 信工学院 | 后勤管理处 | 计划财务处 | 教务处 | 新闻网 | 信息化管理处 | 研究生院 | 农业科学院 | 机械与电子工程学院 | 学术活动 | 生命科学学院 | +| ------ | ---------- | -------- | ---------- | ---------- | ------ | ------ | ------------ | -------- | ---------- | ------------------ | -------- | ------------ | +| lib | youth | cie | gs | jcc | jiaowu | news | nic | yjshy | nxy | cmee | xshd | sm |`, }; async function handler(ctx) { diff --git a/lib/routes/nwafu/namespace.ts b/lib/routes/nwafu/namespace.ts index 11372b74492cc1..a6d6aa92eb4ca8 100644 --- a/lib/routes/nwafu/namespace.ts +++ b/lib/routes/nwafu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '西北农林科技大学', url: 'nwafu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/nyaa/main.ts b/lib/routes/nyaa/main.ts index 284dd5888932ab..a8032a6e723fc4 100644 --- a/lib/routes/nyaa/main.ts +++ b/lib/routes/nyaa/main.ts @@ -16,7 +16,7 @@ export const route: Route = { supportScihub: false, }, name: 'Search Result', - maintainers: ['Lava-Swimmer', 'noname1776'], + maintainers: ['Lava-Swimmer', 'noname1776', 'camera-2018'], handler, }; @@ -32,7 +32,7 @@ async function handler(ctx) { const { query, username } = ctx.req.param(); - const rootURL = ctx.routerPath.split('/')[1] === 'sukebei' ? 'https://sukebei.nyaa.si' : 'https://nyaa.si'; + const rootURL = ctx.req.path.split('/')[2] === 'sukebei' ? 'https://sukebei.nyaa.si' : 'https://nyaa.si'; let currentRSSURL = `${rootURL}/?page=rss`; let currentLink = `${rootURL}/`; @@ -48,7 +48,6 @@ async function handler(ctx) { const feed = await parser.parseURL(currentRSSURL); feed.items.map((item) => { - item.link = item.guid; item.description = item.content; item.enclosure_url = `magnet:?xt=urn:btih:${item.infoHash}`; item.enclosure_type = 'application/x-bittorrent'; diff --git a/lib/routes/nyaa/namespace.ts b/lib/routes/nyaa/namespace.ts index 2e35b2d87a0ac6..b9108883cb52fb 100644 --- a/lib/routes/nyaa/namespace.ts +++ b/lib/routes/nyaa/namespace.ts @@ -3,4 +3,15 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Nyaa', url: 'nyaa.si', + description: ` +::: tip +The 'Nyaa' includes several routes to access different parts of the site: +1. \`/nyaa/search/:query?\` - Use this route to search for content with a specific query. For example, \`/nyaa/search/bocchi\` to search for bocchi related content. +2. \`/nyaa/user/:username?\` - Access a user's profile by their username, e.g., \`/nyaa/user/ANiTorrent\`. +3. \`/nyaa/user/:username/search/:query?\` - Search within a specific user's submissions using a query, e.g., \`/nyaa/user/ANiTorrent/search/bocchi\`. +4. \`/nyaa/sukebei/search/:query?\` - This route is for searching adult content with a specific query, e.g., \`/nyaa/sukebei/search/hentai\`. +5. \`/nyaa/sukebei/user/:username?\` - Access an adult content user's profile, e.g., \`/nyaa/sukebei/user/milannews\`. +6. \`/nyaa/sukebei/user/:username/search/:query?\` - Search within a specific user's adult content submissions, e.g., \`/nyaa/sukebei/user/milannews/search/hentai\`. +:::`, + lang: 'en', }; diff --git a/lib/routes/nymity/namespace.ts b/lib/routes/nymity/namespace.ts index 801ea0533b2351..050d8ca75580bc 100644 --- a/lib/routes/nymity/namespace.ts +++ b/lib/routes/nymity/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'nymity', url: 'censorbib.nymity.ch', + lang: 'en', }; diff --git a/lib/routes/nytimes/book.ts b/lib/routes/nytimes/book.ts index edc4fa0b02c904..e1c9cb197b5e3f 100644 --- a/lib/routes/nytimes/book.ts +++ b/lib/routes/nytimes/book.ts @@ -1,26 +1,36 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { load } from 'cheerio'; const categoryList = { - 'combined-print-and-e-book-nonfiction': '非虚构类 - 综合', - 'hardcover-nonfiction': '非虚构类 - 精装本', - 'paperback-nonfiction': '非虚构类 - 平装本', - 'advice-how-to-and-miscellaneous': '工具类', - 'combined-print-and-e-book-fiction': '虚构类 - 综合', - 'hardcover-fiction': '虚构类 - 精装本', - 'trade-fiction-paperback': '虚构类 - 平装本', - 'childrens-middle-grade-hardcover': '儿童 - 中年级', - 'picture-books': '儿童 - 绘本', - 'series-books': '儿童 - 系列图书', - 'young-adult-hardcover': '青少年', + 'combined-print-and-e-book-nonfiction': 'Combined Print & E-Book Nonfiction', + 'hardcover-nonfiction': 'Hardcover Nonfiction', + 'paperback-nonfiction': 'Paperback Nonfiction', + 'advice-how-to-and-miscellaneous': 'Advice, How-To & Miscellaneous', + 'combined-print-and-e-book-fiction': 'Combined Print & E-Book Fiction', + 'hardcover-fiction': 'Hardcover Fiction', + 'trade-fiction-paperback': 'Paperback Trade Fiction', + 'childrens-middle-grade-hardcover': "Children's Middle Grade Hardcover", + 'picture-books': 'Picture Books', + 'series-books': 'Series Books', + 'young-adult-hardcover': 'Young Adult Hardcover', }; export const route: Route = { path: '/book/:category?', - categories: ['traditional-media'], + categories: ['traditional-media', 'popular'], + view: ViewType.Notifications, example: '/nytimes/book/combined-print-and-e-book-nonfiction', - parameters: { category: 'N' }, + parameters: { + category: { + description: 'Category, can be found on the [official page](https://www.nytimes.com/books/best-sellers/)', + options: Object.keys(categoryList).map((key) => ({ + value: key, + label: categoryList[key], + })), + default: 'combined-print-and-e-book-nonfiction', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -36,22 +46,9 @@ export const route: Route = { }, ], name: 'Best Seller Books', - maintainers: ['melvinto'], + maintainers: ['melvinto', 'pseudoyu'], handler, url: 'nytimes.com/', - description: `| Category | -| ------------------------------------ | -| combined-print-and-e-book-nonfiction | -| hardcover-nonfiction | -| paperback-nonfiction | -| advice-how-to-and-miscellaneous | -| combined-print-and-e-book-fiction | -| hardcover-fiction | -| trade-fiction-paperback | -| childrens-middle-grade-hardcover | -| picture-books | -| series-books | -| young-adult-hardcover |`, }; async function handler(ctx) { diff --git a/lib/routes/nytimes/daily-briefing-chinese.ts b/lib/routes/nytimes/daily-briefing-chinese.ts index 14e229e4c81171..c5f3078ed83d3f 100644 --- a/lib/routes/nytimes/daily-briefing-chinese.ts +++ b/lib/routes/nytimes/daily-briefing-chinese.ts @@ -11,7 +11,7 @@ import path from 'node:path'; export const route: Route = { path: '/daily_briefing_chinese', - categories: ['traditional-media'], + categories: ['traditional-media', 'popular'], example: '/nytimes/daily_briefing_chinese', parameters: {}, features: { @@ -28,11 +28,11 @@ export const route: Route = { target: '', }, ], - name: '新闻简报', + name: 'Daily Briefing', maintainers: ['yueyericardo', 'nczitzk'], handler, url: 'nytimes.com/', - description: `网站地址:[https://www.nytimes.com/zh-hans/series/daily-briefing-chinese](https://www.nytimes.com/zh-hans/series/daily-briefing-chinese)`, + description: `URL: [https://www.nytimes.com/zh-hans/series/daily-briefing-chinese](https://www.nytimes.com/zh-hans/series/daily-briefing-chinese)`, }; async function handler() { @@ -91,7 +91,7 @@ async function handler() { ); return { - title: '新闻简报 - The New York Times', + title: 'Daily Briefing - The New York Times', link: currentUrl, item: items, }; diff --git a/lib/routes/nytimes/index.ts b/lib/routes/nytimes/index.ts index 7aacef14903be0..e3a840e892a378 100644 --- a/lib/routes/nytimes/index.ts +++ b/lib/routes/nytimes/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 parser from '@/utils/rss-parser'; @@ -8,9 +8,20 @@ import puppeteer from '@/utils/puppeteer'; export const route: Route = { path: '/:lang?', - categories: ['traditional-media'], + categories: ['traditional-media', 'popular'], + view: ViewType.Articles, example: '/nytimes/dual', - parameters: { lang: 'language, default to Chinese' }, + parameters: { + lang: { + description: 'language, default to Chinese', + options: [ + { value: 'dual', label: 'Chinese-English' }, + { value: 'en', label: 'English' }, + { value: 'traditionalchinese', label: 'Traditional Chinese' }, + { value: 'dual-traditionalchinese', label: 'Chinese-English (Traditional Chinese)' }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -26,14 +37,10 @@ export const route: Route = { }, ], name: 'News', - maintainers: ['HenryQW'], + maintainers: ['HenryQW', 'pseudoyu'], handler, url: 'nytimes.com/', - description: `By extracting the full text of articles, we provide a better reading experience (full text articles) over the official one. - - | Default to Chinese | Chinese-English | English | Chinese-English (Traditional Chinese) | Traditional Chinese | - | ------------------ | --------------- | ------- | ------------------------------------- | ------------------- | - | (empty) | dual | en | dual-traditionalchinese | traditionalchinese |`, + description: `By extracting the full text of articles, we provide a better reading experience (full text articles) over the official one.`, }; async function handler(ctx) { @@ -144,7 +151,7 @@ async function handler(ctx) { }) ); - browser.close(); + await browser.close(); return { title, diff --git a/lib/routes/nytimes/namespace.ts b/lib/routes/nytimes/namespace.ts index 9871c92ebe0889..78946001e48f74 100644 --- a/lib/routes/nytimes/namespace.ts +++ b/lib/routes/nytimes/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'The New York Times 纽约时报', + name: 'The New York Times', url: 'nytimes.com', + lang: 'en', }; diff --git a/lib/routes/nytimes/rss.ts b/lib/routes/nytimes/rss.ts new file mode 100644 index 00000000000000..681aaa39601017 --- /dev/null +++ b/lib/routes/nytimes/rss.ts @@ -0,0 +1,57 @@ +import { Route, ViewType } from '@/types'; +import cache from '@/utils/cache'; +import parser from '@/utils/rss-parser'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/rss/:cat?', + categories: ['traditional-media', 'popular'], + view: ViewType.Articles, + example: '/nytimes/rss/HomePage', + parameters: { + cat: { + description: "Category name, corresponding to the last segment of [official feed's](https://www.nytimes.com/rss) url.", + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['nytimes.com/'], + target: '', + }, + ], + name: 'News', + maintainers: ['HenryQW', 'pseudoyu', 'dzx-dzx'], + handler, + url: 'nytimes.com/', + description: `Enhance the official EN RSS feed`, +}; + +async function handler(ctx) { + const url = `https://rss.nytimes.com/services/xml/rss/nyt/${ctx.req.param('cat')}.xml`; + + const rss = await parser.parseURL(url); + + return { + ...rss, + item: await Promise.all( + rss.items.map((e) => + cache.tryGet(e.link, async () => { + const res = await ofetch(e.link, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' }, referrer: 'https://www.google.com/' }); + + const $ = load(res); + + return { ...e, description: $("[name='articleBody']").html() }; + }) + ) + ), + }; +} diff --git a/lib/routes/obsidian/namespace.ts b/lib/routes/obsidian/namespace.ts new file mode 100644 index 00000000000000..b9d700b55b6ddb --- /dev/null +++ b/lib/routes/obsidian/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Obsidian', + url: 'obsidian.md', + lang: 'en', +}; diff --git a/lib/routes/obsidian/plugins.ts b/lib/routes/obsidian/plugins.ts new file mode 100644 index 00000000000000..83198bfacd388c --- /dev/null +++ b/lib/routes/obsidian/plugins.ts @@ -0,0 +1,41 @@ +import { Route } from '@/types'; + +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/plugins', + name: 'Obsidian Plugins', + maintainers: ['DIYgod'], + categories: ['program-update'], + example: '/obsidian/plugins', + handler, +}; + +async function handler() { + const data = JSON.parse(await ofetch('https://raw.githubusercontent.com/obsidianmd/obsidian-releases/refs/heads/master/community-plugins.json')) as { + id: string; + name: string; + author: string; + description: string; + repo: string; + }[]; + const stats = JSON.parse(await ofetch('https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-plugin-stats.json')) as { + [key: string]: { + downloads: number; + updated: number; + }; + }; + + return { + title: 'Obsidian Plugins', + link: `https://obsidian.md/plugins`, + item: data.map((item) => ({ + title: item.name, + description: `${item.description}<br><br>Downloads: ${stats[item.id].downloads}`, + link: `https://github.com/${item.repo}`, + guid: item.id, + pubDate: new Date(stats[item.id].updated), + author: item.author, + })), + }; +} diff --git a/lib/routes/obsidian/publish.ts b/lib/routes/obsidian/publish.ts new file mode 100644 index 00000000000000..c0f237c6056c82 --- /dev/null +++ b/lib/routes/obsidian/publish.ts @@ -0,0 +1,83 @@ +import { Route, DataItem } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import { getTitle } from './utils'; +import { config } from '@/config'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/publish/:id', + categories: ['blog'], + example: '/obsidian/publish/marshallontheroad', + parameters: { id: '网站 id,由Publish持有者自定义' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publish.obsidian.md/'], + }, + ], + name: 'Publish', + maintainers: ['Xy2002'], + handler, + url: 'publish.obsidian.md/', +}; + +async function handler(ctx) { + const id = ctx.req.param('id'); + const items = await fetchPage(id); + + return { + title: 'Obsidian Publish', + language: 'en-us', + item: items, + link: 'https://publish.obsidian.md/', + }; +} + +async function fetchPage(id: string) { + const baseUrl = `https://publish.obsidian.md/${id}`; + const response = await ofetch(baseUrl); + const $ = load(response); + const preloadCacheUrl = + $('script:contains("preloadCache")') + .text() + .match(/preloadCache\s*=\s*f\("([^"]+)"\);/)?.[1] || ''; + + let preloadCacheResponse: Record<string, { frontmatter?: Record<string, string> }>; + try { + preloadCacheResponse = await ofetch(preloadCacheUrl, { + headers: { + 'User-Agent': config.trueUA, + Referer: 'https://publish.obsidian.md/', + Origin: 'https://publish.obsidian.m/', + }, + }); + } catch { + preloadCacheResponse = {}; + } + + const items: DataItem[] = Object.entries(preloadCacheResponse) + .map(([postKey, post]) => { + if (!post) { + return null; + } + const item: DataItem = { + title: post.frontmatter?.title || getTitle(postKey), + link: `${baseUrl}/${postKey.replaceAll(' ', '+').split('.md')[0]}`, + pubDate: post.frontmatter?.['date created'] ? parseDate(post.frontmatter['date created']) : undefined, + ...post.frontmatter, + }; + + return item; + }) + .filter(Boolean) as DataItem[]; + + return items; +} diff --git a/lib/routes/obsidian/utils.ts b/lib/routes/obsidian/utils.ts new file mode 100644 index 00000000000000..39dfdee831c230 --- /dev/null +++ b/lib/routes/obsidian/utils.ts @@ -0,0 +1,8 @@ +const regex = /([^/]+)\.md$/; + +const getTitle = (path: string): string => { + const match = path.match(regex); + return match ? match[1] : ''; +}; + +export { getTitle }; diff --git a/lib/routes/oceanengine/arithmetic-index.ts b/lib/routes/oceanengine/arithmetic-index.ts index b40e52ad7fecdd..d276793fd3ef5c 100644 --- a/lib/routes/oceanengine/arithmetic-index.ts +++ b/lib/routes/oceanengine/arithmetic-index.ts @@ -118,7 +118,7 @@ async function handler(ctx) { }); await page.goto('https://trendinsight.oceanengine.com/arithmetic-index'); const res = await getMultiKeywordHotTrend(page, keyword, start_date, end_date, channel); - browser.close(); + await browser.close(); const rawData = JSON.parse(res).data; const data = decrypt(rawData).hot_list[0]; diff --git a/lib/routes/oceanengine/namespace.ts b/lib/routes/oceanengine/namespace.ts index b19df4d41067c9..e5c4544a34d8a3 100644 --- a/lib/routes/oceanengine/namespace.ts +++ b/lib/routes/oceanengine/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '巨量算数 - 算数指数', url: 'trendinsight.oceanengine.com', + lang: 'zh-CN', }; diff --git a/lib/routes/oct0pu5/namespace.ts b/lib/routes/oct0pu5/namespace.ts new file mode 100644 index 00000000000000..1de3296b7b5df1 --- /dev/null +++ b/lib/routes/oct0pu5/namespace.ts @@ -0,0 +1,11 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Oct0pu5 blog', + url: 'Oct0pu5.cn', + + zh: { + name: 'Oct0pu5的小破站', + }, + lang: 'zh-CN', +}; diff --git a/lib/routes/oct0pu5/rss.ts b/lib/routes/oct0pu5/rss.ts new file mode 100644 index 00000000000000..a57892a7238012 --- /dev/null +++ b/lib/routes/oct0pu5/rss.ts @@ -0,0 +1,48 @@ +import { Route } from '@/types'; +import buildData from '@/utils/common-config'; + +const baseUrl = 'https://oct0pu5.cn/'; + +export const route: Route = { + path: '/', + categories: ['blog'], + example: '/oct0pu5', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['oct0pu5.cn'], + target: '/', + }, + ], + name: 'Oct的小破站', + maintainers: ['octopus058', 'wiketool'], + handler, +}; + +async function handler() { + const link = baseUrl; + return await buildData({ + link, + url: link, + title: `%title%`, + description: `%description%`, + params: { + title: '博客', + description: 'Oct0pu5的博客', + }, + item: { + item: '.recent-posts > .recent-post-item', + title: `$('.recent-post-info > a').text()`, + link: `$('.recent-post-info > a').attr('href')`, + description: `$('.recent-post-info > .content').text()`, + pubDate: `Date.parse($('div.recent-post-info > div.article-meta-wrap > span.post-meta-date > time').text().trim())`, + }, + }); +} diff --git a/lib/routes/odaily/activity.ts b/lib/routes/odaily/activity.ts index be3f3edefa06a3..41f99b570529aa 100644 --- a/lib/routes/odaily/activity.ts +++ b/lib/routes/odaily/activity.ts @@ -8,7 +8,7 @@ import { rootUrl } from './utils'; export const route: Route = { path: '/activity', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/odaily/activity', parameters: {}, features: { @@ -55,7 +55,12 @@ async function handler(ctx) { const content = load(detailResponse.data.match(/"content":"(.*)"}},"secondaryList":/)[1]); content('img').each(function () { - content(this).attr('src', content(this).attr('src').replaceAll('\\"', '')); + content(this).attr( + 'src', + content(this) + .attr('src') + .replaceAll(String.raw`\"`, '') + ); }); item.description = content.html(); diff --git a/lib/routes/odaily/namespace.ts b/lib/routes/odaily/namespace.ts index e36c8e61fc1b14..8edfd82a23487b 100644 --- a/lib/routes/odaily/namespace.ts +++ b/lib/routes/odaily/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Odaily 星球日报', url: 'odaily.news', + lang: 'zh-CN', }; diff --git a/lib/routes/odaily/newsflash.ts b/lib/routes/odaily/newsflash.ts index a1834f3d793973..20dcfa1ce78366 100644 --- a/lib/routes/odaily/newsflash.ts +++ b/lib/routes/odaily/newsflash.ts @@ -6,7 +6,7 @@ import { rootUrl } from './utils'; export const route: Route = { path: '/newsflash', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/odaily/newsflash', parameters: {}, features: { diff --git a/lib/routes/odaily/post.ts b/lib/routes/odaily/post.ts index 53e0db4c48653d..28e3945dd0ea2c 100644 --- a/lib/routes/odaily/post.ts +++ b/lib/routes/odaily/post.ts @@ -19,7 +19,7 @@ const titles = { export const route: Route = { path: '/:id?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/odaily', parameters: { id: 'id,见下表,默认为最新' }, features: { @@ -40,8 +40,8 @@ export const route: Route = { handler, url: '0daily.com/', description: `| 最新 | 新品 | DeFi | NFT | 存储 | 波卡 | 行情 | 活动 | - | ---- | ---- | ---- | --- | ---- | ---- | ---- | ---- | - | 280 | 333 | 331 | 334 | 332 | 330 | 297 | 296 |`, +| ---- | ---- | ---- | --- | ---- | ---- | ---- | ---- | +| 280 | 333 | 331 | 334 | 332 | 330 | 297 | 296 |`, }; async function handler(ctx) { diff --git a/lib/routes/odaily/search-news.ts b/lib/routes/odaily/search-news.ts index 8a9e3159c20b31..13108ca21d334e 100644 --- a/lib/routes/odaily/search-news.ts +++ b/lib/routes/odaily/search-news.ts @@ -6,7 +6,7 @@ import { rootUrl } from './utils'; export const route: Route = { path: '/search/news/:keyword', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/odaily/search/news/RSS3', parameters: { keyword: '搜索关键字' }, features: { diff --git a/lib/routes/odaily/user.ts b/lib/routes/odaily/user.ts index 89493cb5201339..3f0cdf92c13e3a 100644 --- a/lib/routes/odaily/user.ts +++ b/lib/routes/odaily/user.ts @@ -8,7 +8,7 @@ import { rootUrl } from './utils'; export const route: Route = { path: '/user/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/odaily/user/2147486902', parameters: { id: '用户 id,可在用户页地址栏中找到' }, features: { @@ -59,7 +59,12 @@ async function handler(ctx) { const content = load(detailResponse.data.match(/"content":"(.*)","extraction_tags":/)[1]); content('img').each(function () { - content(this).attr('src', content(this).attr('src').replaceAll('\\"', '')); + content(this).attr( + 'src', + content(this) + .attr('src') + .replaceAll(String.raw`\"`, '') + ); }); item.description = content.html(); diff --git a/lib/routes/oeeee/namespace.ts b/lib/routes/oeeee/namespace.ts index 83351b500ccb8e..6f906aa119a724 100644 --- a/lib/routes/oeeee/namespace.ts +++ b/lib/routes/oeeee/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南方都市报', url: 'oeeee.com', + lang: 'zh-CN', }; diff --git a/lib/routes/oilchem/index.ts b/lib/routes/oilchem/index.ts index f0db0e8333c4ae..8ef61e9668f054 100644 --- a/lib/routes/oilchem/index.ts +++ b/lib/routes/oilchem/index.ts @@ -39,7 +39,7 @@ async function handler(ctx) { const route = category === '' ? '' : `/${category}${subCategory === '' ? '' : `/${subCategory}`}`; const rootUrl = `https://${type === '' ? 'www' : 'list'}.oilchem.net`; - const currentUrl = `${rootUrl}${type === '' ? '/1/' : type === 'list' ? route : `/${routes[`/${type}${route}`]}`}`; + const currentUrl = `${rootUrl}${type === '' ? '/1/' : (type === 'list' ? route : `/${routes[`/${type}${route}`]}`)}`; const response = await got({ method: 'get', diff --git a/lib/routes/oilchem/namespace.ts b/lib/routes/oilchem/namespace.ts index 99edb194b6e179..7dc2a8304f7919 100644 --- a/lib/routes/oilchem/namespace.ts +++ b/lib/routes/oilchem/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '隆众资讯', url: 'oilchem.net', + lang: 'zh-CN', }; diff --git a/lib/routes/okx/index.ts b/lib/routes/okx/index.ts new file mode 100644 index 00000000000000..d04d5c6c7ed3d9 --- /dev/null +++ b/lib/routes/okx/index.ts @@ -0,0 +1,125 @@ +import { DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import { Context } from 'hono'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import got from '@/utils/got'; + +export const route: Route = { + path: '/:section?', + categories: ['finance'], + example: '/okx/new-listings', + parameters: { + section: { + description: '公告版块', + default: 'latest-announcements', + options: [ + { + value: 'latest-announcements', + label: '最新公告', + }, + { + value: 'new-listings', + label: '新币种上线', + }, + { + value: 'delistings', + label: '币对下线', + }, + { + value: 'trading-updates', + label: '交易规则更新', + }, + { + value: 'deposit-withdrawal-suspension-resumption', + label: '充提暂停/恢复公告', + }, + { + value: 'p2p-trading', + label: 'C2C 公告', + }, + { + value: 'web3', + label: 'Web3', + }, + { + value: 'earn', + label: '赚币', + }, + { + value: 'jumpstart', + label: 'Jumpstart', + }, + { + value: 'api', + label: 'API公告', + }, + { + value: 'okb-buy-back-burn', + label: 'OKB销毁', + }, + { + value: 'others', + label: '其他', + }, + ], + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.okx.com/zh-hans/help/section/:section'], + target: '/:section', + }, + ], + name: '公告', + maintainers: ['lxl66566'], + handler, +}; + +async function handler(ctx: Context) { + const baseUrl = 'https://www.okx.com'; + let { section = 'latest-announcements' } = ctx.req.param(); + section = section.replace(/^announcements-/, ''); + + const data = await ofetch(`${baseUrl}/zh-hans/help/section/announcements-${section}`); + const $ = load(data); + + const ssrData = JSON.parse($('script[data-id="__app_data_for_ssr__"]').text()); + const itemsTemp: { title: string; link: string; pubDate: Date }[] = + ssrData?.appContext?.initialProps?.sectionData?.articleList?.items?.map((item: { title: string; slug: string; publishTime: string }) => ({ + title: item.title, + link: `${baseUrl}/zh-hans/help/${item.slug}`, + pubDate: new Date(item.publishTime), + })) || []; + + const items = await Promise.all( + itemsTemp.map((item) => + cache.tryGet(item.link, async () => { + const content = await got(item.link).then((response) => { + const $ = load(response.data); + return $('div[class^="index_richTextContent"]').html(); + }); + + return { + ...item, + description: content || '内容获取失败', + }; + }) + ) + ); + + return { + title: ssrData?.appContext?.serverSideProps?.sectionOutline?.title || 'Unknown', + link: `${baseUrl}/zh-hans/help/section/announcements-${section}`, + item: items as DataItem[], + allowEmpty: true, + }; +} diff --git a/lib/routes/okx/namespace.ts b/lib/routes/okx/namespace.ts new file mode 100644 index 00000000000000..656a2a93770b72 --- /dev/null +++ b/lib/routes/okx/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '欧易 OKX', + url: 'www.okx.com/zh-hans', + lang: 'zh-CN', +}; diff --git a/lib/routes/olevod/namespace.ts b/lib/routes/olevod/namespace.ts index c0bcbcee5895c0..c159bdeb376026 100644 --- a/lib/routes/olevod/namespace.ts +++ b/lib/routes/olevod/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '欧乐影院', url: 'olevod.one', + lang: 'zh-CN', }; diff --git a/lib/routes/ollama/blog.ts b/lib/routes/ollama/blog.ts new file mode 100644 index 00000000000000..36e88e9e02990b --- /dev/null +++ b/lib/routes/ollama/blog.ts @@ -0,0 +1,39 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/blog', + categories: ['programming'], + example: '/ollama/blog', + radar: [ + { + source: ['ollama.com/blog'], + }, + ], + name: 'Blog', + maintainers: ['gavrilov'], + handler, +}; + +async function handler() { + const baseUrl = 'https://ollama.com'; + + const response = await ofetch(`${baseUrl}/blog`); + const $ = load(response); + + const items = $('a.group.border-b.py-10') + .toArray() + .map((item) => ({ + title: $(item).children('h2').first().text(), + link: baseUrl + $(item).attr('href'), + pubDate: parseDate($(item).children('h3').first().text()), + description: $(item).children('p').first().text(), + })); + return { + title: 'ollama blog', + link: 'https://ollama.com/blog', + item: items, + }; +} diff --git a/lib/routes/ollama/models.ts b/lib/routes/ollama/models.ts index 335fe40424258f..3e6b4aedfc329a 100644 --- a/lib/routes/ollama/models.ts +++ b/lib/routes/ollama/models.ts @@ -1,6 +1,7 @@ import { Route } from '@/types'; import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; +import { parseRelativeDate } from '@/utils/parse-date'; export const route: Route = { path: '/library', @@ -12,23 +13,25 @@ export const route: Route = { }, ], name: 'Models', - maintainers: ['Nick22nd'], + maintainers: ['Nick22nd', 'gavrilov'], handler, }; async function handler() { - const response = await ofetch('https://ollama.com/library'); + const response = await ofetch('https://ollama.com/library?sort=newest'); const $ = load(response); const items = $('#repo > ul > li > a') .toArray() .map((item) => { - const name = $(item).children('h2').first(); + const name = $(item).find('h2 span').first(); const link = $(item).attr('href'); - const description = $(item).children('p').first(); + const description = $(item).find('div p.break-words').first(); + const pubDate = $(item).find('span:contains("Updated")').first(); return { title: name.text(), link, description: description.text(), + pubDate: parseRelativeDate(pubDate.text()), }; }); return { diff --git a/lib/routes/ollama/namespace.ts b/lib/routes/ollama/namespace.ts index 1e60b826e13473..c6ad0901c80504 100644 --- a/lib/routes/ollama/namespace.ts +++ b/lib/routes/ollama/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Ollama', url: 'ollama.com', + lang: 'en', }; diff --git a/lib/routes/oncc/money18.ts b/lib/routes/oncc/money18.ts index 134981847cfcb2..aa0a96c88facb6 100644 --- a/lib/routes/oncc/money18.ts +++ b/lib/routes/oncc/money18.ts @@ -41,8 +41,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 新聞總覽 | 全日焦點 | 板塊新聞 | 國際金融 | 大行報告 | A 股新聞 | 地產新聞 | 投資理財 | 新股 IPO | 科技財情 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | - | exp | fov | industry | int | recagent | ntlgroup | pro | weainvest | ipo | tech |`, +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | +| exp | fov | industry | int | recagent | ntlgroup | pro | weainvest | ipo | tech |`, }; async function handler(ctx) { @@ -56,7 +56,7 @@ async function handler(ctx) { const toApiUrl = (date) => `${rootUrl}/cnt/utf8/content/${date}/articleList/list_${id}_all.js`; - let apiUrl = id === 'ipo' ? ipoApiUrl : id === 'industry' ? industryApiUrl : toApiUrl(dayjs().format('YYYYMMDD')), + let apiUrl = id === 'ipo' ? ipoApiUrl : (id === 'industry' ? industryApiUrl : toApiUrl(dayjs().format('YYYYMMDD'))), hasArticle = false, items = [], i = 0, diff --git a/lib/routes/oncc/namespace.ts b/lib/routes/oncc/namespace.ts index 601f972b0c6566..c25eb35ff71126 100644 --- a/lib/routes/oncc/namespace.ts +++ b/lib/routes/oncc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '东网', url: 'hk.on.cc', + lang: 'zh-HK', }; diff --git a/lib/routes/oncc/templates/article.art b/lib/routes/oncc/templates/article.art index 1fd784e87bffca..4fd443f47109d5 100644 --- a/lib/routes/oncc/templates/article.art +++ b/lib/routes/oncc/templates/article.art @@ -1,2 +1,2 @@ <img src="{{ imageUrl }}"> -{{ content }} +{{@ content }} diff --git a/lib/routes/onehu/namespace.ts b/lib/routes/onehu/namespace.ts index 04d3cab6681a6c..6223c46e56fdaa 100644 --- a/lib/routes/onehu/namespace.ts +++ b/lib/routes/onehu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '我不是盐神', url: 'onehu.xyz', + lang: 'zh-CN', }; diff --git a/lib/routes/onet/namespace.ts b/lib/routes/onet/namespace.ts index 79d7362fa452d9..6cf0a02d5e6887 100644 --- a/lib/routes/onet/namespace.ts +++ b/lib/routes/onet/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Onet', url: 'wiadomosci.onet.pl', + lang: 'en', }; diff --git a/lib/routes/oo-software/changelog.ts b/lib/routes/oo-software/changelog.ts index 37be9504a539d9..44632d500081d9 100644 --- a/lib/routes/oo-software/changelog.ts +++ b/lib/routes/oo-software/changelog.ts @@ -20,11 +20,11 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| Software | Id | - | --------------- | ----------- | - | O\&O ShutUp10++ | shutup10 | - | O\&O AppBuster | ooappbuster | - | O\&O Lanytix | oolanytix | - | O\&O DeskInfo | oodeskinfo |`, +| --------------- | ----------- | +| O\&O ShutUp10++ | shutup10 | +| O\&O AppBuster | ooappbuster | +| O\&O Lanytix | oolanytix | +| O\&O DeskInfo | oodeskinfo |`, }; async function handler(ctx) { diff --git a/lib/routes/oo-software/namespace.ts b/lib/routes/oo-software/namespace.ts index 0b5d4bfb83d707..f6868538482d42 100644 --- a/lib/routes/oo-software/namespace.ts +++ b/lib/routes/oo-software/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'O&O Software', url: 'oo-software.com', + lang: 'en', }; diff --git a/lib/routes/openai/blog.ts b/lib/routes/openai/blog.ts index b7814380c27163..1b1ea68a6f4c3b 100644 --- a/lib/routes/openai/blog.ts +++ b/lib/routes/openai/blog.ts @@ -5,7 +5,7 @@ import { getApiUrl, parseArticle } from './common'; export const route: Route = { path: '/blog/:tag?', - categories: ['new-media'], + categories: ['programming'], example: '/openai/blog', parameters: { tag: 'Tag, see below, All by default' }, features: { @@ -20,8 +20,8 @@ export const route: Route = { maintainers: ['StevenRCE0', 'nczitzk'], handler, description: `| All | Announcements | Events | Safety & Alignment | Community | Product | Culture & Careers | Milestones | Research | - | --- | ------------- | ------ | ------------------ | --------- | ------- | ------------------- | ---------- | -------- | - | | announcements | events | safety-alignment | community | product | culture-and-careers | milestones | research |`, +| --- | ------------- | ------ | ------------------ | --------- | ------- | ------------------- | ---------- | -------- | +| | announcements | events | safety-alignment | community | product | culture-and-careers | milestones | research |`, }; async function handler(ctx) { diff --git a/lib/routes/openai/chatgpt.ts b/lib/routes/openai/chatgpt.ts index 507dc15a54facc..15aba9dde7dd8c 100644 --- a/lib/routes/openai/chatgpt.ts +++ b/lib/routes/openai/chatgpt.ts @@ -10,7 +10,7 @@ dayjs.extend(isSameOrBefore); export const route: Route = { path: '/chatgpt/release-notes', - categories: ['new-media'], + categories: ['program-update'], example: '/openai/chatgpt/release-notes', parameters: {}, features: { diff --git a/lib/routes/openai/common.ts b/lib/routes/openai/common.ts index fe73a141a61cde..75a1b89063340d 100644 --- a/lib/routes/openai/common.ts +++ b/lib/routes/openai/common.ts @@ -19,7 +19,7 @@ const getApiUrl = async () => { const apiBaseUrl = initResponse.data .toString() .match(/(?<=TWILL_API_BASE:").+?(?=")/)[0] - .replaceAll('\\u002F', '/'); + .replaceAll(String.raw`\u002F`, '/'); return new URL(apiBaseUrl); }; diff --git a/lib/routes/openai/cookbook.ts b/lib/routes/openai/cookbook.ts new file mode 100644 index 00000000000000..e0f87f90942881 --- /dev/null +++ b/lib/routes/openai/cookbook.ts @@ -0,0 +1,77 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; +import logger from '@/utils/logger'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/cookbook', + categories: ['programming'], + description: + 'OpenAI Cookbook 提供了大量使用 OpenAI API 的实用指南和示例代码,涵盖了从基础到高级的各种主题,包括 GPT 模型、嵌入、函数调用、微调等。这里汇集了最新的 API 功能介绍和流行的应用案例,是开发者学习和应用 OpenAI 技术的宝贵资源。', + maintainers: ['liyaozhong'], + radar: [ + { + source: ['cookbook.openai.com/'], + }, + ], + url: 'cookbook.openai.com/', + handler, + example: '/openai/cookbook', + name: 'Cookbook', +}; + +async function handler() { + const rootUrl = 'https://cookbook.openai.com'; + const currentUrl = `${rootUrl}/`; + + try { + const response = await ofetch(currentUrl); + const $ = load(response); + + let items = $('[class="min-h-[90vh]"] .grid a') + .toArray() + .map((element) => { + const $element = $(element); + const $title = $element.find('div.font-semibold.text-sm.text-primary.line-clamp-1.overflow-ellipsis'); + const $date = $element.find(String.raw`span.text-xs.text-muted-foreground.md\:w-24.text-end`); + const $author = $element.find('p:contains("OpenAI")'); + const $tags = $element.find('span[style^="color:"]'); + + return { + title: $title.text().trim(), + link: `${rootUrl}/${$element.attr('href')}`, + pubDate: $date.text().trim(), + author: $author.text().replace('OpenAI', '').trim(), + category: $tags.toArray().map((tag) => $(tag).text().trim()), + }; + }); + + items = ( + await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + try { + const detailResponse = await ofetch(item.link); + const $ = load(detailResponse); + + item.description = $(String.raw`article.prose.prose-sm.sm\:prose-base.max-w-none.dark\:prose-invert`).html(); + return item; + } catch { + return { ...item, description: '' }; + } + }) + ) + ) + ).filter((item) => item?.description); + + return { + title: 'OpenAI Cookbook', + link: currentUrl, + item: items, + }; + } catch (error) { + logger.error(`处理 OpenAI Cookbook 请求时发生错误: ${error}`); + throw error; + } +} diff --git a/lib/routes/openai/namespace.ts b/lib/routes/openai/namespace.ts index 768232c884ed6f..633684e88faac7 100644 --- a/lib/routes/openai/namespace.ts +++ b/lib/routes/openai/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'OpenAI', url: 'openai.com', + lang: 'en', }; diff --git a/lib/routes/openai/research.ts b/lib/routes/openai/research.ts index 9a6dba2352596a..b0a0b0e6f50532 100644 --- a/lib/routes/openai/research.ts +++ b/lib/routes/openai/research.ts @@ -4,7 +4,7 @@ import { getApiUrl, parseArticle } from './common'; export const route: Route = { path: '/research', - categories: ['new-media'], + categories: ['programming'], example: '/openai/research', parameters: {}, features: { diff --git a/lib/routes/openrice/chart.ts b/lib/routes/openrice/chart.ts new file mode 100644 index 00000000000000..30a4a9377eb0cb --- /dev/null +++ b/lib/routes/openrice/chart.ts @@ -0,0 +1,68 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); +const baseUrl = 'https://www.openrice.com'; + +export const route: Route = { + path: '/:lang/hongkong/explore/chart/:category', + maintainers: ['after9'], + handler, + categories: ['shopping'], + example: '/openrice/zh/hongkong/explore/chart/most-bookmarked', + parameters: { lang: '语言,缺省为 zh', category: '类别,缺省为 most-bookmarked' }, + name: '香港餐廳排行榜', + description: ` +| 简体 | 繁體 | EN | +| ----- | ------ | ----- | +| zh-cn | zh | en | + +| 最多收藏 | 每周最高评分 | 最高浏览 | 最佳甜品餐厅 | +| ----- | ------ | ----- | ----- | +| most-bookmarked | best-rating | most-popular | best-dessert | + `, +}; + +async function handler(ctx) { + const lang = ctx.req.param('lang') ?? 'zh'; + const category = ctx.req.param('category') ?? 'most-bookmarked'; + + const urlPath: string = `/${lang}/hongkong/explore/chart/${category}`; + const response = await ofetch(baseUrl + urlPath); + const $ = load(response); + + const title = $('title').text() ?? 'Hong Kong Restaurant Chart'; + const description = $('title').text() ?? 'Hong Kong Restaurant Chart'; + + const data = $('.poi-chart-main-grid-item-desktop-wrapper'); + const resultList = data.toArray().map((item) => { + const $item = $(item); + const rankClass = $item.find('.rank-icon').attr('class'); + const rankNumber = rankClass?.match(/rank-(\d+)/)?.[1] ?? ''; + const desTags = $item.find('.pcmgidtr-left-section-poi-info-details .pcmgidtrls-poi-info-details-text'); + const desTagsArray: string[] = desTags.toArray().map((tag) => $(tag).text()); + const title = $item.find('.pcmgidtr-left-section-poi-info-name .link').text() ?? ''; + const link = $item.find('.pcmgidtr-left-section-poi-info-name .link').attr('href') ?? ''; + const coverImg = $item.find('.pcmgidtr-left-section-door-photo img').attr('src') ?? null; + const description = art(path.join(__dirname, 'templates/chart.art'), { + description: desTagsArray ?? [], + rankNumber, + image: coverImg, + }); + return { + title, + description, + link, + }; + }); + + return { + title, + link: baseUrl + urlPath, + description, + item: resultList, + }; +} diff --git a/lib/routes/openrice/namespace.ts b/lib/routes/openrice/namespace.ts new file mode 100644 index 00000000000000..b8c3c9a5dd79ba --- /dev/null +++ b/lib/routes/openrice/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Openrice開飯喇', + url: 'www.openrice.com', + categories: ['shopping'], + description: '美食網站Openrice相关資訊', + lang: 'zh-HK', +}; diff --git a/lib/routes/openrice/offers.ts b/lib/routes/openrice/offers.ts new file mode 100644 index 00000000000000..4d991984918019 --- /dev/null +++ b/lib/routes/openrice/offers.ts @@ -0,0 +1,80 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); +const baseUrl = 'https://www.openrice.com'; + +export const route: Route = { + path: '/:lang/hongkong/offers', + maintainers: ['after9'], + handler, + categories: ['shopping'], + example: '/openrice/zh/hongkong/offers', + parameters: { lang: '语言,缺省为 zh' }, + name: '香港餐廳精選優惠券', + description: ` +| 简体 | 繁體 | EN | +| ----- | ------ | ----- | +| zh-cn | zh | en | + `, +}; + +async function handler(ctx) { + const lang = ctx.req.param('lang') ?? 'zh'; + + const apiPath = '/api/offers'; + let urlPath: string; + switch (lang) { + case 'zh-cn': + urlPath = '/zh-cn/hongkong/offers'; + break; + case 'en': + urlPath = '/en/hongkong/offers'; + break; + case 'zh': + default: + urlPath = '/zh/hongkong/offers'; + break; + } + const response = await ofetch(baseUrl + apiPath, { + headers: { + accept: 'application/json', + }, + query: { + uiLang: lang, + uiCity: 'hongkong', + page: 1, + sortBy: 'PublishTime', + couponTypeId: 1, + }, + }); + const pageInfo = response.pageInfo; + const highlightedOffers = response.highlightedOffers; + const normalOffers = response.searchResult.paginationResult.results; + const data = [...highlightedOffers, ...normalOffers]; + + const resultList = data.map((item) => { + const title = item.title ?? ''; + const link = baseUrl + item.urlUI; + const coverImg = item.doorPhotoUI.urls.full ?? ''; + const descriptionText = item.couponType === 0 ? item.poiNameUI : `${item.desc} (${item.startTimeUI} - ${item.expireTimeUI}) [${item.multiplePoiDistrictName}]`; + const description = art(path.join(__dirname, 'templates/description.art'), { + description: descriptionText, + image: coverImg, + }); + return { + title, + description, + link, + }; + }); + + return { + title: pageInfo.seoInfo.title ?? 'OpenRice Hong Kong Offers', + link: baseUrl + urlPath, + description: pageInfo.seoInfo.metadataDictionary.name.find((item: { key: string; value: string }) => item.key === 'description')?.value ?? 'OpenRice Hong Kong Offers', + item: resultList, + }; +} diff --git a/lib/routes/openrice/promos.ts b/lib/routes/openrice/promos.ts new file mode 100644 index 00000000000000..d0d9b7b2bb4727 --- /dev/null +++ b/lib/routes/openrice/promos.ts @@ -0,0 +1,74 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); +const baseUrl = 'https://www.openrice.com'; + +export const route: Route = { + path: '/:lang/hongkong/promos', + maintainers: ['after9'], + handler, + categories: ['shopping'], + example: '/openrice/zh/hongkong/promos', + parameters: { lang: '语言,缺省为 zh' }, + name: '香港餐厅滋讯', + description: ` +| 简体 | 繁體 | EN | +| ----- | ------ | ----- | +| zh-cn | zh | en | + `, +}; + +async function handler(ctx) { + const lang = ctx.req.param('lang') ?? 'zh'; + + let urlPath; + switch (lang) { + case 'zh-cn': + urlPath = '/zh-cn/hongkong/promos'; + break; + case 'en': + urlPath = '/en/hongkong/promos'; + break; + case 'zh': + default: + urlPath = '/zh/hongkong/promos'; + break; + } + const response = await ofetch(baseUrl + urlPath, {}); + const $ = load(response); + + const title = $('title').text() ?? "Openrice - What's Hot"; + const description = $('meta[name="description"]').attr('content') ?? "What's Hot from Openrice"; + + const data = $('.article-listing-content-cell-wrapper'); + const resultList = data.toArray().map((item) => { + const $item = $(item); + const title = $item.find('.title-name').text() ?? ''; + const link = $item.find('a.sr1-listing-content-cell').attr('href') ?? ''; + const coverImg = + $item + .find('.cover-photo') + .attr('style') + ?.match(/url\(['"]?(.*?)['"]?\)/)?.[1] ?? null; + const description = art(path.join(__dirname, 'templates/description.art'), { + description: $item.find('.article-details .desc').text() ?? '', + image: coverImg, + }); + return { + title, + description, + link, + }; + }); + + return { + title, + link: baseUrl + urlPath, + description, + item: resultList, + }; +} diff --git a/lib/routes/openrice/templates/chart.art b/lib/routes/openrice/templates/chart.art new file mode 100644 index 00000000000000..7433821a1ddf03 --- /dev/null +++ b/lib/routes/openrice/templates/chart.art @@ -0,0 +1,9 @@ +<h3>Rank: {{ rankNumber }} / {{ title }}</h3> +<p> +{{ each description }} +{{ $value }} +{{ /each }} +</p> +{{ if image }} +<img src="{{ image }}"> +{{ /if }} \ No newline at end of file diff --git a/lib/routes/openrice/templates/description.art b/lib/routes/openrice/templates/description.art new file mode 100644 index 00000000000000..9aa75c6f8c0a8a --- /dev/null +++ b/lib/routes/openrice/templates/description.art @@ -0,0 +1,4 @@ +{{ description }} +{{ if image }} +<img src="{{ image }}"> +{{ /if }} \ No newline at end of file diff --git a/lib/routes/openrice/voting.ts b/lib/routes/openrice/voting.ts new file mode 100644 index 00000000000000..40cddfde0a1b0f --- /dev/null +++ b/lib/routes/openrice/voting.ts @@ -0,0 +1,84 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +const baseUrl = 'https://www.openrice.com'; + +export const route: Route = { + path: '/:lang/hongkong/voting/top/:categoryKey', + maintainers: ['after9'], + handler, + categories: ['shopping'], + example: '/openrice/zh/hongkong/voting/top/chinese', + parameters: { lang: '语言,缺省为 zh', categoryKey: '类别,缺省为 chinese' }, + name: 'OpenRice 開飯熱店 - 年度餐廳投票', + description: ` + lang: 语言,见下方列表 +| 简体 | 繁體 | EN | +| ----- | ------ | ----- | +| zh-cn | zh | en | + + categoryKey: 部分类别,见下方列表 (更多的类别可以在页面的link中对照获取) +| 中菜館 | 上海菜 | 粵菜 | 川菜 | 港式 | 粥粉麵店 | 廚師發辦 | 韓國菜 | 泰國菜 | 越南菜 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| chinese | shanghainese | guangdong | sichuan | hkstyle | congee_noodles | omakase | korean | thai | vietnamese | + `, +}; + +async function handler(ctx) { + const lang = ctx.req.param('lang') ?? 'zh'; + const categoryKey = ctx.req.param('categoryKey') ?? 'chinese'; + + const apiPath = '/api/v2/voting/search/poi'; + const urlPath = `/${lang}/hongkong/voting/top`; + let title: string, description: string; + switch (lang) { + case 'zh-cn': + title = 'OpenRice 开饭热店'; + description = 'OpenRice用戶可以在網站或手機應用程式,點擊餐廳頁面中「投票」按鈕,即可完成投票。參加投票的用戶有機會參加大抽獎,贏取豐富獎品。'; + break; + case 'en': + title = 'OpenRice Best Restaurant'; + description = + 'OpenRice users can vote by clicking the "Vote" button on the restaurant page on the website or mobile app. Voters will have the opportunity to participate in the grand lottery and win grand prizes.'; + break; + case 'zh': + default: + title = 'OpenRice 開飯熱店'; + description = 'OpenRice用戶可以在網站或手機應用程式,點擊餐廳頁面中「投票」按鈕,即可完成投票。參加投票的用戶有機會參加大抽獎,贏取豐富獎品。'; + } + const response = await ofetch(baseUrl + apiPath, { + headers: { + accept: 'application/json', + }, + query: { + uiLang: lang, + uiCity: 'hongkong', + categoryKey, + shortlistIndexLt: 20, + startAt: 0, + regionId: 0, + rows: 20, + needTag: true, + _isPrivate: true, + }, + }); + + const data = response.paginationResult.results; + + const resultList = data.map((item) => { + const title = item.name ?? ''; + const link = `${baseUrl}/${lang}/hongkong/r-${item.name}-r${item.poiId}`; + const description = `${item.district.name}-${item.categories.map((category) => category.name).join('-')}`; + return { + title, + description, + link, + }; + }); + + return { + title, + link: baseUrl + urlPath, + description, + item: resultList, + }; +} diff --git a/lib/routes/openwrt/namespace.ts b/lib/routes/openwrt/namespace.ts index d3fb396c483047..ffef97a8740914 100644 --- a/lib/routes/openwrt/namespace.ts +++ b/lib/routes/openwrt/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Unknown', + name: 'OpenWrt', url: 'openwrt.org', + lang: 'en', }; diff --git a/lib/routes/orcid/namespace.ts b/lib/routes/orcid/namespace.ts index b425eea7e83dc3..086e92e50ab206 100644 --- a/lib/routes/orcid/namespace.ts +++ b/lib/routes/orcid/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ORCID', url: 'orcid.org', + lang: 'en', }; diff --git a/lib/routes/oreno3d/main.ts b/lib/routes/oreno3d/main.ts index 33ecf7aaa4b42c..b43a60c8438aa4 100644 --- a/lib/routes/oreno3d/main.ts +++ b/lib/routes/oreno3d/main.ts @@ -56,8 +56,8 @@ export const route: Route = { maintainers: ['xueli_sherryli'], handler, description: `| favorites | hot | latest | popularity | - | --------- | --- | ------ | ---------- | - | favorites | hot | latest | popularity |`, +| --------- | --- | ------ | ---------- | +| favorites | hot | latest | popularity |`, }; async function handler(ctx) { diff --git a/lib/routes/oreno3d/namespace.ts b/lib/routes/oreno3d/namespace.ts index afedd31953c277..50dcc1d5c95a67 100644 --- a/lib/routes/oreno3d/namespace.ts +++ b/lib/routes/oreno3d/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '俺の 3D エロ動画 (oreno3d)', url: 'oreno3d.com', - description: `:::tip + description: `::: tip You can use some RSS parsing libraries (like \`feedpraser\` in \`Python\`) to receive the video update messages and download them automatically :::`, + lang: 'ja', }; diff --git a/lib/routes/oschina/column.ts b/lib/routes/oschina/column.ts new file mode 100644 index 00000000000000..7a72c2881a9d9b --- /dev/null +++ b/lib/routes/oschina/column.ts @@ -0,0 +1,246 @@ +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'; +import timezone from '@/utils/timezone'; + +const __dirname = getCurrentPath(import.meta.url); + +export const handler = async (ctx: Context): Promise<Data> => { + const { id } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '10', 10); + + const baseUrl: string = 'https://www.oschina.net'; + const userHostRegex: string = String.raw`https://my\.oschina\.net`; + const targetUrl: string = new URL(`news/column?columnId=${id}`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + items = $('div.news-item') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio<Element> = $(el); + + const title: string = $el.find('div.title').text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + intro: $el.find('div.description p.line-clamp').text(), + }); + const pubDateStr: string | undefined = $el.find('inddiv.item').contents().last().text().trim(); + const linkUrl: string | undefined = $el.attr('data-url'); + const authorEls: Element[] = $el.find('inddiv.item a').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $authorEl: Cheerio<Element> = $(authorEl); + + return { + name: $authorEl.text(), + url: $authorEl.attr('href'), + }; + }); + const image: string | undefined = $el.find('img').attr('src'); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : undefined, + link: linkUrl, + author: authors, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? timezone(parseDate(upDatedStr), +8) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + $$('.ad-wrap').remove(); + + const title: string = $$('h1.article-box__title').text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.content').html(), + }); + const pubDateEl: Element = $$('div.article-box__meta div.item-list div.item') + .toArray() + .find((i) => /\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}/.test($$(i).text())); + const pubDateStr: string | undefined = pubDateEl ? $$(pubDateEl).text() : undefined; + const linkUrl: string | undefined = $$('val[data-name="shareUrl"]').attr('data-value'); + const categoryEls: Element[] = [...$$('div.breadcrumb-box a.item').toArray().slice(0, -1), ...$$('div.article-box__meta div.item-list div.item span.label').toArray(), ...$$('div.tags-box a.tag-item').toArray()]; + const categories: string[] = [...new Set(categoryEls.map((el) => $$(el).text()).filter(Boolean))]; + const authorEls: Element[] = $$('div.article-box__meta div.item-list div.item a') + .toArray() + .filter((i) => ($$(i).attr('href') ? new RegExp(`^${userHostRegex}/u/\\d+$`).test($$(i).attr('href') as string) : false)); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $authorEl: Cheerio<Element> = $$(authorEl); + + return { + name: $authorEl.text(), + url: $authorEl.attr('href'), + }; + }); + const guid: string = `oschina-${$$('val[data-name="objId"]').attr('data-value')}`; + const image: string | undefined = $$('val[data-name="sharePic"]').attr('data-value'); + const upDatedStr: string | undefined = $$('meta[property="bytedance:updated_time"]').attr('content') || pubDateStr; + + let processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : item.pubDate, + link: linkUrl ? new URL(linkUrl, baseUrl).href : item.link, + category: categories, + author: authors, + guid, + id: guid, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + const extraLinkEls: Element[] = $$('div.related-links-box ul.link-list li a').toArray(); + const extraLinks = extraLinkEls + .map((extraLinkEl) => { + const $$extraLinkEl: Cheerio<Element> = $$(extraLinkEl); + + return { + url: $$extraLinkEl.attr('href'), + type: 'related', + content_html: $$extraLinkEl.parent().html(), + }; + }) + .filter((_): _ is { url: string; type: string; content_html: string } => true); + + if (extraLinks) { + processedItem = { + ...processedItem, + _extra: { + links: extraLinks, + }, + }; + } + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const author: string | undefined = $('a.logo').attr('title'); + + return { + title: `${author} - ${$('div#tabDropdownListOpen a.selected').text()}`, + description: $('meta[name="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('a.logo img').attr('src'), + author, + language, + id: $('val[data-name="weixinShareUrl"]').attr('data-value'), + }; +}; + +export const route: Route = { + path: '/column/:id', + name: '专栏', + url: 'www.oschina.net', + maintainers: ['nczitzk'], + handler, + example: '/oschina/column/14', + parameters: { + id: '专栏 id,可在对应专栏页 URL 中找到', + }, + description: `:::tip +若订阅 [开源安全专栏](https://www.oschina.net/news/column?columnId=14),网址为 \`https://www.oschina.net/news/column?columnId=14\`,请截取 \`https://www.oschina.net/news/column?columnId=\` 到末尾的部分 \`14\` 作为 \`id\` 参数填入,此时目标路由为 [\`/oschina/column/14\`](https://rsshub.app/oschina/column/14)。 + +::: + +<details> +<summary>更多专栏</summary> + +| 名称 | ID | +| --------------- | --- | +| 古典主义 Debian | 4 | +| 自由&开源 | 5 | +| 溯源 | 6 | +| 开源先懂协议 | 7 | +| 开源变局 | 8 | +| 创造者说 | 9 | +| 精英主义 BSD | 10 | +| 苹果有开源 | 11 | +| 开源访谈 | 12 | +| 抱团找组织 | 13 | +| 开源安全 | 14 | +| OSPO | 15 | +| 创业小辑 | 16 | +| 星推荐 | 17 | +| 单口开源 | 18 | +| 编辑部观察直播 | 19 | +| 开源商业化 | 20 | +| ChatGPT 专题 | 21 | +| 开源新思 | 24 | +| 开源日报 | 25 | +| 大模型思辨 | 26 | +| 家里有个程序员 | 27 | +| 开源漫谈 | 23 | + +</details> +`, + categories: ['programming'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.oschina.net'], + target: (_, url) => { + const urlObj: URL = new URL(url); + const id: string | undefined = urlObj.searchParams.get('id') ?? undefined; + + return `/oschina/column/${id}`; + }, + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/oschina/event.ts b/lib/routes/oschina/event.ts new file mode 100644 index 00000000000000..91ddd19bc7fc4a --- /dev/null +++ b/lib/routes/oschina/event.ts @@ -0,0 +1,243 @@ +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<Data> => { + const { category = 'latest' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'https://www.oschina.net'; + const targetUrl: string = new URL(`event?tab=${category}`, baseUrl).href; + const apiUrl: string = new URL('action/ajax/get_more_event_list', baseUrl).href; + + const response = await ofetch(apiUrl, { + method: 'post', + body: { + tab: category, + }, + }); + const $: CheerioAPI = load(response); + + const targetResponse = await ofetch(targetUrl); + const $target: CheerioAPI = load(targetResponse); + const language = $target('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + items = $('div.event-item') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio<Element> = $(el); + + const title: string = $el.find('a.summary').text(); + const image: string | undefined = $el.find('header.item-banner img').attr('data-delay'); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + description: $el.html(), + }); + const pubDateStr: string | undefined = $el.find('footer.when-where label').first().text(); + const linkUrl: string | undefined = $el.find('a.summary').attr('href'); + const categoryEl: Element = $el.find('footer.when-where label').last(); + const categories: string[] = [categoryEl.text()]; + const authorEls: Element[] = $el.find('div.sponsor').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $authorEl: Cheerio<Element> = $(authorEl); + + return { + name: $authorEl.find('span').text(), + avatar: $authorEl.find('img').attr('data-delay'), + }; + }); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('h1').text(); + const image: string | undefined = $$('div.event-img img').attr('src'); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + description: $$('div.event-detail').html(), + }); + const pubDateStr: string | undefined = $$('span.box-fl') + .filter((_, el) => $$(el).text().includes('时间')) + .next() + .text() + ?.split('至')[0] + ?.trim(); + const linkUrl: string | undefined = $$('val[data-name="weixinUrl"]').attr('data-value'); + const categories: string[] = [...(item.category ?? []), $$('div.cost span.c').text()].filter(Boolean); + const authorEls: Element[] = $$('div.user-list div.box-aw').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $$authorEl: Cheerio<Element> = $$(authorEl); + + return { + name: $$authorEl.find('h3').text(), + url: $$authorEl.find('a').attr('href'), + avatar: $$authorEl.prev().find('img').attr('src'), + }; + }); + const upDatedStr: string | undefined = pubDateStr; + + let processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + link: linkUrl ?? item.link, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + const extraLinkEls: Element[] = $$('div.aside-list ul li').toArray(); + const extraLinks = extraLinkEls + .map((extraLinkEl) => { + const $$extraLinkEl: Cheerio<Element> = $$(extraLinkEl); + + return { + url: $$extraLinkEl.find('a.list-item').attr('href'), + type: 'related', + content_html: $$extraLinkEl.find('a.list-item').html(), + }; + }) + .filter((_): _ is { url: string; type: string; content_html: string } => true); + + if (extraLinks) { + processedItem = { + ...processedItem, + _extra: { + links: extraLinks, + }, + }; + } + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: $target('title').text(), + description: $target('meta[name="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/event/:category?', + name: '活动', + url: 'www.oschina.net', + maintainers: ['nczitzk'], + handler, + example: '/oschina/event', + parameters: { + category: '分类,默认为 `latest`,即最新活动,可在对应分类页 URL 中找到', + }, + description: `:::tip +若订阅 [强力推荐](https://www.oschina.net/event?tab=recommend),网址为 \`https://www.oschina.net/event?tab=recommend\`,请截取 \`https://www.oschina.net/event?tab=\` 到末尾的部分 \`recommend\` 作为 \`category\` 参数填入,此时目标路由为 [\`/oschina/event/recommend\`](https://rsshub.app/oschina/event/recommend)。 +::: + +| 强力推荐 | 最新活动 | +| --------- | -------- | +| recommend | latest | +`, + categories: ['programming'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.oschina.net'], + target: (_, url) => { + const urlObj: URL = new URL(url); + const category: string | undefined = urlObj.searchParams.get('tab') ?? undefined; + + return `/oschina/event${category ? `/${category}` : ''}`; + }, + }, + { + title: '强力推荐', + source: ['www.oschina.net'], + target: '/event/recommend', + }, + { + title: '最新活动', + source: ['www.oschina.net'], + target: '/event/latest', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/oschina/namespace.ts b/lib/routes/oschina/namespace.ts index c14d84446e2012..c3404656a29a78 100644 --- a/lib/routes/oschina/namespace.ts +++ b/lib/routes/oschina/namespace.ts @@ -3,4 +3,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '开源中国', url: 'oschina.net', + lang: 'zh-CN', + description: 'OSCHINA', }; diff --git a/lib/routes/oschina/news.ts b/lib/routes/oschina/news.ts index 06b7027710519a..29ad7c1548a124 100644 --- a/lib/routes/oschina/news.ts +++ b/lib/routes/oschina/news.ts @@ -1,9 +1,11 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate, parseRelativeDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; import { load } from 'cheerio'; +import { fetchArticle } from '@/utils/wechat-mp'; +import { config } from '@/config'; const configs = { all: { @@ -56,8 +58,8 @@ export const route: Route = { maintainers: ['tgly307', 'zengxs'], handler, description: `| [综合资讯][osc_gen] | [软件更新资讯][osc_proj] | [行业资讯][osc_ind] | [编程语言资讯][osc_pl] | - | ------------------- | ------------------------ | ------------------- | ---------------------- | - | industry | project | industry-news | programming | +| ------------------- | ------------------------ | ------------------- | ---------------------- | +| industry | project | industry-news | programming | 订阅 [全部板块资讯][osc_all] 可以使用 [https://rsshub.app/oschina/news](https://rsshub.app/oschina/news) @@ -72,17 +74,36 @@ export const route: Route = { [osc_pl]: https://www.oschina.net/news/programming "开源中国 - 编程语言资讯"`, }; +const getCookie = () => + cache.tryGet( + 'oschina:cookie', + async () => { + const res = await ofetch.raw('https://www.oschina.net/news'); + const cookie = res.headers + .getSetCookie() + .map((i) => i.split(';')[0]) + .join('; '); + return cookie; + }, + config.cache.routeExpire, + false + ); + async function handler(ctx) { const category = ctx.req.param('category') ?? 'all'; const config = configs[category]; - const res = await got(config.ajaxUrl, { + const cookie = await getCookie(); + const res = await ofetch(config.ajaxUrl, { headers: { Referer: config.link, 'X-Requested-With': 'XMLHttpRequest', + Cookie: cookie, }, }); - const $ = load(res.data); + const $ = load(res); + + $('.ad-wrap').remove(); const list = $('.items .news-item') .toArray() @@ -101,26 +122,28 @@ async function handler(ctx) { list.map((item) => cache.tryGet(item.link, async () => { if (/^https?:\/\/(my|www)\.oschina.net\/.*$/.test(item.link)) { - const detail = await got(item.link, { + const detail = await ofetch(item.link, { headers: { Referer: config.link, + Cookie: cookie, }, }); - const content = load(detail.data); + const content = load(detail); content('.ad-wrap').remove(); item.description = content('.article-detail').html(); item.author = content('.article-box__meta .item').first().text(); - } - if (/^https?:\/\/gitee.com\/.*$/.test(item.link)) { - const detail = await got(item.link, { + } else if (/^https?:\/\/gitee\.com\/.*$/.test(item.link)) { + const detail = await ofetch(item.link, { headers: { Referer: config.link, }, }); - const content = load(detail.data); + const content = load(detail); item.description = content('.file_content').html(); + } else if (/^https?:\/\/osc\.cool\/.*$/.test(item.link)) { + return fetchArticle(item.link, true); } return item; }) diff --git a/lib/routes/oschina/templates/description.art b/lib/routes/oschina/templates/description.art new file mode 100644 index 00000000000000..249654e7e618a4 --- /dev/null +++ b/lib/routes/oschina/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} + <figure> + <img + {{ if image.alt }} + alt="{{ image.alt }}" + {{ /if }} + src="{{ image.src }}"> + </figure> + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} + <blockquote>{{ intro }}</blockquote> +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/oschina/user.ts b/lib/routes/oschina/user.ts index 3c0198625cef75..c5069c72ef168c 100644 --- a/lib/routes/oschina/user.ts +++ b/lib/routes/oschina/user.ts @@ -69,7 +69,7 @@ async function handler(ctx) { return { title: author + '的博客', description: $('.user-text .user-signature').text(), - link: `https://my.oschina.net/${id ?? uid}`, + link: `https://my.oschina.net/u/${id ?? uid}`, item: resultItem, }; } diff --git a/lib/routes/oshwhub/namespace.ts b/lib/routes/oshwhub/namespace.ts index b03916bfa6a751..6dfdaeaa2a049c 100644 --- a/lib/routes/oshwhub/namespace.ts +++ b/lib/routes/oshwhub/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'oshwhub 立创开源硬件平台', url: 'oshwhub.com', + lang: 'zh-CN', }; diff --git a/lib/routes/osu/beatmaps/latest-ranked.ts b/lib/routes/osu/beatmaps/latest-ranked.ts new file mode 100644 index 00000000000000..9462c0fd102e02 --- /dev/null +++ b/lib/routes/osu/beatmaps/latest-ranked.ts @@ -0,0 +1,313 @@ +import { Data, DataItem, Route } from '@/types'; +import path from 'node:path'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import { config } from '@/config'; +import { art } from '@/utils/render'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +const actualParametersDescTable = ` +| Name | Default | Description | +| ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| \`includeMode\` | All mode | Could be \`osu\`, \`mania\`, \`fruits\` or \`taiko\`. Specify included game mode of beatmaps. Including this paramseter multiple times to specify multiple game modes, e.g.: \`includeMode=osu&includeMode=mania\`. Subscribe to all game modes if not specified | +| \`difficultyLimit\` | None | Lower/upper limit of star rating of the beatmaps in the beatmapset item, e.g.:\`difficultyLimit=U6\`. Checkout tips in descriptions for detailed explaination and examples. | +| \`modeInTitle\` | \`true\` | \`true\` or \`false\` Add mode info into feed title. +`; + +const descriptionDoc: string = ` +Subscribe to the new beatmaps on https://osu.ppy.sh/beatmapsets. + +#### Parameter Description + +Parameters allows you to: + +- Filter game mode +- Limit beatmap difficulty +- Show/hide game mode in feed title + +Below is a table of all allowed parameters passed to \`routeParams\` + +${actualParametersDescTable} + +This actual parameters should be passed as \`routeParams\` in URL Query String format without \`?\`, e.g.: + + /osu/latest-ranked/modeInTitle=true&includeMode=osu + +:::tip +You could make use of \`difficultyLimit\` paramters to create a "high difficulty/low difficulty only" only feed. + +For example, if you only wants to play low star rating beatmap like 1 or 2 star, you could subscribe to: + + /osu/latest-ranked/difficultyLimit=U2 + +This will filter out all beatmapsets that do not provide at least one beatmap with star rating<=\`2.00\`. + +Similarly, you could use lower bound to filter out beatmapsets which don't have at least one beatmap +with star rating higher than a certain threshold. + + /osu/latest-ranked/difficultyLimit=L6 + +Now all beatmapsets that don't provided at least one beatmap with star rating higher than \`6.00\` will be filtered. +::: +`; + +export const route: Route = { + path: '/latest-ranked/:routeParams?', + categories: ['game'], + example: '/osu/latest-ranked/includeMode=osu&difficultyLimit=L3&difficultyLimit=U7', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + supportRadar: true, + }, + parameters: { + routeParams: { + description: 'Used to pass route parameters in Query String format. Check out route description for more info.', + default: 'null', + }, + }, + name: 'Latest Ranked Beatmap', + description: descriptionDoc, + maintainers: ['nfnfgo'], + radar: [ + { + source: ['osu.ppy.sh/beatmapsets'], + }, + ], + handler, +}; + +interface Beatmap { + beatmapset_id: number; + difficulty_rating: number; + id: number; + mode: string; + status: string; + total_length: number; + user_id: number; + version: string; + accuracy: number; + ar: number; + bpm: number; + convert: boolean; + count_circles: number; + count_sliders: number; + count_spinners: number; + cs: number; + deleted_at: string | null; + drain: number; + hit_length: number; + is_scoreable: boolean; + last_updated: string; + mode_int: number; + passcount: number; + playcount: number; + ranked: number; + url: string; + checksum: string; + max_combo: number; +} + +interface NominationsSummary { + current: number; + eligible_main_rulesets: string[]; + required_meta: { + main_ruleset: number; + non_main_ruleset: number; + }; +} + +interface Covers { + cover: string; + 'cover@2x': string; + card: string; + 'card@2x': string; + list: string; + 'list@2x': string; + slimcover: string; + 'slimcover@2x': string; +} + +interface Availability { + download_disabled: boolean; + more_information: string | null; +} + +interface BeatmapsetInfo { + artist: string; + artist_unicode: string; + covers: Covers; + creator: string; + favourite_count: number; + hype: null | string; + id: number; + nsfw: boolean; + offset: number; + play_count: number; + preview_url: string; + source: string; + spotlight: boolean; + status: string; + title: string; + title_unicode: string; + track_id: number; + user_id: number; + video: boolean; + bpm: number; + can_be_hyped: boolean; + deleted_at: string | null; + discussion_enabled: boolean; + discussion_locked: boolean; + is_scoreable: boolean; + last_updated: string; + legacy_thread_url: string; + nominations_summary: NominationsSummary; + ranked: number; + ranked_date: string; + storyboard: boolean; + submitted_date: string; + tags: string; + availability: Availability; + beatmaps: Beatmap[]; + pack_tags: string[]; +} + +async function handler(ctx): Promise<Data> { + // Parse & retrive searchParams + const pathParams = ctx.req.param('routeParams'); + // Here user actually pass the query using path param, like: `/osu/latest-ranked/includeMode=osu` + // We first retrieve path param part: `includeMode=osu`, then concat it with host to construct a "fake" URL: + // `https://osu.ppy.sh?includeMode=osu` + // Then we use URL.searchParams to parse and retrieve params from this "fake" URL. + const searchParams = new URL(`https://osu.ppy.sh?${pathParams}`).searchParams; // use URL to parse params + const includeModes = searchParams.getAll('includeMode'); + const difficultyLimits = searchParams.getAll('difficultyLimit'); + const modeInTitle = searchParams.get('modeInTitle') ?? 'true'; // show mode name in title, default to true. + + // fetch beatmap JSON info from website within cache + let beatmapsetList = (await cache.tryGet( + 'https://osu.ppy.sh/beatmapsets:JSON', + async () => { + const link = 'https://osu.ppy.sh/beatmapsets'; + + const response = await got.get(link); + const $ = load(response.data); + + const beatmapInfo = JSON.parse($('#json-beatmaps').text() ?? '{"beatmapsets": undefined}'); + + const beatmapList: BeatmapsetInfo[] = beatmapInfo.beatmapsets; + + // Failed to fetch, raise error + if (beatmapList === undefined) { + throw new Error('Failed to retrieve JSON beatmap info from osu! website'); + } + + return beatmapList; + }, + config.cache.routeExpire, + false + )) as BeatmapsetInfo[]; + + // Sort beatmap by difficultyRate.desc + // This step is necessary even if difficultyLimit not enabled, since we want the beatmap + // in RSS description sorted when displayed + for (const item of beatmapsetList) { + item.beatmaps.sort((a, b) => a.difficulty_rating - b.difficulty_rating); + } + + // filter beatmapset types + // Note: + // One Osu beatmapset could actually contains several beatmaps with different game mode. + // Here for simplicity we just use the mode of first beatmap in this set for filtering criteria. + if (includeModes?.length && includeModes?.length > 0) { + beatmapsetList = beatmapsetList.filter((bm) => includeModes.includes(bm.beatmaps[0].mode)); + } + + let upperLimit = 99; // Osu! will never have maps with 99+ star rating right? + let lowerLimit = 0; + if (difficultyLimits && difficultyLimits.length > 0 && difficultyLimits.length < 2) { + for (const dfLimit of difficultyLimits) { + if (dfLimit.startsWith('U')) { + upperLimit = Number.parseFloat(dfLimit.substring(1)); + } else if (dfLimit.startsWith('L')) { + lowerLimit = Number.parseFloat(dfLimit.substring(1)); + } + } + + const difficultyRateFilterFunc = (item: BeatmapsetInfo): boolean => { + if (item.beatmaps.at(0)!.difficulty_rating > upperLimit) { + return false; + } + if (item.beatmaps.at(-1)!.difficulty_rating < lowerLimit) { + return false; + } + return true; + }; + + beatmapsetList = beatmapsetList.filter((item) => difficultyRateFilterFunc(item)); + } + + // Returns a user-readble string that shows details about route parameter config + function getReadableFeedConfig(): string { + if (!pathParams) { + return ''; + } + + let readableConf = 'Feed Configurations:\n'; + readableConf += `Game Mode: ${includeModes.length > 0 ? JSON.stringify(includeModes) : 'All modes'}\n`; + readableConf += `Star Rating Limit: Lower=${lowerLimit}, Upper=${upperLimit}`; + return readableConf; + } + + // Construct beatmap feed items + const rssItems: DataItem[] = beatmapsetList.map((beatmapset) => { + // Format publication date using parseDate utility + // Here it make sense to consider the ranked date as the pubDate of this item since this is ranked map RSS + const pubDate = parseDate(beatmapset.ranked_date); + + // Select the best resolution cover (2x if available) + const coverImage = beatmapset.covers['cover@2x'] || beatmapset.covers.cover; + const bannerImage = beatmapset.covers['card@2x'] || beatmapset.covers.card; + + // Readable beatmap total length + const readableTotalLength = `${Math.floor(beatmapset.beatmaps[0].total_length / 60) + .toString() + .padStart(2, '0')}:${(beatmapset.beatmaps[0].total_length % 60).toString().padStart(2, '0')}`; + + const modeLiteralToDisplayNameMap = { + osu: 'Osu!', + fruits: 'Osu!Catch', + taiko: 'Osu!Taiko', + mania: 'Osu!Mania', + }; + + // Create a description with beatmap details and a table of difficulties + const description = art(path.join(__dirname, 'templates/beatmapset.art'), { ...beatmapset, readableTotalLength, modeLiteralToDisplayNameMap }); + + return { + title: `${modeInTitle === 'true' ? `[${modeLiteralToDisplayNameMap[beatmapset.beatmaps[0].mode]}] ` : ``}${beatmapset.title_unicode ?? beatmapset.title}`, + description, + pubDate, + link: `https://osu.ppy.sh/beatmapsets/${beatmapset.id}`, + category: ['osu!', 'game'], + author: [{ name: beatmapset.creator }], + image: coverImage, + banner: bannerImage, + updated: beatmapset.last_updated, + }; + }); + + return { + title: 'Osu! Latest Ranked Map', + link: 'https://osu.ppy.sh/beatmapsets', + description: `Newly ranked beatmaps at https://osu.ppy.sh/beatmapsets.\n${getReadableFeedConfig()}`, + item: rssItems, + }; +} diff --git a/lib/routes/osu/beatmaps/templates/beatmapset.art b/lib/routes/osu/beatmaps/templates/beatmapset.art new file mode 100644 index 00000000000000..5d9c50f3daab38 --- /dev/null +++ b/lib/routes/osu/beatmaps/templates/beatmapset.art @@ -0,0 +1,37 @@ +<img src="{{ covers['cover@2x'] || covers.cover }}" alt="{{ title }}" style="max-width: 100%; height: auto;" /> + +<h3>Song Info</h3> +<ul> + <li><strong>English Title:</strong> {{ title }}</li> + <li><strong>Artist:</strong> {{ artist_unicode }} ({{ artist }})</li> + <li><strong>Length:</strong> {{ readableTotalLength }}</li> + <li><strong>BPM:</strong> {{ bpm }}</li> +</ul> + +<h3>Beatmapset Info</h3> +<ul> + <li><strong>Mode:</strong> {{ modeLiteralToDisplayNameMap[beatmaps[0].mode] }}</li> + <li><strong>Creator:</strong> {{ creator }}</li> +</ul> + +<h3>Difficulties</h3> +<table border="1"> + <thead> + <tr> + <th>Version</th> + <th>Rating</th> + <th>AR</th> + <th>Drain</th> + </tr> + </thead> + <tbody> + {{ each beatmaps as beatmap }} + <tr> + <td><a href="{{ beatmap.url }}" target="_blank">{{ beatmap.version }}</a></td> + <td>{{ beatmap.difficulty_rating.toFixed(2) }}</td> + <td>{{ beatmap.ar.toFixed(1) }}</td> + <td>{{ beatmap.drain }}</td> + </tr> + {{ /each }} + </tbody> +</table> diff --git a/lib/routes/osu/namespace.ts b/lib/routes/osu/namespace.ts index 0ae63e530dbc05..51b580a3200fc2 100644 --- a/lib/routes/osu/namespace.ts +++ b/lib/routes/osu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'osu!', url: 'osu.ppy.sh', + lang: 'en', }; diff --git a/lib/routes/otobanana/namespace.ts b/lib/routes/otobanana/namespace.ts index d7fc4fe8d6b235..ecfa30fc89b356 100644 --- a/lib/routes/otobanana/namespace.ts +++ b/lib/routes/otobanana/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'OTOBANANA', url: 'otobanana.com', + lang: 'zh-CN', }; diff --git a/lib/routes/ouc/it-tx.ts b/lib/routes/ouc/it-tx.ts index 88290eec2723c5..c63c228b2d0b0e 100644 --- a/lib/routes/ouc/it-tx.ts +++ b/lib/routes/ouc/it-tx.ts @@ -28,8 +28,8 @@ export const route: Route = { handler, url: 'it.ouc.edu.cn/', description: `| 新闻动态 | 学院活动 | 奖助工作获奖情况 | - | -------- | -------- | ---------------- | - | xwdt | tzgg | 21758 |`, +| -------- | -------- | ---------------- | +| xwdt | tzgg | 21758 |`, }; async function handler(ctx) { diff --git a/lib/routes/ouc/it.ts b/lib/routes/ouc/it.ts index 507fbe06cf461b..23bacb0aaeeba8 100644 --- a/lib/routes/ouc/it.ts +++ b/lib/routes/ouc/it.ts @@ -28,8 +28,8 @@ export const route: Route = { handler, url: 'it.ouc.edu.cn/', description: `| 学院要闻 | 学院公告 | 学院活动 | - | -------- | -------- | -------- | - | 0 | 1 | 2 |`, +| -------- | -------- | -------- | +| 0 | 1 | 2 |`, }; async function handler(ctx) { diff --git a/lib/routes/ouc/jwgl.ts b/lib/routes/ouc/jwgl.ts index a26401cb55f547..bdeb5a02dbdd97 100644 --- a/lib/routes/ouc/jwgl.ts +++ b/lib/routes/ouc/jwgl.ts @@ -26,9 +26,9 @@ export const route: Route = { maintainers: ['3401797899'], handler, url: 'jwgl.ouc.edu.cn/cas/login.action', - description: `:::warning + description: `::: warning 由于选课通知仅允许校园网访问,需自行部署。 - :::`, +:::`, }; async function handler() { diff --git a/lib/routes/ouc/namespace.ts b/lib/routes/ouc/namespace.ts index 4ece75f72239be..9a77bbbe12664c 100644 --- a/lib/routes/ouc/namespace.ts +++ b/lib/routes/ouc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国海洋大学', url: 'it.ouc.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/oup/index.ts b/lib/routes/oup/index.ts index b4c056e476009b..278efc5927b2e2 100644 --- a/lib/routes/oup/index.ts +++ b/lib/routes/oup/index.ts @@ -3,7 +3,7 @@ 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 { parseDate } from '@/utils/parse-date'; import path from 'node:path'; @@ -29,46 +29,47 @@ export const route: Route = { source: ['academic.oup.com/', 'academic.oup.com/:name/issue'], }, ], - name: 'Oxford Academic', - maintainers: [], + name: 'Oxford Academic - Journal', + maintainers: ['Fatpandac'], handler, url: 'academic.oup.com/', - description: `#### Journal {#oxford-university-press-oxford-academic-journal}`, }; async function handler(ctx) { const name = ctx.req.param('name'); const url = `${rootUrl}/${name}/issue`; - const response = await got(url); - const cookies = response.headers['set-cookie'].map((item) => item.split(';')[0]).join(';'); - const $ = load(response.data); + const response = await ofetch.raw(url); + const cookies = response.headers + .getSetCookie() + .map((item) => item.split(';')[0]) + .join(';'); + const $ = load(response._data); const list = $('div.al-article-items') - .map((_, item) => ({ + .toArray() + .map((item) => ({ title: $(item).find('a.at-articleLink').text(), link: new URL($(item).find('a.at-articleLink').attr('href'), rootUrl).href, - })) - .get(); + })); const items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { - const detailResponse = await got(item.link, { + const detailResponse = await ofetch(item.link, { headers: { Cookie: cookies, }, }); - const content = load(detailResponse.data); + const $ = load(detailResponse); - item.author = content('a.linked-name.js-linked-name-trigger').text(); + item.author = $('.al-authors-list button').text(); item.description = art(path.join(__dirname, 'templates/article.art'), { - abstractContent: content('section.abstract > p.chapter-para').text(), - keywords: content('div.kwd-group > a') - .map((_, item) => $(item).text()) - .get() - .join(','), + abstractContent: $('section.abstract > p.chapter-para').text(), }); - item.pubDate = parseDate(content('div.citation-date').text()); + item.pubDate = parseDate($('div.citation-date').text()); + item.category = $('div.kwd-group > a') + .toArray() + .map((item) => $(item).text()); return item; }) diff --git a/lib/routes/oup/namespace.ts b/lib/routes/oup/namespace.ts index 0be762567e7a42..178e1879261d61 100644 --- a/lib/routes/oup/namespace.ts +++ b/lib/routes/oup/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Oxford University Press', url: 'academic.oup.com', + lang: 'en', }; diff --git a/lib/routes/oup/templates/article.art b/lib/routes/oup/templates/article.art index 56c9cf29764890..05639ab238edf9 100644 --- a/lib/routes/oup/templates/article.art +++ b/lib/routes/oup/templates/article.art @@ -1,6 +1,2 @@ <h2>Abstract</h2> <p>{{abstractContent}}</p> -{{ if keywords }} -<h2>Keywords</h2> -<p>{{keywords}}</p> -{{ /if }} diff --git a/lib/routes/outagereport/namespace.ts b/lib/routes/outagereport/namespace.ts index 2f29f3a0ce7f28..3303e6a13b1a3d 100644 --- a/lib/routes/outagereport/namespace.ts +++ b/lib/routes/outagereport/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Outage.Report', url: 'outage.report', + lang: 'en', }; diff --git a/lib/routes/p-articles/contributors.ts b/lib/routes/p-articles/contributors.ts new file mode 100644 index 00000000000000..1e5a397bef258a --- /dev/null +++ b/lib/routes/p-articles/contributors.ts @@ -0,0 +1,53 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import { rootUrl, ProcessFeed } from './utils'; + +export const route: Route = { + path: '/contributors/:author', + categories: ['reading'], + example: '/p-articles/contributors/黃衍仁', + parameters: { author: '虛詞作者, 可在作者页面 URL 找到' }, + name: '虛詞作者', + maintainers: ['Insomnia1437'], + handler, + radar: [ + { + source: ['p-articles.com/contributors/:author'], + }, + ], +}; + +async function handler(ctx) { + const authorName: string = ctx.req.param('author'); + const authorUrl = new URL(`/contributors/${authorName}`, rootUrl).href; + const response = await ofetch(authorUrl); + const $ = load(response); + + const list = $('div.contect_box_05in > a') + .map(function () { + const info = { + title: $(this).find('h3').text().trim(), + link: new URL($(this).attr('href'), rootUrl).href, + }; + return info; + }) + .get(); + + const items = await Promise.all( + list.map((info) => + cache.tryGet(info.link, async () => { + const response = await ofetch(info.link); + // const $ = load(response); + return ProcessFeed(info, response); + }) + ) + ); + return { + title: '虚词 p-articles', + link: authorUrl, + item: items, + language: 'zh-cn', + }; +} diff --git a/lib/routes/p-articles/namespace.ts b/lib/routes/p-articles/namespace.ts new file mode 100644 index 00000000000000..81c1ef0a740720 --- /dev/null +++ b/lib/routes/p-articles/namespace.ts @@ -0,0 +1,14 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '虚词', + url: 'p-articles.com', + description: ` +::: tip +p-articles provides some official RSS feeds: + +- section: \`https://p-articles.com/section/:section\` +- contributors: \`https://p-articles.com/contributors/:author\` +:::`, + lang: 'en', +}; diff --git a/lib/routes/p-articles/section.ts b/lib/routes/p-articles/section.ts new file mode 100644 index 00000000000000..dbc3650c36ade2 --- /dev/null +++ b/lib/routes/p-articles/section.ts @@ -0,0 +1,60 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import { rootUrl, ProcessFeed } from './utils'; + +export const route: Route = { + path: '/section/:section', + categories: ['reading'], + example: '/p-articles/section/critics', + parameters: { section: '版块名称, 可在对应版块 URL 中找到, 子版块链接用`-`连接' }, + name: '版块', + maintainers: ['Insomnia1437'], + handler, + radar: [ + { + source: ['p-articles.com/:section/'], + }, + ], +}; + +async function handler(ctx) { + let sectionName: string = ctx.req.param('section'); + sectionName = sectionName.replace('-', '/'); + sectionName += '/'; + const sectionUrl = new URL(sectionName, rootUrl).href; + const response = await ofetch(sectionUrl); + const $ = load(response); + const topInfo = { + title: $('div.inner_top_title_01 > h1 > a').text(), + link: new URL($('div.inner_top_title_01 > h1 > a').prop('href'), rootUrl).href, + }; + + const list = $('div.contect_box_04 > a') + .map(function () { + const info = { + title: $(this).find('h1').text().trim(), + link: new URL($(this).attr('href'), rootUrl).href, + }; + return info; + }) + .get(); + list.unshift(topInfo); + + const items = await Promise.all( + list.map((info) => + cache.tryGet(info.link, async () => { + const response = await ofetch(info.link); + // const $ = load(response); + return ProcessFeed(info, response); + }) + ) + ); + return { + title: '虚词 p-articles', + link: sectionUrl, + item: items, + language: 'zh-cn', + }; +} diff --git a/lib/routes/p-articles/utils.ts b/lib/routes/p-articles/utils.ts new file mode 100644 index 00000000000000..6645ad6c11196c --- /dev/null +++ b/lib/routes/p-articles/utils.ts @@ -0,0 +1,21 @@ +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const rootUrl: string = 'https://p-articles.com'; + +const ProcessFeed = (info, data) => { + // const $ = cheerio.load(data); + const $ = load(data); + const author = $('div.detail_title_02 > h4 > a:nth-child(2)').text().trim(); + info.author = author; + + const dateValue = $('div.detail_title_02 > h4 ').text().trim(); + info.pubDate = timezone(parseDate(dateValue), +8); + + const description = $('div.detail_contect_01').html(); + info.description = description; + return info; +}; + +export { rootUrl, ProcessFeed }; diff --git a/lib/routes/pacilution/latest.ts b/lib/routes/pacilution/latest.ts new file mode 100644 index 00000000000000..fa65b069ceaae6 --- /dev/null +++ b/lib/routes/pacilution/latest.ts @@ -0,0 +1,102 @@ +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'; +import iconv from 'iconv-lite'; + +const BASE_URL = 'http://www.pacilution.com/'; + +const handler: Route['handler'] = async () => { + // Fetch the target page + const response = await got({ + method: 'get', + url: BASE_URL, + responseType: 'buffer', + }); + const $ = load(iconv.decode(response.data, 'gb2312')); + + // Select all list items containing target information + const ITEM_SELECTOR = 'ul[class*="ullbxwnew"] > li'; + const listItems = $(ITEM_SELECTOR); + + // Map through each list item to extract details + const contentLinkList = listItems.toArray().map((element) => { + const title = $(element).find('a').text(); + const relativeHref = $(element).find('a').attr('href') || ''; + const link = `${BASE_URL}${relativeHref}`; + + return { + title, + link, + }; + }); + + return { + title: '普世社会科学研究网最新文章', + description: '普世社会科学研究网首页上不同板块的最新文章汇总集合', + link: BASE_URL, + image: 'http://www.pacilution.com/img/top_banner.jpg', + item: ( + await Promise.all( + contentLinkList.map((item) => + cache.tryGet(item.link, async () => { + try { + const CONTENT_SELECTOR = '#MyContent'; + const DATE_SELECTOR = 'td[class*="con_info"] > span'; + const response = await got({ + method: 'get', + url: item.link, + responseType: 'buffer', + }); + const targetPage = load(iconv.decode(response.data, 'gb2312')); + const content = targetPage(CONTENT_SELECTOR).html() || ''; + const date = parseDate(targetPage(DATE_SELECTOR).text().trim().replaceAll('日', '')).toISOString(); + return { + title: item.title, + pubDate: date, + link: item.link, + description: content, + category: ['journal'], + guid: item.link, + id: item.link, + image: 'http://www.pacilution.com/img/top_banner.jpg', + content, + updated: date, + language: 'zh-cn', + }; + } catch { + return null as unknown as DataItem; + } + }) + ) + ) + ).filter((item) => item !== null) as DataItem[], + allowEmpty: true, + language: 'zh-cn', + feedLink: 'https://rsshub.app/pacilution/latest', + id: 'https://rsshub.app/pacilution/latest', + }; +}; + +export const route: Route = { + path: '/latest', + name: '最新文章', + maintainers: ['PrinOrange'], + handler, + categories: ['journal'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + example: '/pacilution/latest', + radar: [ + { + source: ['www.pacilution.com'], + }, + ], +}; diff --git a/lib/routes/pacilution/namespace.ts b/lib/routes/pacilution/namespace.ts new file mode 100644 index 00000000000000..8951709049d0e5 --- /dev/null +++ b/lib/routes/pacilution/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '普世社会科学研究所', + url: 'www.pacilution.com', +}; diff --git a/lib/routes/panewslab/author.ts b/lib/routes/panewslab/author.ts index d4c66eb323cc49..d8a8c539027fd5 100644 --- a/lib/routes/panewslab/author.ts +++ b/lib/routes/panewslab/author.ts @@ -6,7 +6,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: ['/author/:id', '/column/:id'], - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/panewslab/author/166', parameters: { id: '专栏 id,可在地址栏 URL 中找到' }, features: { diff --git a/lib/routes/panewslab/namespace.ts b/lib/routes/panewslab/namespace.ts index c814b552d84284..f9ac02afec6a2c 100644 --- a/lib/routes/panewslab/namespace.ts +++ b/lib/routes/panewslab/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PANews', url: 'panewslab.com', + lang: 'zh-CN', }; diff --git a/lib/routes/panewslab/news.ts b/lib/routes/panewslab/news.ts index 5a46f25bbba541..cbc24f45aaa0ba 100644 --- a/lib/routes/panewslab/news.ts +++ b/lib/routes/panewslab/news.ts @@ -3,18 +3,9 @@ import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: ['/news', '/newsflash'], - categories: ['new-media'], + path: '/news', + categories: ['new-media', 'popular'], example: '/panewslab/news', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, radar: [ { source: ['panewslab.com/'], @@ -24,7 +15,6 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'panewslab.com/', - url: 'panewslab.com/', }; async function handler(ctx) { diff --git a/lib/routes/panewslab/index.ts b/lib/routes/panewslab/profundity.ts similarity index 83% rename from lib/routes/panewslab/index.ts rename to lib/routes/panewslab/profundity.ts index e85fe8fabfb9a9..98a57d57db5f63 100644 --- a/lib/routes/panewslab/index.ts +++ b/lib/routes/panewslab/profundity.ts @@ -20,21 +20,13 @@ const categories = { }; export const route: Route = { - path: '/:category?', - categories: ['new-media'], - example: '/panewslab', + path: '/profundity/:category?', + categories: ['new-media', 'popular'], + example: '/panewslab/profundity', parameters: { category: '分类,见下表,默认为精选' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, radar: [ { - source: ['panewslab.com/'], + source: ['panewslab.com/', 'www.panewslab.com/zh/profundity/index.html'], }, ], name: '深度', @@ -42,7 +34,7 @@ export const route: Route = { handler, url: 'panewslab.com/', description: `| 精选 | 链游 | 元宇宙 | NFT | DeFi | 监管 | 央行数字货币 | 波卡 | Layer 2 | DAO | 融资 | 活动 | - | ---- | ---- | ------ | --- | ---- | ---- | ------------ | ---- | ------- | --- | ---- | ---- |`, +| ---- | ---- | ------ | --- | ---- | ---- | ------------ | ---- | ------- | --- | ---- | ---- |`, }; async function handler(ctx) { diff --git a/lib/routes/panewslab/topic.ts b/lib/routes/panewslab/topic.ts index 46909c8d8f4d17..4ccf701e414fa6 100644 --- a/lib/routes/panewslab/topic.ts +++ b/lib/routes/panewslab/topic.ts @@ -6,7 +6,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/topic/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/panewslab/topic/1629365774078402', parameters: { id: '专题 id,可在地址栏 URL 中找到' }, features: { diff --git a/lib/routes/papers/category.ts b/lib/routes/papers/category.ts new file mode 100644 index 00000000000000..cac50309da0958 --- /dev/null +++ b/lib/routes/papers/category.ts @@ -0,0 +1,1222 @@ +import { type Data, type DataItem, type Route, 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'; + +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<Data> => { + const { id } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '50', 10); + + const baseUrl: string = 'https://papers.cool'; + const targetUrl: string = new URL(`${id}?show=${limit}`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'en'; + + const items: DataItem[] = $('div.paper') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio<Element> = $(el); + + const title: string = $el.find('a.title-link').text(); + const pubDateStr: string | undefined = $el + .find('p.date') + .contents() + .last() + .text() + ?.trim() + ?.match(/(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})/)?.[1]; + const linkUrl: string | undefined = $el.find('a.title-link').attr('href'); + const categoryEls: Element[] = $el.find('p.subjects a').toArray(); + const categories: string[] = [...new Set(categoryEls.map((el) => $(el).text()).filter(Boolean))]; + const authorEls: Element[] = $el.find('p.authors a.author').toArray(); + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $authorEl: Cheerio<Element> = $(authorEl); + + return { + name: $authorEl.text(), + url: $authorEl.attr('href'), + avatar: undefined, + }; + }); + const doi: string = $el.attr('id') as string; + const guid: string = `papers.cool-${doi}`; + const upDatedStr: string | undefined = pubDateStr; + + let processedItem: DataItem = { + title, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +0) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + category: categories, + author: authors, + doi, + guid, + id: guid, + updated: upDatedStr ? timezone(parseDate(upDatedStr), +0) : undefined, + language, + }; + + const $enclosureEl: Cheerio<Element> = $el.find('a.title-pdf').first(); + const enclosureUrl: string | undefined = $enclosureEl.attr('onclick')?.match(/togglePdf\('.*?',\s'(.*?)',\sthis\)/)?.[1]; + + if (enclosureUrl) { + processedItem = { + ...processedItem, + enclosure_url: enclosureUrl, + enclosure_type: 'application/pdf', + enclosure_title: title, + enclosure_length: undefined, + }; + } + + const description: string = art(path.join(__dirname, 'templates/description.art'), { + pdfUrl: enclosureUrl, + kimiUrl: `${targetUrl.replace(/[a-zA-Z0-9.]+$/, 'kimi')}?paper=${doi}`, + authors, + summary: $el.find('p.summary').text(), + }); + + processedItem = { + ...processedItem, + description, + content: { + html: description, + text: description, + }, + }; + + return processedItem; + }) + .filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[name="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('meta[property="og:image"]').attr('content'), + language, + feedLink: `${targetUrl}/feed`, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/category/:id{.+}?', + name: 'Category', + url: 'papers.cool', + maintainers: ['nczitzk', 'Muyun99'], + handler, + example: '/papers/category/arxiv/cs.AI', + parameters: { + id: { + description: 'Category ID, can be found in URL', + }, + }, + description: `:::tip +To subscribe to [Artificial Intelligence (cs.AI)](https://papers.cool/arxiv/cs.AI) (<https://papers.cool/arxiv/cs.AI>), extract \`arxiv/cs.AI\` from the URL as the \`category\` parameter. The resulting route will be [\`/papers/category/arxiv/cs.AI\`](https://rsshub.app/papers/category/arxiv/cs.AI). +::: + +<details> + <summary>More categories</summary> + +#### [Astrophysics (astro-ph)](https://papers.cool/arxiv/astro-ph) + +| [Astrophysics (astro-ph)](https://papers.cool/arxiv/astro-ph) | [Astrophysics of Galaxies (astro-ph.GA)](https://papers.cool/arxiv/astro-ph.GA) | [Cosmology and Nongalactic Astrophysics (astro-ph.CO)](https://papers.cool/arxiv/astro-ph.CO) | [Earth and Planetary Astrophysics (astro-ph.EP)](https://papers.cool/arxiv/astro-ph.EP) | [High Energy Astrophysical Phenomena (astro-ph.HE)](https://papers.cool/arxiv/astro-ph.HE) | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| [arxiv/astro-ph](https://rsshub.app/papers/category/arxiv/astro-ph) | [arxiv/astro-ph.GA](https://rsshub.app/papers/category/arxiv/astro-ph.GA) | [arxiv/astro-ph.CO](https://rsshub.app/papers/category/arxiv/astro-ph.CO) | [arxiv/astro-ph.EP](https://rsshub.app/papers/category/arxiv/astro-ph.EP) | [arxiv/astro-ph.HE](https://rsshub.app/papers/category/arxiv/astro-ph.HE) | + +| [Instrumentation and Methods for Astrophysics (astro-ph.IM)](https://papers.cool/arxiv/astro-ph.IM) | [Solar and Stellar Astrophysics (astro-ph.SR)](https://papers.cool/arxiv/astro-ph.SR) | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| [arxiv/astro-ph.IM](https://rsshub.app/papers/category/arxiv/astro-ph.IM) | [arxiv/astro-ph.SR](https://rsshub.app/papers/category/arxiv/astro-ph.SR) | + +#### [Condensed Matter (cond-mat)](https://papers.cool/arxiv/cond-mat) + +| [Condensed Matter (cond-mat)](https://papers.cool/arxiv/cond-mat) | [Disordered Systems and Neural Networks (cond-mat.dis-nn)](https://papers.cool/arxiv/cond-mat.dis-nn) | [Materials Science (cond-mat.mtrl-sci)](https://papers.cool/arxiv/cond-mat.mtrl-sci) | [Mesoscale and Nanoscale Physics (cond-mat.mes-hall)](https://papers.cool/arxiv/cond-mat.mes-hall) | [Other Condensed Matter (cond-mat.other)](https://papers.cool/arxiv/cond-mat.other) | +| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| [arxiv/cond-mat](https://rsshub.app/papers/category/arxiv/cond-mat) | [arxiv/cond-mat.dis-nn](https://rsshub.app/papers/category/arxiv/cond-mat.dis-nn) | [arxiv/cond-mat.mtrl-sci](https://rsshub.app/papers/category/arxiv/cond-mat.mtrl-sci) | [arxiv/cond-mat.mes-hall](https://rsshub.app/papers/category/arxiv/cond-mat.mes-hall) | [arxiv/cond-mat.other](https://rsshub.app/papers/category/arxiv/cond-mat.other) | + +| [Quantum Gases (cond-mat.quant-gas)](https://papers.cool/arxiv/cond-mat.quant-gas) | [Soft Condensed Matter (cond-mat.soft)](https://papers.cool/arxiv/cond-mat.soft) | [Statistical Mechanics (cond-mat.stat-mech)](https://papers.cool/arxiv/cond-mat.stat-mech) | [Strongly Correlated Electrons (cond-mat.str-el)](https://papers.cool/arxiv/cond-mat.str-el) | [Superconductivity (cond-mat.supr-con)](https://papers.cool/arxiv/cond-mat.supr-con) | +| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| [arxiv/cond-mat.quant-gas](https://rsshub.app/papers/category/arxiv/cond-mat.quant-gas) | [arxiv/cond-mat.soft](https://rsshub.app/papers/category/arxiv/cond-mat.soft) | [arxiv/cond-mat.stat-mech](https://rsshub.app/papers/category/arxiv/cond-mat.stat-mech) | [arxiv/cond-mat.str-el](https://rsshub.app/papers/category/arxiv/cond-mat.str-el) | [arxiv/cond-mat.supr-con](https://rsshub.app/papers/category/arxiv/cond-mat.supr-con) | + +#### [General Relativity and Quantum Cosmology (gr-qc)](https://papers.cool/arxiv/gr-qc) + +| [General Relativity and Quantum Cosmology (gr-qc)](https://papers.cool/arxiv/gr-qc) | +| ----------------------------------------------------------------------------------- | +| [arxiv/gr-qc](https://rsshub.app/papers/category/arxiv/gr-qc) | + +#### [High Energy Physics - Experiment (hep-ex)](https://papers.cool/arxiv/hep-ex) + +| [High Energy Physics - Experiment (hep-ex)](https://papers.cool/arxiv/hep-ex) | +| ----------------------------------------------------------------------------- | +| [arxiv/hep-ex](https://rsshub.app/papers/category/arxiv/hep-ex) | + +#### [High Energy Physics - Lattice (hep-lat)](https://papers.cool/arxiv/hep-lat) + +| [High Energy Physics - Lattice (hep-lat)](https://papers.cool/arxiv/hep-lat) | +| ---------------------------------------------------------------------------- | +| [arxiv/hep-lat](https://rsshub.app/papers/category/arxiv/hep-lat) | + +#### [High Energy Physics - Phenomenology (hep-ph)](https://papers.cool/arxiv/hep-ph) + +| [High Energy Physics - Phenomenology (hep-ph)](https://papers.cool/arxiv/hep-ph) | +| -------------------------------------------------------------------------------- | +| [arxiv/hep-ph](https://rsshub.app/papers/category/arxiv/hep-ph) | + +#### [High Energy Physics - Theory (hep-th)](https://papers.cool/arxiv/hep-th) + +| [High Energy Physics - Theory (hep-th)](https://papers.cool/arxiv/hep-th) | +| ------------------------------------------------------------------------- | +| [arxiv/hep-th](https://rsshub.app/papers/category/arxiv/hep-th) | + +#### [Mathematical Physics (math-ph)](https://papers.cool/arxiv/math-ph) + +| [Mathematical Physics (math-ph)](https://papers.cool/arxiv/math-ph) | +| ------------------------------------------------------------------- | +| [arxiv/math-ph](https://rsshub.app/papers/category/arxiv/math-ph) | + +#### [Nonlinear Sciences (nlin)](https://papers.cool/arxiv/nlin) + +| [Nonlinear Sciences (nlin)](https://papers.cool/arxiv/nlin) | [Adaptation and Self-Organizing Systems (nlin.AO)](https://papers.cool/arxiv/nlin.AO) | [Cellular Automata and Lattice Gases (nlin.CG)](https://papers.cool/arxiv/nlin.CG) | [Chaotic Dynamics (nlin.CD)](https://papers.cool/arxiv/nlin.CD) | [Exactly Solvable and Integrable Systems (nlin.SI)](https://papers.cool/arxiv/nlin.SI) | +| ----------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| [arxiv/nlin](https://rsshub.app/papers/category/arxiv/nlin) | [arxiv/nlin.AO](https://rsshub.app/papers/category/arxiv/nlin.AO) | [arxiv/nlin.CG](https://rsshub.app/papers/category/arxiv/nlin.CG) | [arxiv/nlin.CD](https://rsshub.app/papers/category/arxiv/nlin.CD) | [arxiv/nlin.SI](https://rsshub.app/papers/category/arxiv/nlin.SI) | + +| [Pattern Formation and Solitons (nlin.PS)](https://papers.cool/arxiv/nlin.PS) | +| ----------------------------------------------------------------------------- | +| [arxiv/nlin.PS](https://rsshub.app/papers/category/arxiv/nlin.PS) | + +#### [Nuclear Experiment (nucl-ex)](https://papers.cool/arxiv/nucl-ex) + +| [Nuclear Experiment (nucl-ex)](https://papers.cool/arxiv/nucl-ex) | +| ----------------------------------------------------------------- | +| [arxiv/nucl-ex](https://rsshub.app/papers/category/arxiv/nucl-ex) | + +#### [Nuclear Theory (nucl-th)](https://papers.cool/arxiv/nucl-th) + +| [Nuclear Theory (nucl-th)](https://papers.cool/arxiv/nucl-th) | +| ----------------------------------------------------------------- | +| [arxiv/nucl-th](https://rsshub.app/papers/category/arxiv/nucl-th) | + +#### [Physics (physics)](https://papers.cool/arxiv/physics) + +| [Physics (physics)](https://papers.cool/arxiv/physics) | [Accelerator Physics (physics.acc-ph)](https://papers.cool/arxiv/physics.acc-ph) | [Applied Physics (physics.app-ph)](https://papers.cool/arxiv/physics.app-ph) | [Atmospheric and Oceanic Physics (physics.ao-ph)](https://papers.cool/arxiv/physics.ao-ph) | [Atomic and Molecular Clusters (physics.atm-clus)](https://papers.cool/arxiv/physics.atm-clus) | +| ----------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| [arxiv/physics](https://rsshub.app/papers/category/arxiv/physics) | [arxiv/physics.acc-ph](https://rsshub.app/papers/category/arxiv/physics.acc-ph) | [arxiv/physics.app-ph](https://rsshub.app/papers/category/arxiv/physics.app-ph) | [arxiv/physics.ao-ph](https://rsshub.app/papers/category/arxiv/physics.ao-ph) | [arxiv/physics.atm-clus](https://rsshub.app/papers/category/arxiv/physics.atm-clus) | + +| [Atomic Physics (physics.atom-ph)](https://papers.cool/arxiv/physics.atom-ph) | [Biological Physics (physics.bio-ph)](https://papers.cool/arxiv/physics.bio-ph) | [Chemical Physics (physics.chem-ph)](https://papers.cool/arxiv/physics.chem-ph) | [Classical Physics (physics.class-ph)](https://papers.cool/arxiv/physics.class-ph) | [Computational Physics (physics.comp-ph)](https://papers.cool/arxiv/physics.comp-ph) | +| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| [arxiv/physics.atom-ph](https://rsshub.app/papers/category/arxiv/physics.atom-ph) | [arxiv/physics.bio-ph](https://rsshub.app/papers/category/arxiv/physics.bio-ph) | [arxiv/physics.chem-ph](https://rsshub.app/papers/category/arxiv/physics.chem-ph) | [arxiv/physics.class-ph](https://rsshub.app/papers/category/arxiv/physics.class-ph) | [arxiv/physics.comp-ph](https://rsshub.app/papers/category/arxiv/physics.comp-ph) | + +| [Data Analysis, Statistics and Probability (physics.data-an)](https://papers.cool/arxiv/physics.data-an) | [Fluid Dynamics (physics.flu-dyn)](https://papers.cool/arxiv/physics.flu-dyn) | [General Physics (physics.gen-ph)](https://papers.cool/arxiv/physics.gen-ph) | [Geophysics (physics.geo-ph)](https://papers.cool/arxiv/physics.geo-ph) | [History and Philosophy of Physics (physics.hist-ph)](https://papers.cool/arxiv/physics.hist-ph) | +| -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| [arxiv/physics.data-an](https://rsshub.app/papers/category/arxiv/physics.data-an) | [arxiv/physics.flu-dyn](https://rsshub.app/papers/category/arxiv/physics.flu-dyn) | [arxiv/physics.gen-ph](https://rsshub.app/papers/category/arxiv/physics.gen-ph) | [arxiv/physics.geo-ph](https://rsshub.app/papers/category/arxiv/physics.geo-ph) | [arxiv/physics.hist-ph](https://rsshub.app/papers/category/arxiv/physics.hist-ph) | + +| [Instrumentation and Detectors (physics.ins-det)](https://papers.cool/arxiv/physics.ins-det) | [Medical Physics (physics.med-ph)](https://papers.cool/arxiv/physics.med-ph) | [Optics (physics.optics)](https://papers.cool/arxiv/physics.optics) | [Physics and Society (physics.soc-ph)](https://papers.cool/arxiv/physics.soc-ph) | [Physics Education (physics.ed-ph)](https://papers.cool/arxiv/physics.ed-ph) | +| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| [arxiv/physics.ins-det](https://rsshub.app/papers/category/arxiv/physics.ins-det) | [arxiv/physics.med-ph](https://rsshub.app/papers/category/arxiv/physics.med-ph) | [arxiv/physics.optics](https://rsshub.app/papers/category/arxiv/physics.optics) | [arxiv/physics.soc-ph](https://rsshub.app/papers/category/arxiv/physics.soc-ph) | [arxiv/physics.ed-ph](https://rsshub.app/papers/category/arxiv/physics.ed-ph) | + +| [Plasma Physics (physics.plasm-ph)](https://papers.cool/arxiv/physics.plasm-ph) | [Popular Physics (physics.pop-ph)](https://papers.cool/arxiv/physics.pop-ph) | [Space Physics (physics.space-ph)](https://papers.cool/arxiv/physics.space-ph) | +| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| [arxiv/physics.plasm-ph](https://rsshub.app/papers/category/arxiv/physics.plasm-ph) | [arxiv/physics.pop-ph](https://rsshub.app/papers/category/arxiv/physics.pop-ph) | [arxiv/physics.space-ph](https://rsshub.app/papers/category/arxiv/physics.space-ph) | + +#### [Quantum Physics (quant-ph)](https://papers.cool/arxiv/quant-ph) + +| [Quantum Physics (quant-ph)](https://papers.cool/arxiv/quant-ph) | +| ------------------------------------------------------------------- | +| [arxiv/quant-ph](https://rsshub.app/papers/category/arxiv/quant-ph) | + +#### [Mathematics (math)](https://papers.cool/arxiv/math) + +| [Mathematics (math)](https://papers.cool/arxiv/math) | [Algebraic Geometry (math.AG)](https://papers.cool/arxiv/math.AG) | [Algebraic Topology (math.AT)](https://papers.cool/arxiv/math.AT) | [Analysis of PDEs (math.AP)](https://papers.cool/arxiv/math.AP) | [Category Theory (math.CT)](https://papers.cool/arxiv/math.CT) | +| ----------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | +| [arxiv/math](https://rsshub.app/papers/category/arxiv/math) | [arxiv/math.AG](https://rsshub.app/papers/category/arxiv/math.AG) | [arxiv/math.AT](https://rsshub.app/papers/category/arxiv/math.AT) | [arxiv/math.AP](https://rsshub.app/papers/category/arxiv/math.AP) | [arxiv/math.CT](https://rsshub.app/papers/category/arxiv/math.CT) | + +| [Classical Analysis and ODEs (math.CA)](https://papers.cool/arxiv/math.CA) | [Combinatorics (math.CO)](https://papers.cool/arxiv/math.CO) | [Commutative Algebra (math.AC)](https://papers.cool/arxiv/math.AC) | [Complex Variables (math.CV)](https://papers.cool/arxiv/math.CV) | [Differential Geometry (math.DG)](https://papers.cool/arxiv/math.DG) | +| -------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------- | -------------------------------------------------------------------- | +| [arxiv/math.CA](https://rsshub.app/papers/category/arxiv/math.CA) | [arxiv/math.CO](https://rsshub.app/papers/category/arxiv/math.CO) | [arxiv/math.AC](https://rsshub.app/papers/category/arxiv/math.AC) | [arxiv/math.CV](https://rsshub.app/papers/category/arxiv/math.CV) | [arxiv/math.DG](https://rsshub.app/papers/category/arxiv/math.DG) | + +| [Dynamical Systems (math.DS)](https://papers.cool/arxiv/math.DS) | [Functional Analysis (math.FA)](https://papers.cool/arxiv/math.FA) | [General Mathematics (math.GM)](https://papers.cool/arxiv/math.GM) | [General Topology (math.GN)](https://papers.cool/arxiv/math.GN) | [Geometric Topology (math.GT)](https://papers.cool/arxiv/math.GT) | +| ----------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ----------------------------------------------------------------- | ----------------------------------------------------------------- | +| [arxiv/math.DS](https://rsshub.app/papers/category/arxiv/math.DS) | [arxiv/math.FA](https://rsshub.app/papers/category/arxiv/math.FA) | [arxiv/math.GM](https://rsshub.app/papers/category/arxiv/math.GM) | [arxiv/math.GN](https://rsshub.app/papers/category/arxiv/math.GN) | [arxiv/math.GT](https://rsshub.app/papers/category/arxiv/math.GT) | + +| [Group Theory (math.GR)](https://papers.cool/arxiv/math.GR) | [History and Overview (math.HO)](https://papers.cool/arxiv/math.HO) | [Information Theory (math.IT)](https://papers.cool/arxiv/math.IT) | [K-Theory and Homology (math.KT)](https://papers.cool/arxiv/math.KT) | [Logic (math.LO)](https://papers.cool/arxiv/math.LO) | +| ----------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------- | +| [arxiv/math.GR](https://rsshub.app/papers/category/arxiv/math.GR) | [arxiv/math.HO](https://rsshub.app/papers/category/arxiv/math.HO) | [arxiv/math.IT](https://rsshub.app/papers/category/arxiv/math.IT) | [arxiv/math.KT](https://rsshub.app/papers/category/arxiv/math.KT) | [arxiv/math.LO](https://rsshub.app/papers/category/arxiv/math.LO) | + +| [Mathematical Physics (math.MP)](https://papers.cool/arxiv/math.MP) | [Metric Geometry (math.MG)](https://papers.cool/arxiv/math.MG) | [Number Theory (math.NT)](https://papers.cool/arxiv/math.NT) | [Numerical Analysis (math.NA)](https://papers.cool/arxiv/math.NA) | [Operator Algebras (math.OA)](https://papers.cool/arxiv/math.OA) | +| ------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | +| [arxiv/math.MP](https://rsshub.app/papers/category/arxiv/math.MP) | [arxiv/math.MG](https://rsshub.app/papers/category/arxiv/math.MG) | [arxiv/math.NT](https://rsshub.app/papers/category/arxiv/math.NT) | [arxiv/math.NA](https://rsshub.app/papers/category/arxiv/math.NA) | [arxiv/math.OA](https://rsshub.app/papers/category/arxiv/math.OA) | + +| [Optimization and Control (math.OC)](https://papers.cool/arxiv/math.OC) | [Probability (math.PR)](https://papers.cool/arxiv/math.PR) | [Quantum Algebra (math.QA)](https://papers.cool/arxiv/math.QA) | [Representation Theory (math.RT)](https://papers.cool/arxiv/math.RT) | [Rings and Algebras (math.RA)](https://papers.cool/arxiv/math.RA) | +| ----------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------- | +| [arxiv/math.OC](https://rsshub.app/papers/category/arxiv/math.OC) | [arxiv/math.PR](https://rsshub.app/papers/category/arxiv/math.PR) | [arxiv/math.QA](https://rsshub.app/papers/category/arxiv/math.QA) | [arxiv/math.RT](https://rsshub.app/papers/category/arxiv/math.RT) | [arxiv/math.RA](https://rsshub.app/papers/category/arxiv/math.RA) | + +| [Spectral Theory (math.SP)](https://papers.cool/arxiv/math.SP) | [Statistics Theory (math.ST)](https://papers.cool/arxiv/math.ST) | [Symplectic Geometry (math.SG)](https://papers.cool/arxiv/math.SG) | +| ----------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------ | +| [arxiv/math.SP](https://rsshub.app/papers/category/arxiv/math.SP) | [arxiv/math.ST](https://rsshub.app/papers/category/arxiv/math.ST) | [arxiv/math.SG](https://rsshub.app/papers/category/arxiv/math.SG) | + +#### [Computer Science (cs)](https://papers.cool/arxiv/cs) + +| [Computer Science (cs)](https://papers.cool/arxiv/cs) | [Artificial Intelligence (cs.AI)](https://papers.cool/arxiv/cs.AI) | [Computation and Language (cs.CL)](https://papers.cool/arxiv/cs.CL) | [Computational Complexity (cs.CC)](https://papers.cool/arxiv/cs.CC) | [Computational Engineering, Finance, and Science (cs.CE)](https://papers.cool/arxiv/cs.CE) | +| ------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| [arxiv/cs](https://rsshub.app/papers/category/arxiv/cs) | [arxiv/cs.AI](https://rsshub.app/papers/category/arxiv/cs.AI) | [arxiv/cs.CL](https://rsshub.app/papers/category/arxiv/cs.CL) | [arxiv/cs.CC](https://rsshub.app/papers/category/arxiv/cs.CC) | [arxiv/cs.CE](https://rsshub.app/papers/category/arxiv/cs.CE) | + +| [Computational Geometry (cs.CG)](https://papers.cool/arxiv/cs.CG) | [Computer Science and Game Theory (cs.GT)](https://papers.cool/arxiv/cs.GT) | [Computer Vision and Pattern Recognition (cs.CV)](https://papers.cool/arxiv/cs.CV) | [Computers and Society (cs.CY)](https://papers.cool/arxiv/cs.CY) | [Cryptography and Security (cs.CR)](https://papers.cool/arxiv/cs.CR) | +| ----------------------------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------- | +| [arxiv/cs.CG](https://rsshub.app/papers/category/arxiv/cs.CG) | [arxiv/cs.GT](https://rsshub.app/papers/category/arxiv/cs.GT) | [arxiv/cs.CV](https://rsshub.app/papers/category/arxiv/cs.CV) | [arxiv/cs.CY](https://rsshub.app/papers/category/arxiv/cs.CY) | [arxiv/cs.CR](https://rsshub.app/papers/category/arxiv/cs.CR) | + +| [Data Structures and Algorithms (cs.DS)](https://papers.cool/arxiv/cs.DS) | [Databases (cs.DB)](https://papers.cool/arxiv/cs.DB) | [Digital Libraries (cs.DL)](https://papers.cool/arxiv/cs.DL) | [Discrete Mathematics (cs.DM)](https://papers.cool/arxiv/cs.DM) | [Distributed, Parallel, and Cluster Computing (cs.DC)](https://papers.cool/arxiv/cs.DC) | +| ------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| [arxiv/cs.DS](https://rsshub.app/papers/category/arxiv/cs.DS) | [arxiv/cs.DB](https://rsshub.app/papers/category/arxiv/cs.DB) | [arxiv/cs.DL](https://rsshub.app/papers/category/arxiv/cs.DL) | [arxiv/cs.DM](https://rsshub.app/papers/category/arxiv/cs.DM) | [arxiv/cs.DC](https://rsshub.app/papers/category/arxiv/cs.DC) | + +| [Emerging Technologies (cs.ET)](https://papers.cool/arxiv/cs.ET) | [Formal Languages and Automata Theory (cs.FL)](https://papers.cool/arxiv/cs.FL) | [General Literature (cs.GL)](https://papers.cool/arxiv/cs.GL) | [Graphics (cs.GR)](https://papers.cool/arxiv/cs.GR) | [Hardware Architecture (cs.AR)](https://papers.cool/arxiv/cs.AR) | +| ---------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------- | +| [arxiv/cs.ET](https://rsshub.app/papers/category/arxiv/cs.ET) | [arxiv/cs.FL](https://rsshub.app/papers/category/arxiv/cs.FL) | [arxiv/cs.GL](https://rsshub.app/papers/category/arxiv/cs.GL) | [arxiv/cs.GR](https://rsshub.app/papers/category/arxiv/cs.GR) | [arxiv/cs.AR](https://rsshub.app/papers/category/arxiv/cs.AR) | + +| [Human-Computer Interaction (cs.HC)](https://papers.cool/arxiv/cs.HC) | [Information Retrieval (cs.IR)](https://papers.cool/arxiv/cs.IR) | [Information Theory (cs.IT)](https://papers.cool/arxiv/cs.IT) | [Logic in Computer Science (cs.LO)](https://papers.cool/arxiv/cs.LO) | [Machine Learning (cs.LG)](https://papers.cool/arxiv/cs.LG) | +| --------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------- | +| [arxiv/cs.HC](https://rsshub.app/papers/category/arxiv/cs.HC) | [arxiv/cs.IR](https://rsshub.app/papers/category/arxiv/cs.IR) | [arxiv/cs.IT](https://rsshub.app/papers/category/arxiv/cs.IT) | [arxiv/cs.LO](https://rsshub.app/papers/category/arxiv/cs.LO) | [arxiv/cs.LG](https://rsshub.app/papers/category/arxiv/cs.LG) | + +| [Mathematical Software (cs.MS)](https://papers.cool/arxiv/cs.MS) | [Multiagent Systems (cs.MA)](https://papers.cool/arxiv/cs.MA) | [Multimedia (cs.MM)](https://papers.cool/arxiv/cs.MM) | [Networking and Internet Architecture (cs.NI)](https://papers.cool/arxiv/cs.NI) | [Neural and Evolutionary Computing (cs.NE)](https://papers.cool/arxiv/cs.NE) | +| ---------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| [arxiv/cs.MS](https://rsshub.app/papers/category/arxiv/cs.MS) | [arxiv/cs.MA](https://rsshub.app/papers/category/arxiv/cs.MA) | [arxiv/cs.MM](https://rsshub.app/papers/category/arxiv/cs.MM) | [arxiv/cs.NI](https://rsshub.app/papers/category/arxiv/cs.NI) | [arxiv/cs.NE](https://rsshub.app/papers/category/arxiv/cs.NE) | + +| [Numerical Analysis (cs.NA)](https://papers.cool/arxiv/cs.NA) | [Operating Systems (cs.OS)](https://papers.cool/arxiv/cs.OS) | [Other Computer Science (cs.OH)](https://papers.cool/arxiv/cs.OH) | [Performance (cs.PF)](https://papers.cool/arxiv/cs.PF) | [Programming Languages (cs.PL)](https://papers.cool/arxiv/cs.PL) | +| ------------------------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------- | +| [arxiv/cs.NA](https://rsshub.app/papers/category/arxiv/cs.NA) | [arxiv/cs.OS](https://rsshub.app/papers/category/arxiv/cs.OS) | [arxiv/cs.OH](https://rsshub.app/papers/category/arxiv/cs.OH) | [arxiv/cs.PF](https://rsshub.app/papers/category/arxiv/cs.PF) | [arxiv/cs.PL](https://rsshub.app/papers/category/arxiv/cs.PL) | + +| [Robotics (cs.RO)](https://papers.cool/arxiv/cs.RO) | [Social and Information Networks (cs.SI)](https://papers.cool/arxiv/cs.SI) | [Software Engineering (cs.SE)](https://papers.cool/arxiv/cs.SE) | [Sound (cs.SD)](https://papers.cool/arxiv/cs.SD) | [Symbolic Computation (cs.SC)](https://papers.cool/arxiv/cs.SC) | +| ------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------- | +| [arxiv/cs.RO](https://rsshub.app/papers/category/arxiv/cs.RO) | [arxiv/cs.SI](https://rsshub.app/papers/category/arxiv/cs.SI) | [arxiv/cs.SE](https://rsshub.app/papers/category/arxiv/cs.SE) | [arxiv/cs.SD](https://rsshub.app/papers/category/arxiv/cs.SD) | [arxiv/cs.SC](https://rsshub.app/papers/category/arxiv/cs.SC) | + +| [Systems and Control (cs.SY)](https://papers.cool/arxiv/cs.SY) | +| -------------------------------------------------------------- | +| [arxiv/cs.SY](https://rsshub.app/papers/category/arxiv/cs.SY) | + +#### [Quantitative Biology (q-bio)](https://papers.cool/arxiv/q-bio) + +| [Quantitative Biology (q-bio)](https://papers.cool/arxiv/q-bio) | [Biomolecules (q-bio.BM)](https://papers.cool/arxiv/q-bio.BM) | [Cell Behavior (q-bio.CB)](https://papers.cool/arxiv/q-bio.CB) | [Genomics (q-bio.GN)](https://papers.cool/arxiv/q-bio.GN) | [Molecular Networks (q-bio.MN)](https://papers.cool/arxiv/q-bio.MN) | +| --------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [arxiv/q-bio](https://rsshub.app/papers/category/arxiv/q-bio) | [arxiv/q-bio.BM](https://rsshub.app/papers/category/arxiv/q-bio.BM) | [arxiv/q-bio.CB](https://rsshub.app/papers/category/arxiv/q-bio.CB) | [arxiv/q-bio.GN](https://rsshub.app/papers/category/arxiv/q-bio.GN) | [arxiv/q-bio.MN](https://rsshub.app/papers/category/arxiv/q-bio.MN) | + +| [Neurons and Cognition (q-bio.NC)](https://papers.cool/arxiv/q-bio.NC) | [Other Quantitative Biology (q-bio.OT)](https://papers.cool/arxiv/q-bio.OT) | [Populations and Evolution (q-bio.PE)](https://papers.cool/arxiv/q-bio.PE) | [Quantitative Methods (q-bio.QM)](https://papers.cool/arxiv/q-bio.QM) | [Subcellular Processes (q-bio.SC)](https://papers.cool/arxiv/q-bio.SC) | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| [arxiv/q-bio.NC](https://rsshub.app/papers/category/arxiv/q-bio.NC) | [arxiv/q-bio.OT](https://rsshub.app/papers/category/arxiv/q-bio.OT) | [arxiv/q-bio.PE](https://rsshub.app/papers/category/arxiv/q-bio.PE) | [arxiv/q-bio.QM](https://rsshub.app/papers/category/arxiv/q-bio.QM) | [arxiv/q-bio.SC](https://rsshub.app/papers/category/arxiv/q-bio.SC) | + +| [Tissues and Organs (q-bio.TO)](https://papers.cool/arxiv/q-bio.TO) | +| ------------------------------------------------------------------- | +| [arxiv/q-bio.TO](https://rsshub.app/papers/category/arxiv/q-bio.TO) | + +#### [Quantitative Finance (q-fin)](https://papers.cool/arxiv/q-fin) + +| [Quantitative Finance (q-fin)](https://papers.cool/arxiv/q-fin) | [Computational Finance (q-fin.CP)](https://papers.cool/arxiv/q-fin.CP) | [Economics (q-fin.EC)](https://papers.cool/arxiv/q-fin.EC) | [General Finance (q-fin.GN)](https://papers.cool/arxiv/q-fin.GN) | [Mathematical Finance (q-fin.MF)](https://papers.cool/arxiv/q-fin.MF) | +| --------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------- | +| [arxiv/q-fin](https://rsshub.app/papers/category/arxiv/q-fin) | [arxiv/q-fin.CP](https://rsshub.app/papers/category/arxiv/q-fin.CP) | [arxiv/q-fin.EC](https://rsshub.app/papers/category/arxiv/q-fin.EC) | [arxiv/q-fin.GN](https://rsshub.app/papers/category/arxiv/q-fin.GN) | [arxiv/q-fin.MF](https://rsshub.app/papers/category/arxiv/q-fin.MF) | + +| [Portfolio Management (q-fin.PM)](https://papers.cool/arxiv/q-fin.PM) | [Pricing of Securities (q-fin.PR)](https://papers.cool/arxiv/q-fin.PR) | [Risk Management (q-fin.RM)](https://papers.cool/arxiv/q-fin.RM) | [Statistical Finance (q-fin.ST)](https://papers.cool/arxiv/q-fin.ST) | [Trading and Market Microstructure (q-fin.TR)](https://papers.cool/arxiv/q-fin.TR) | +| --------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| [arxiv/q-fin.PM](https://rsshub.app/papers/category/arxiv/q-fin.PM) | [arxiv/q-fin.PR](https://rsshub.app/papers/category/arxiv/q-fin.PR) | [arxiv/q-fin.RM](https://rsshub.app/papers/category/arxiv/q-fin.RM) | [arxiv/q-fin.ST](https://rsshub.app/papers/category/arxiv/q-fin.ST) | [arxiv/q-fin.TR](https://rsshub.app/papers/category/arxiv/q-fin.TR) | + +#### [Statistics (stat)](https://papers.cool/arxiv/stat) + +| [Statistics (stat)](https://papers.cool/arxiv/stat) | [Applications (stat.AP)](https://papers.cool/arxiv/stat.AP) | [Computation (stat.CO)](https://papers.cool/arxiv/stat.CO) | [Machine Learning (stat.ML)](https://papers.cool/arxiv/stat.ML) | [Methodology (stat.ME)](https://papers.cool/arxiv/stat.ME) | +| ----------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | +| [arxiv/stat](https://rsshub.app/papers/category/arxiv/stat) | [arxiv/stat.AP](https://rsshub.app/papers/category/arxiv/stat.AP) | [arxiv/stat.CO](https://rsshub.app/papers/category/arxiv/stat.CO) | [arxiv/stat.ML](https://rsshub.app/papers/category/arxiv/stat.ML) | [arxiv/stat.ME](https://rsshub.app/papers/category/arxiv/stat.ME) | + +| [Other Statistics (stat.OT)](https://papers.cool/arxiv/stat.OT) | [Statistics Theory (stat.TH)](https://papers.cool/arxiv/stat.TH) | +| ----------------------------------------------------------------- | ----------------------------------------------------------------- | +| [arxiv/stat.OT](https://rsshub.app/papers/category/arxiv/stat.OT) | [arxiv/stat.TH](https://rsshub.app/papers/category/arxiv/stat.TH) | + +#### [Electrical Engineering and Systems Science (eess)](https://papers.cool/arxiv/eess) + +| [Electrical Engineering and Systems Science (eess)](https://papers.cool/arxiv/eess) | [Audio and Speech Processing (eess.AS)](https://papers.cool/arxiv/eess.AS) | [Image and Video Processing (eess.IV)](https://papers.cool/arxiv/eess.IV) | [Signal Processing (eess.SP)](https://papers.cool/arxiv/eess.SP) | [Systems and Control (eess.SY)](https://papers.cool/arxiv/eess.SY) | +| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------ | +| [arxiv/eess](https://rsshub.app/papers/category/arxiv/eess) | [arxiv/eess.AS](https://rsshub.app/papers/category/arxiv/eess.AS) | [arxiv/eess.IV](https://rsshub.app/papers/category/arxiv/eess.IV) | [arxiv/eess.SP](https://rsshub.app/papers/category/arxiv/eess.SP) | [arxiv/eess.SY](https://rsshub.app/papers/category/arxiv/eess.SY) | + +#### [Economics (econ)](https://papers.cool/arxiv/econ) + +| [Economics (econ)](https://papers.cool/arxiv/econ) | [Econometrics (econ.EM)](https://papers.cool/arxiv/econ.EM) | [General Economics (econ.GN)](https://papers.cool/arxiv/econ.GN) | [Theoretical Economics (econ.TH)](https://papers.cool/arxiv/econ.TH) | +| ----------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------- | +| [arxiv/econ](https://rsshub.app/papers/category/arxiv/econ) | [arxiv/econ.EM](https://rsshub.app/papers/category/arxiv/econ.EM) | [arxiv/econ.GN](https://rsshub.app/papers/category/arxiv/econ.GN) | [arxiv/econ.TH](https://rsshub.app/papers/category/arxiv/econ.TH) | + +</details> +`, + categories: ['journal'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: true, + }, + radar: [ + { + source: ['papers.cool/:id{.+}'], + target: '/category/:id', + }, + { + title: 'Astrophysics (astro-ph)', + source: ['papers.cool/arxiv/astro-ph'], + target: '/category/arxiv/astro-ph', + }, + { + title: 'Astrophysics of Galaxies (astro-ph.GA)', + source: ['papers.cool/arxiv/astro-ph.GA'], + target: '/category/arxiv/astro-ph.GA', + }, + { + title: 'Cosmology and Nongalactic Astrophysics (astro-ph.CO)', + source: ['papers.cool/arxiv/astro-ph.CO'], + target: '/category/arxiv/astro-ph.CO', + }, + { + title: 'Earth and Planetary Astrophysics (astro-ph.EP)', + source: ['papers.cool/arxiv/astro-ph.EP'], + target: '/category/arxiv/astro-ph.EP', + }, + { + title: 'High Energy Astrophysical Phenomena (astro-ph.HE)', + source: ['papers.cool/arxiv/astro-ph.HE'], + target: '/category/arxiv/astro-ph.HE', + }, + { + title: 'Instrumentation and Methods for Astrophysics (astro-ph.IM)', + source: ['papers.cool/arxiv/astro-ph.IM'], + target: '/category/arxiv/astro-ph.IM', + }, + { + title: 'Solar and Stellar Astrophysics (astro-ph.SR)', + source: ['papers.cool/arxiv/astro-ph.SR'], + target: '/category/arxiv/astro-ph.SR', + }, + { + title: 'Condensed Matter (cond-mat)', + source: ['papers.cool/arxiv/cond-mat'], + target: '/category/arxiv/cond-mat', + }, + { + title: 'Disordered Systems and Neural Networks (cond-mat.dis-nn)', + source: ['papers.cool/arxiv/cond-mat.dis-nn'], + target: '/category/arxiv/cond-mat.dis-nn', + }, + { + title: 'Materials Science (cond-mat.mtrl-sci)', + source: ['papers.cool/arxiv/cond-mat.mtrl-sci'], + target: '/category/arxiv/cond-mat.mtrl-sci', + }, + { + title: 'Mesoscale and Nanoscale Physics (cond-mat.mes-hall)', + source: ['papers.cool/arxiv/cond-mat.mes-hall'], + target: '/category/arxiv/cond-mat.mes-hall', + }, + { + title: 'Other Condensed Matter (cond-mat.other)', + source: ['papers.cool/arxiv/cond-mat.other'], + target: '/category/arxiv/cond-mat.other', + }, + { + title: 'Quantum Gases (cond-mat.quant-gas)', + source: ['papers.cool/arxiv/cond-mat.quant-gas'], + target: '/category/arxiv/cond-mat.quant-gas', + }, + { + title: 'Soft Condensed Matter (cond-mat.soft)', + source: ['papers.cool/arxiv/cond-mat.soft'], + target: '/category/arxiv/cond-mat.soft', + }, + { + title: 'Statistical Mechanics (cond-mat.stat-mech)', + source: ['papers.cool/arxiv/cond-mat.stat-mech'], + target: '/category/arxiv/cond-mat.stat-mech', + }, + { + title: 'Strongly Correlated Electrons (cond-mat.str-el)', + source: ['papers.cool/arxiv/cond-mat.str-el'], + target: '/category/arxiv/cond-mat.str-el', + }, + { + title: 'Superconductivity (cond-mat.supr-con)', + source: ['papers.cool/arxiv/cond-mat.supr-con'], + target: '/category/arxiv/cond-mat.supr-con', + }, + { + title: 'General Relativity and Quantum Cosmology (gr-qc)', + source: ['papers.cool/arxiv/gr-qc'], + target: '/category/arxiv/gr-qc', + }, + { + title: 'High Energy Physics - Experiment (hep-ex)', + source: ['papers.cool/arxiv/hep-ex'], + target: '/category/arxiv/hep-ex', + }, + { + title: 'High Energy Physics - Lattice (hep-lat)', + source: ['papers.cool/arxiv/hep-lat'], + target: '/category/arxiv/hep-lat', + }, + { + title: 'High Energy Physics - Phenomenology (hep-ph)', + source: ['papers.cool/arxiv/hep-ph'], + target: '/category/arxiv/hep-ph', + }, + { + title: 'High Energy Physics - Theory (hep-th)', + source: ['papers.cool/arxiv/hep-th'], + target: '/category/arxiv/hep-th', + }, + { + title: 'Mathematical Physics (math-ph)', + source: ['papers.cool/arxiv/math-ph'], + target: '/category/arxiv/math-ph', + }, + { + title: 'Nonlinear Sciences (nlin)', + source: ['papers.cool/arxiv/nlin'], + target: '/category/arxiv/nlin', + }, + { + title: 'Adaptation and Self-Organizing Systems (nlin.AO)', + source: ['papers.cool/arxiv/nlin.AO'], + target: '/category/arxiv/nlin.AO', + }, + { + title: 'Cellular Automata and Lattice Gases (nlin.CG)', + source: ['papers.cool/arxiv/nlin.CG'], + target: '/category/arxiv/nlin.CG', + }, + { + title: 'Chaotic Dynamics (nlin.CD)', + source: ['papers.cool/arxiv/nlin.CD'], + target: '/category/arxiv/nlin.CD', + }, + { + title: 'Exactly Solvable and Integrable Systems (nlin.SI)', + source: ['papers.cool/arxiv/nlin.SI'], + target: '/category/arxiv/nlin.SI', + }, + { + title: 'Pattern Formation and Solitons (nlin.PS)', + source: ['papers.cool/arxiv/nlin.PS'], + target: '/category/arxiv/nlin.PS', + }, + { + title: 'Nuclear Experiment (nucl-ex)', + source: ['papers.cool/arxiv/nucl-ex'], + target: '/category/arxiv/nucl-ex', + }, + { + title: 'Nuclear Theory (nucl-th)', + source: ['papers.cool/arxiv/nucl-th'], + target: '/category/arxiv/nucl-th', + }, + { + title: 'Physics (physics)', + source: ['papers.cool/arxiv/physics'], + target: '/category/arxiv/physics', + }, + { + title: 'Accelerator Physics (physics.acc-ph)', + source: ['papers.cool/arxiv/physics.acc-ph'], + target: '/category/arxiv/physics.acc-ph', + }, + { + title: 'Applied Physics (physics.app-ph)', + source: ['papers.cool/arxiv/physics.app-ph'], + target: '/category/arxiv/physics.app-ph', + }, + { + title: 'Atmospheric and Oceanic Physics (physics.ao-ph)', + source: ['papers.cool/arxiv/physics.ao-ph'], + target: '/category/arxiv/physics.ao-ph', + }, + { + title: 'Atomic and Molecular Clusters (physics.atm-clus)', + source: ['papers.cool/arxiv/physics.atm-clus'], + target: '/category/arxiv/physics.atm-clus', + }, + { + title: 'Atomic Physics (physics.atom-ph)', + source: ['papers.cool/arxiv/physics.atom-ph'], + target: '/category/arxiv/physics.atom-ph', + }, + { + title: 'Biological Physics (physics.bio-ph)', + source: ['papers.cool/arxiv/physics.bio-ph'], + target: '/category/arxiv/physics.bio-ph', + }, + { + title: 'Chemical Physics (physics.chem-ph)', + source: ['papers.cool/arxiv/physics.chem-ph'], + target: '/category/arxiv/physics.chem-ph', + }, + { + title: 'Classical Physics (physics.class-ph)', + source: ['papers.cool/arxiv/physics.class-ph'], + target: '/category/arxiv/physics.class-ph', + }, + { + title: 'Computational Physics (physics.comp-ph)', + source: ['papers.cool/arxiv/physics.comp-ph'], + target: '/category/arxiv/physics.comp-ph', + }, + { + title: 'Data Analysis, Statistics and Probability (physics.data-an)', + source: ['papers.cool/arxiv/physics.data-an'], + target: '/category/arxiv/physics.data-an', + }, + { + title: 'Fluid Dynamics (physics.flu-dyn)', + source: ['papers.cool/arxiv/physics.flu-dyn'], + target: '/category/arxiv/physics.flu-dyn', + }, + { + title: 'General Physics (physics.gen-ph)', + source: ['papers.cool/arxiv/physics.gen-ph'], + target: '/category/arxiv/physics.gen-ph', + }, + { + title: 'Geophysics (physics.geo-ph)', + source: ['papers.cool/arxiv/physics.geo-ph'], + target: '/category/arxiv/physics.geo-ph', + }, + { + title: 'History and Philosophy of Physics (physics.hist-ph)', + source: ['papers.cool/arxiv/physics.hist-ph'], + target: '/category/arxiv/physics.hist-ph', + }, + { + title: 'Instrumentation and Detectors (physics.ins-det)', + source: ['papers.cool/arxiv/physics.ins-det'], + target: '/category/arxiv/physics.ins-det', + }, + { + title: 'Medical Physics (physics.med-ph)', + source: ['papers.cool/arxiv/physics.med-ph'], + target: '/category/arxiv/physics.med-ph', + }, + { + title: 'Optics (physics.optics)', + source: ['papers.cool/arxiv/physics.optics'], + target: '/category/arxiv/physics.optics', + }, + { + title: 'Physics and Society (physics.soc-ph)', + source: ['papers.cool/arxiv/physics.soc-ph'], + target: '/category/arxiv/physics.soc-ph', + }, + { + title: 'Physics Education (physics.ed-ph)', + source: ['papers.cool/arxiv/physics.ed-ph'], + target: '/category/arxiv/physics.ed-ph', + }, + { + title: 'Plasma Physics (physics.plasm-ph)', + source: ['papers.cool/arxiv/physics.plasm-ph'], + target: '/category/arxiv/physics.plasm-ph', + }, + { + title: 'Popular Physics (physics.pop-ph)', + source: ['papers.cool/arxiv/physics.pop-ph'], + target: '/category/arxiv/physics.pop-ph', + }, + { + title: 'Space Physics (physics.space-ph)', + source: ['papers.cool/arxiv/physics.space-ph'], + target: '/category/arxiv/physics.space-ph', + }, + { + title: 'Quantum Physics (quant-ph)', + source: ['papers.cool/arxiv/quant-ph'], + target: '/category/arxiv/quant-ph', + }, + { + title: 'Mathematics (math)', + source: ['papers.cool/arxiv/math'], + target: '/category/arxiv/math', + }, + { + title: 'Algebraic Geometry (math.AG)', + source: ['papers.cool/arxiv/math.AG'], + target: '/category/arxiv/math.AG', + }, + { + title: 'Algebraic Topology (math.AT)', + source: ['papers.cool/arxiv/math.AT'], + target: '/category/arxiv/math.AT', + }, + { + title: 'Analysis of PDEs (math.AP)', + source: ['papers.cool/arxiv/math.AP'], + target: '/category/arxiv/math.AP', + }, + { + title: 'Category Theory (math.CT)', + source: ['papers.cool/arxiv/math.CT'], + target: '/category/arxiv/math.CT', + }, + { + title: 'Classical Analysis and ODEs (math.CA)', + source: ['papers.cool/arxiv/math.CA'], + target: '/category/arxiv/math.CA', + }, + { + title: 'Combinatorics (math.CO)', + source: ['papers.cool/arxiv/math.CO'], + target: '/category/arxiv/math.CO', + }, + { + title: 'Commutative Algebra (math.AC)', + source: ['papers.cool/arxiv/math.AC'], + target: '/category/arxiv/math.AC', + }, + { + title: 'Complex Variables (math.CV)', + source: ['papers.cool/arxiv/math.CV'], + target: '/category/arxiv/math.CV', + }, + { + title: 'Differential Geometry (math.DG)', + source: ['papers.cool/arxiv/math.DG'], + target: '/category/arxiv/math.DG', + }, + { + title: 'Dynamical Systems (math.DS)', + source: ['papers.cool/arxiv/math.DS'], + target: '/category/arxiv/math.DS', + }, + { + title: 'Functional Analysis (math.FA)', + source: ['papers.cool/arxiv/math.FA'], + target: '/category/arxiv/math.FA', + }, + { + title: 'General Mathematics (math.GM)', + source: ['papers.cool/arxiv/math.GM'], + target: '/category/arxiv/math.GM', + }, + { + title: 'General Topology (math.GN)', + source: ['papers.cool/arxiv/math.GN'], + target: '/category/arxiv/math.GN', + }, + { + title: 'Geometric Topology (math.GT)', + source: ['papers.cool/arxiv/math.GT'], + target: '/category/arxiv/math.GT', + }, + { + title: 'Group Theory (math.GR)', + source: ['papers.cool/arxiv/math.GR'], + target: '/category/arxiv/math.GR', + }, + { + title: 'History and Overview (math.HO)', + source: ['papers.cool/arxiv/math.HO'], + target: '/category/arxiv/math.HO', + }, + { + title: 'Information Theory (math.IT)', + source: ['papers.cool/arxiv/math.IT'], + target: '/category/arxiv/math.IT', + }, + { + title: 'K-Theory and Homology (math.KT)', + source: ['papers.cool/arxiv/math.KT'], + target: '/category/arxiv/math.KT', + }, + { + title: 'Logic (math.LO)', + source: ['papers.cool/arxiv/math.LO'], + target: '/category/arxiv/math.LO', + }, + { + title: 'Mathematical Physics (math.MP)', + source: ['papers.cool/arxiv/math.MP'], + target: '/category/arxiv/math.MP', + }, + { + title: 'Metric Geometry (math.MG)', + source: ['papers.cool/arxiv/math.MG'], + target: '/category/arxiv/math.MG', + }, + { + title: 'Number Theory (math.NT)', + source: ['papers.cool/arxiv/math.NT'], + target: '/category/arxiv/math.NT', + }, + { + title: 'Numerical Analysis (math.NA)', + source: ['papers.cool/arxiv/math.NA'], + target: '/category/arxiv/math.NA', + }, + { + title: 'Operator Algebras (math.OA)', + source: ['papers.cool/arxiv/math.OA'], + target: '/category/arxiv/math.OA', + }, + { + title: 'Optimization and Control (math.OC)', + source: ['papers.cool/arxiv/math.OC'], + target: '/category/arxiv/math.OC', + }, + { + title: 'Probability (math.PR)', + source: ['papers.cool/arxiv/math.PR'], + target: '/category/arxiv/math.PR', + }, + { + title: 'Quantum Algebra (math.QA)', + source: ['papers.cool/arxiv/math.QA'], + target: '/category/arxiv/math.QA', + }, + { + title: 'Representation Theory (math.RT)', + source: ['papers.cool/arxiv/math.RT'], + target: '/category/arxiv/math.RT', + }, + { + title: 'Rings and Algebras (math.RA)', + source: ['papers.cool/arxiv/math.RA'], + target: '/category/arxiv/math.RA', + }, + { + title: 'Spectral Theory (math.SP)', + source: ['papers.cool/arxiv/math.SP'], + target: '/category/arxiv/math.SP', + }, + { + title: 'Statistics Theory (math.ST)', + source: ['papers.cool/arxiv/math.ST'], + target: '/category/arxiv/math.ST', + }, + { + title: 'Symplectic Geometry (math.SG)', + source: ['papers.cool/arxiv/math.SG'], + target: '/category/arxiv/math.SG', + }, + { + title: 'Computer Science (cs)', + source: ['papers.cool/arxiv/cs'], + target: '/category/arxiv/cs', + }, + { + title: 'Artificial Intelligence (cs.AI)', + source: ['papers.cool/arxiv/cs.AI'], + target: '/category/arxiv/cs.AI', + }, + { + title: 'Computation and Language (cs.CL)', + source: ['papers.cool/arxiv/cs.CL'], + target: '/category/arxiv/cs.CL', + }, + { + title: 'Computational Complexity (cs.CC)', + source: ['papers.cool/arxiv/cs.CC'], + target: '/category/arxiv/cs.CC', + }, + { + title: 'Computational Engineering, Finance, and Science (cs.CE)', + source: ['papers.cool/arxiv/cs.CE'], + target: '/category/arxiv/cs.CE', + }, + { + title: 'Computational Geometry (cs.CG)', + source: ['papers.cool/arxiv/cs.CG'], + target: '/category/arxiv/cs.CG', + }, + { + title: 'Computer Science and Game Theory (cs.GT)', + source: ['papers.cool/arxiv/cs.GT'], + target: '/category/arxiv/cs.GT', + }, + { + title: 'Computer Vision and Pattern Recognition (cs.CV)', + source: ['papers.cool/arxiv/cs.CV'], + target: '/category/arxiv/cs.CV', + }, + { + title: 'Computers and Society (cs.CY)', + source: ['papers.cool/arxiv/cs.CY'], + target: '/category/arxiv/cs.CY', + }, + { + title: 'Cryptography and Security (cs.CR)', + source: ['papers.cool/arxiv/cs.CR'], + target: '/category/arxiv/cs.CR', + }, + { + title: 'Data Structures and Algorithms (cs.DS)', + source: ['papers.cool/arxiv/cs.DS'], + target: '/category/arxiv/cs.DS', + }, + { + title: 'Databases (cs.DB)', + source: ['papers.cool/arxiv/cs.DB'], + target: '/category/arxiv/cs.DB', + }, + { + title: 'Digital Libraries (cs.DL)', + source: ['papers.cool/arxiv/cs.DL'], + target: '/category/arxiv/cs.DL', + }, + { + title: 'Discrete Mathematics (cs.DM)', + source: ['papers.cool/arxiv/cs.DM'], + target: '/category/arxiv/cs.DM', + }, + { + title: 'Distributed, Parallel, and Cluster Computing (cs.DC)', + source: ['papers.cool/arxiv/cs.DC'], + target: '/category/arxiv/cs.DC', + }, + { + title: 'Emerging Technologies (cs.ET)', + source: ['papers.cool/arxiv/cs.ET'], + target: '/category/arxiv/cs.ET', + }, + { + title: 'Formal Languages and Automata Theory (cs.FL)', + source: ['papers.cool/arxiv/cs.FL'], + target: '/category/arxiv/cs.FL', + }, + { + title: 'General Literature (cs.GL)', + source: ['papers.cool/arxiv/cs.GL'], + target: '/category/arxiv/cs.GL', + }, + { + title: 'Graphics (cs.GR)', + source: ['papers.cool/arxiv/cs.GR'], + target: '/category/arxiv/cs.GR', + }, + { + title: 'Hardware Architecture (cs.AR)', + source: ['papers.cool/arxiv/cs.AR'], + target: '/category/arxiv/cs.AR', + }, + { + title: 'Human-Computer Interaction (cs.HC)', + source: ['papers.cool/arxiv/cs.HC'], + target: '/category/arxiv/cs.HC', + }, + { + title: 'Information Retrieval (cs.IR)', + source: ['papers.cool/arxiv/cs.IR'], + target: '/category/arxiv/cs.IR', + }, + { + title: 'Information Theory (cs.IT)', + source: ['papers.cool/arxiv/cs.IT'], + target: '/category/arxiv/cs.IT', + }, + { + title: 'Logic in Computer Science (cs.LO)', + source: ['papers.cool/arxiv/cs.LO'], + target: '/category/arxiv/cs.LO', + }, + { + title: 'Machine Learning (cs.LG)', + source: ['papers.cool/arxiv/cs.LG'], + target: '/category/arxiv/cs.LG', + }, + { + title: 'Mathematical Software (cs.MS)', + source: ['papers.cool/arxiv/cs.MS'], + target: '/category/arxiv/cs.MS', + }, + { + title: 'Multiagent Systems (cs.MA)', + source: ['papers.cool/arxiv/cs.MA'], + target: '/category/arxiv/cs.MA', + }, + { + title: 'Multimedia (cs.MM)', + source: ['papers.cool/arxiv/cs.MM'], + target: '/category/arxiv/cs.MM', + }, + { + title: 'Networking and Internet Architecture (cs.NI)', + source: ['papers.cool/arxiv/cs.NI'], + target: '/category/arxiv/cs.NI', + }, + { + title: 'Neural and Evolutionary Computing (cs.NE)', + source: ['papers.cool/arxiv/cs.NE'], + target: '/category/arxiv/cs.NE', + }, + { + title: 'Numerical Analysis (cs.NA)', + source: ['papers.cool/arxiv/cs.NA'], + target: '/category/arxiv/cs.NA', + }, + { + title: 'Operating Systems (cs.OS)', + source: ['papers.cool/arxiv/cs.OS'], + target: '/category/arxiv/cs.OS', + }, + { + title: 'Other Computer Science (cs.OH)', + source: ['papers.cool/arxiv/cs.OH'], + target: '/category/arxiv/cs.OH', + }, + { + title: 'Performance (cs.PF)', + source: ['papers.cool/arxiv/cs.PF'], + target: '/category/arxiv/cs.PF', + }, + { + title: 'Programming Languages (cs.PL)', + source: ['papers.cool/arxiv/cs.PL'], + target: '/category/arxiv/cs.PL', + }, + { + title: 'Robotics (cs.RO)', + source: ['papers.cool/arxiv/cs.RO'], + target: '/category/arxiv/cs.RO', + }, + { + title: 'Social and Information Networks (cs.SI)', + source: ['papers.cool/arxiv/cs.SI'], + target: '/category/arxiv/cs.SI', + }, + { + title: 'Software Engineering (cs.SE)', + source: ['papers.cool/arxiv/cs.SE'], + target: '/category/arxiv/cs.SE', + }, + { + title: 'Sound (cs.SD)', + source: ['papers.cool/arxiv/cs.SD'], + target: '/category/arxiv/cs.SD', + }, + { + title: 'Symbolic Computation (cs.SC)', + source: ['papers.cool/arxiv/cs.SC'], + target: '/category/arxiv/cs.SC', + }, + { + title: 'Systems and Control (cs.SY)', + source: ['papers.cool/arxiv/cs.SY'], + target: '/category/arxiv/cs.SY', + }, + { + title: 'Quantitative Biology (q-bio)', + source: ['papers.cool/arxiv/q-bio'], + target: '/category/arxiv/q-bio', + }, + { + title: 'Biomolecules (q-bio.BM)', + source: ['papers.cool/arxiv/q-bio.BM'], + target: '/category/arxiv/q-bio.BM', + }, + { + title: 'Cell Behavior (q-bio.CB)', + source: ['papers.cool/arxiv/q-bio.CB'], + target: '/category/arxiv/q-bio.CB', + }, + { + title: 'Genomics (q-bio.GN)', + source: ['papers.cool/arxiv/q-bio.GN'], + target: '/category/arxiv/q-bio.GN', + }, + { + title: 'Molecular Networks (q-bio.MN)', + source: ['papers.cool/arxiv/q-bio.MN'], + target: '/category/arxiv/q-bio.MN', + }, + { + title: 'Neurons and Cognition (q-bio.NC)', + source: ['papers.cool/arxiv/q-bio.NC'], + target: '/category/arxiv/q-bio.NC', + }, + { + title: 'Other Quantitative Biology (q-bio.OT)', + source: ['papers.cool/arxiv/q-bio.OT'], + target: '/category/arxiv/q-bio.OT', + }, + { + title: 'Populations and Evolution (q-bio.PE)', + source: ['papers.cool/arxiv/q-bio.PE'], + target: '/category/arxiv/q-bio.PE', + }, + { + title: 'Quantitative Methods (q-bio.QM)', + source: ['papers.cool/arxiv/q-bio.QM'], + target: '/category/arxiv/q-bio.QM', + }, + { + title: 'Subcellular Processes (q-bio.SC)', + source: ['papers.cool/arxiv/q-bio.SC'], + target: '/category/arxiv/q-bio.SC', + }, + { + title: 'Tissues and Organs (q-bio.TO)', + source: ['papers.cool/arxiv/q-bio.TO'], + target: '/category/arxiv/q-bio.TO', + }, + { + title: 'Quantitative Finance (q-fin)', + source: ['papers.cool/arxiv/q-fin'], + target: '/category/arxiv/q-fin', + }, + { + title: 'Computational Finance (q-fin.CP)', + source: ['papers.cool/arxiv/q-fin.CP'], + target: '/category/arxiv/q-fin.CP', + }, + { + title: 'Economics (q-fin.EC)', + source: ['papers.cool/arxiv/q-fin.EC'], + target: '/category/arxiv/q-fin.EC', + }, + { + title: 'General Finance (q-fin.GN)', + source: ['papers.cool/arxiv/q-fin.GN'], + target: '/category/arxiv/q-fin.GN', + }, + { + title: 'Mathematical Finance (q-fin.MF)', + source: ['papers.cool/arxiv/q-fin.MF'], + target: '/category/arxiv/q-fin.MF', + }, + { + title: 'Portfolio Management (q-fin.PM)', + source: ['papers.cool/arxiv/q-fin.PM'], + target: '/category/arxiv/q-fin.PM', + }, + { + title: 'Pricing of Securities (q-fin.PR)', + source: ['papers.cool/arxiv/q-fin.PR'], + target: '/category/arxiv/q-fin.PR', + }, + { + title: 'Risk Management (q-fin.RM)', + source: ['papers.cool/arxiv/q-fin.RM'], + target: '/category/arxiv/q-fin.RM', + }, + { + title: 'Statistical Finance (q-fin.ST)', + source: ['papers.cool/arxiv/q-fin.ST'], + target: '/category/arxiv/q-fin.ST', + }, + { + title: 'Trading and Market Microstructure (q-fin.TR)', + source: ['papers.cool/arxiv/q-fin.TR'], + target: '/category/arxiv/q-fin.TR', + }, + { + title: 'Statistics (stat)', + source: ['papers.cool/arxiv/stat'], + target: '/category/arxiv/stat', + }, + { + title: 'Applications (stat.AP)', + source: ['papers.cool/arxiv/stat.AP'], + target: '/category/arxiv/stat.AP', + }, + { + title: 'Computation (stat.CO)', + source: ['papers.cool/arxiv/stat.CO'], + target: '/category/arxiv/stat.CO', + }, + { + title: 'Machine Learning (stat.ML)', + source: ['papers.cool/arxiv/stat.ML'], + target: '/category/arxiv/stat.ML', + }, + { + title: 'Methodology (stat.ME)', + source: ['papers.cool/arxiv/stat.ME'], + target: '/category/arxiv/stat.ME', + }, + { + title: 'Other Statistics (stat.OT)', + source: ['papers.cool/arxiv/stat.OT'], + target: '/category/arxiv/stat.OT', + }, + { + title: 'Statistics Theory (stat.TH)', + source: ['papers.cool/arxiv/stat.TH'], + target: '/category/arxiv/stat.TH', + }, + { + title: 'Electrical Engineering and Systems Science (eess)', + source: ['papers.cool/arxiv/eess'], + target: '/category/arxiv/eess', + }, + { + title: 'Audio and Speech Processing (eess.AS)', + source: ['papers.cool/arxiv/eess.AS'], + target: '/category/arxiv/eess.AS', + }, + { + title: 'Image and Video Processing (eess.IV)', + source: ['papers.cool/arxiv/eess.IV'], + target: '/category/arxiv/eess.IV', + }, + { + title: 'Signal Processing (eess.SP)', + source: ['papers.cool/arxiv/eess.SP'], + target: '/category/arxiv/eess.SP', + }, + { + title: 'Systems and Control (eess.SY)', + source: ['papers.cool/arxiv/eess.SY'], + target: '/category/arxiv/eess.SY', + }, + { + title: 'Economics (econ)', + source: ['papers.cool/arxiv/econ'], + target: '/category/arxiv/econ', + }, + { + title: 'Econometrics (econ.EM)', + source: ['papers.cool/arxiv/econ.EM'], + target: '/category/arxiv/econ.EM', + }, + { + title: 'General Economics (econ.GN)', + source: ['papers.cool/arxiv/econ.GN'], + target: '/category/arxiv/econ.GN', + }, + { + title: 'Theoretical Economics (econ.TH)', + source: ['papers.cool/arxiv/econ.TH'], + target: '/category/arxiv/econ.TH', + }, + ], + view: ViewType.Articles, + + zh: { + path: '/category/:id{.+}?', + name: 'Category', + url: 'papers.cool', + maintainers: ['nczitzk'], + handler, + example: '/papers/arxiv/cs.AI', + parameters: { + id: { + description: '分类 id,可在对应分类页 URL 中找到', + }, + }, + description: `:::tip +订阅 [人工智能 (cs.AI)](https://papers.cool/arxiv/cs.AI)(<https://papers.cool/arxiv/cs.AI>),请从 URL 中提取 \`arxiv/cs.AI\` 作为 \`category\` 参数,得到的路由将是 [\`/papers/category/arxiv/cs.AI\`](https://rsshub.app/papers/category/arxiv/cs.AI)。 +::: +`, + }, +}; diff --git a/lib/routes/papers/namespace.ts b/lib/routes/papers/namespace.ts index 6a831b4405fb1b..26ed68e0312fb7 100644 --- a/lib/routes/papers/namespace.ts +++ b/lib/routes/papers/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: 'papers.cool', categories: ['journal'], description: '', + lang: 'en', }; diff --git a/lib/routes/papers/index.ts b/lib/routes/papers/query.ts similarity index 54% rename from lib/routes/papers/index.ts rename to lib/routes/papers/query.ts index 47cb03656e425e..57eb4be9c0946a 100644 --- a/lib/routes/papers/index.ts +++ b/lib/routes/papers/query.ts @@ -12,16 +12,15 @@ const pdfUrlGenerators = { }; export const handler = async (ctx) => { - const { category = 'arxiv/cs.AI' } = ctx.req.param(); + const { keyword = 'query/Detection' } = ctx.req.param(); const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 150; const rootUrl = 'https://papers.cool'; - const currentUrl = new URL(category, rootUrl).href; - const feedUrl = new URL(`${category}/feed`, rootUrl).href; - - const site = category.split(/\//)[0]; - const apiKimiUrl = new URL(`${site}/kimi/`, rootUrl).href; + const currentUrl = new URL(`arxiv/search?highlight=1&query=${keyword}&sort=0`, rootUrl).href; + const feedUrl = new URL(`arxiv/search/feed?query=${keyword}`, rootUrl).href; + const site = keyword.split(/\//)[0]; + const apiKimiUrl = new URL(`${site}/kimi?paper=`, rootUrl).href; const feed = await parser.parseURL(feedUrl); const language = 'en'; @@ -34,10 +33,12 @@ export const handler = async (ctx) => { const kimiUrl = new URL(id, apiKimiUrl).href; const pdfUrl = Object.hasOwn(pdfUrlGenerators, site) ? pdfUrlGenerators[site](id) : undefined; + const authorString = item.author; const description = art(path.join(__dirname, 'templates/description.art'), { pdfUrl, siteUrl: item.link, kimiUrl, + authorString, summary: item.summary, }); @@ -47,7 +48,7 @@ export const handler = async (ctx) => { pubDate: parseDate(item.pubDate ?? ''), link: item.link, category: item.categories, - author: item.creator, + author: authorString, doi: `${site}${id}`, guid, id: guid, @@ -74,23 +75,21 @@ export const handler = async (ctx) => { }; export const route: Route = { - path: '/:category{.+}?', + path: '/query/:keyword{.+}?', name: 'Topic', url: 'papers.cool', - maintainers: ['nczitzk'], + maintainers: ['Muyun99'], handler, - example: '/papers/arxiv/cs.AI', - parameters: { category: 'Category, arXiv Artificial Intelligence (cs.AI) by default' }, - description: `:::tip - If you subscribe to [arXiv Artificial Intelligence (cs.AI)](https://papers.cool/arxiv/cs.AI),where the URL is \`https://papers.cool/arxiv/cs.AI\`, extract the part \`https://papers.cool/\` to the end, and use it as the parameter to fill in. Therefore, the route will be [\`/papers/arxiv/cs.AI\`](https://rsshub.app/papers/arxiv/cs.AI). - ::: + example: '/papers/query/Detection', + parameters: { keyword: 'Keyword to search for papers, e.g., Detection, Segmentation, etc.' }, + description: `::: tip + If you subscibe to [arXiv Paper queryed by Detection](https://papers.cool/arxiv/search?highlight=1&query=Detection), where the URL is \`https://papers.cool/arxiv/search?highlight=1&query=Detection\`, extract the part \`https://papers.cool/\` to the end, and use it as the parameter to fill in. Therefore, the route will be [\`/papers/query/Detection\`](https://rsshub.app/papers/query/Detection). +::: - | Category | id | - | ----------------------------------------------------- | ----------- | - | arXiv Artificial Intelligence (cs.AI) | arxiv/cs.AI | - | arXiv Computation and Language (cs.CL) | arxiv/cs.CL | - | arXiv Computer Vision and Pattern Recognition (cs.CV) | arxiv/cs.CV | - | arXiv Machine Learning (cs.LG) | arxiv/cs.LG | +| Category | id | +| ----------------------------------------------------- | ------------------- | +| arXiv Paper queryed by Detection | query/Detection | +| arXiv Paper queryed by Segmentation | query/Segmentation | `, categories: ['journal'], @@ -105,24 +104,9 @@ export const route: Route = { }, radar: [ { - title: 'arXiv Artificial Intelligence (cs.AI)', - source: ['papers.cool/arxiv/cs.AI'], - target: '/arxiv/cs.AI', - }, - { - title: 'arXiv Computation and Language (cs.CL)', - source: ['papers.cool/arxiv/cs.CL'], - target: '/arxiv/cs.CL', - }, - { - title: 'arXiv Computer Vision and Pattern Recognition (cs.CV)', - source: ['papers.cool/arxiv/cs.CV'], - target: '/arxiv/cs.CV', - }, - { - title: 'arXiv Machine Learning (cs.LG)', - source: ['papers.cool/arxiv/cs.LG'], - target: '/arxiv/cs.LG', + title: 'arXiv Paper queryed by Keyword', + source: ['papers.cool/arxiv/search?highlight=1&query=*&sort=0'], + target: '/papers/query/:keyword', }, ], }; diff --git a/lib/routes/papers/templates/description.art b/lib/routes/papers/templates/description.art index 368ea61f8ee842..93aac7b3c6f5fd 100644 --- a/lib/routes/papers/templates/description.art +++ b/lib/routes/papers/templates/description.art @@ -2,14 +2,21 @@ <a href="{{ pdfUrl }}">[PDF]</a> {{ /if }} -{{ if siteUrl }} - <a href="{{ siteUrl }}">[Site]</a> -{{ /if }} - {{ if kimiUrl }} <a href="{{ kimiUrl }}">[Kimi]</a> {{ /if }} +{{ if authors }} + <p> + <b>Authors:</b> + {{ each authors author }} + <a href="{{ author.url }}"> + {{ author.name }} + </a>, + {{ /each }} + </p> +{{ /if }} + {{ if summary }} <p>{{ summary }}</p> {{ /if }} \ No newline at end of file diff --git a/lib/routes/paradigm/namespace.ts b/lib/routes/paradigm/namespace.ts index 524d79b45b8e26..b95b8f171b4cfd 100644 --- a/lib/routes/paradigm/namespace.ts +++ b/lib/routes/paradigm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Paradigm', url: 'paradigm.xyz', + lang: 'en', }; diff --git a/lib/routes/parliament.uk/commonslibrary.ts b/lib/routes/parliament.uk/commonslibrary.ts new file mode 100644 index 00000000000000..41fa182b7a914a --- /dev/null +++ b/lib/routes/parliament.uk/commonslibrary.ts @@ -0,0 +1,55 @@ +import { load } from 'cheerio'; +import { Route } from '@/types'; +import puppeteer from '@/utils/puppeteer'; +import timezone from '@/utils/timezone'; + +export const route: Route = { + path: '/commonslibrary/type/:topic?', + categories: ['government'], + example: '/parliament.uk/commonslibrary/type/research-briefing', + parameters: { topic: 'research by topic, string, example: [research-briefing|data-dashboard]' }, + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Commonlibrary', + maintainers: ['AntiKnot'], + handler, +}; + +async function handler(ctx) { + const { topic } = ctx.req.param(); + const baseUrl = 'https://commonslibrary.parliament.uk'; + const url = `${baseUrl}/type/${topic}/`; + const browser = await puppeteer(); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (request) => { + request.resourceType() === 'document' ? request.continue() : request.abort(); + }); + await page.goto(url, { + waitUntil: 'domcontentloaded', + }); + + const html = await page.evaluate(() => document.documentElement.innerHTML); + await page.close(); + const $ = load(html); + const items = $('div.l-box.l-box--no-border.card__text') + .toArray() + .map((article) => ({ + title: $(article).find('.card__text a').text().trim(), + link: $(article).find('.card__text a').attr('href'), + description: $(article).find('p').last().text().trim(), + pubDate: timezone($(article).find('.card__date time').attr('datetime')), + })); + await browser.close(); + return { + title: `parliament - lordslibrary - ${topic}`, + link: url, + item: items, + }; +} diff --git a/lib/routes/parliament.uk/lordslibrary.ts b/lib/routes/parliament.uk/lordslibrary.ts new file mode 100644 index 00000000000000..f0f07beda52641 --- /dev/null +++ b/lib/routes/parliament.uk/lordslibrary.ts @@ -0,0 +1,55 @@ +import { load } from 'cheerio'; +import { Route } from '@/types'; +import puppeteer from '@/utils/puppeteer'; +import timezone from '@/utils/timezone'; + +export const route: Route = { + path: '/lordslibrary/type/:topic?', + categories: ['government'], + example: '/parliament.uk/lordslibrary/type/research-briefing', + parameters: { topic: 'research by topic, string, example: [research-briefing|buisness|economy]' }, + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'House of Lords Library', + maintainers: ['AntiKnot'], + handler, +}; + +async function handler(ctx) { + const { topic } = ctx.req.param(); + const baseUrl = 'https://lordslibrary.parliament.uk'; + const url = `${baseUrl}/type/${topic}/`; + const browser = await puppeteer(); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (request) => { + request.resourceType() === 'document' ? request.continue() : request.abort(); + }); + await page.goto(url, { + waitUntil: 'domcontentloaded', + }); + + const html = await page.evaluate(() => document.documentElement.innerHTML); + await page.close(); + const $ = load(html); + const items = $('div.l-box.l-box--no-border.card__text') + .toArray() + .map((article) => ({ + title: $(article).find('.card__text a').text().trim(), + link: $(article).find('.card__text a').attr('href'), + description: $(article).find('p').last().text().trim(), + pubDate: timezone($(article).find('.card__date time').attr('datetime')), + })); + await browser.close(); + return { + title: `parliament - lordslibrary - ${topic}`, + link: url, + item: items, + }; +} diff --git a/lib/routes/parliament.uk/namespace.ts b/lib/routes/parliament.uk/namespace.ts new file mode 100644 index 00000000000000..5817bd6ba50d75 --- /dev/null +++ b/lib/routes/parliament.uk/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'UK Parliament', + url: 'parliament.uk', + categories: ['government'], + description: 'The UK Parliament has two Houses that work on behalf of UK citizens to check and challenge the work of Government, make and shape effective laws, and debate/make decisions on the big issues of the day.', + lang: 'en', +}; diff --git a/lib/routes/parliament.uk/petitions.ts b/lib/routes/parliament.uk/petitions.ts new file mode 100644 index 00000000000000..95eec47353c9cb --- /dev/null +++ b/lib/routes/parliament.uk/petitions.ts @@ -0,0 +1,191 @@ +import path from 'node:path'; + +import { type Context } from 'hono'; +import { load, type CheerioAPI } from 'cheerio'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import ofetch from '@/utils/ofetch'; + +const __dirname = getCurrentPath(import.meta.url); + +export const handler = async (ctx: Context): Promise<Data> => { + const { state = 'all' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '50', 10); + + const rootUrl: string = 'https://petition.parliament.uk'; + const targetUrl: string = new URL(`petitions?state=${state}`, rootUrl).href; + const jsonUrl: string = new URL('petitions.json', rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'en'; + + const jsonResponse = await ofetch(jsonUrl, { + query: { + page: 1, + state, + }, + }); + + const items = jsonResponse.data.slice(0, limit).map((item): DataItem => { + const attributes = item.attributes; + + const title = attributes.action; + const description = art(path.join(__dirname, 'templates/description.art'), { + intro: attributes.background, + description: attributes.additional_details, + }); + const guid = `parliament.uk-petition-${item.id}`; + + const author: DataItem['author'] = attributes.creator_name; + + const extraLinks = attributes.departments?.map((link) => ({ + url: link.url, + type: 'related', + content_html: link.name, + })); + + return { + title, + description, + pubDate: parseDate(attributes.created_at), + link: new URL(`petitions/${item.id}`, rootUrl).href, + category: [...new Set([...(attributes.topics ?? []), ...(attributes.departments?.map((d) => d.name) ?? [])])].filter(Boolean), + author, + guid, + id: guid, + content: { + html: description, + text: attributes.background, + }, + updated: parseDate(attributes.updated_at), + language, + _extra: { + links: extraLinks?.length ? extraLinks : undefined, + }, + }; + }); + + const feedImage = $('meta[property="og:image"]').prop('content'); + + return { + title: $('h1.page-title').text(), + description: $('meta[property="twitter:description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author: $('meta[name="msapplication-tooltip"]').prop('content'), + language, + id: $('meta[property="og:url"]').prop('content'), + }; +}; + +export const route: Route = { + path: '/petitions/:state?', + name: 'Petitions', + url: 'petition.parliament.uk', + maintainers: ['nczitzk'], + handler, + example: '/parliament.uk/petitions/all', + parameters: { + state: 'State, `all` by default, see below', + }, + description: `::: tip +If you subscribe to [Recent petitions](https://petition.parliament.uk/petitions?state=recent),where the URL is \`https://petition.parliament.uk/petitions?state=recent\`, use the value of \`state\` as the parameter to fill in. Therefore, the route will be [\`/parliament.uk/petitions/recent\`](https://rsshub.app/parliament.uk/petitions/recent). +::: + +<details> +<summary>More states</summary> + +| Name | ID | +| ------------------------------- | ----------------- | +| All petitions | all | +| Open petitions | open | +| Recent petitions | recent | +| Closed petitions | closed | +| Rejected petitions | rejected | +| Awaiting government response | awaiting_response | +| Government responses | with_response | +| Awaiting a debate in Parliament | awaiting_debate | +| Debated in Parliament | debated | +| Not debated in Parliament | not_debated | + +</details> + `, + categories: ['government'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['petition.parliament.uk/petitions'], + target: (_, url) => { + const urlObj = new URL(url); + const state = urlObj.searchParams.get('state'); + + return `/parliament.uk/petitions${state ? `/${state}` : ''}`; + }, + }, + { + title: 'All petitions', + source: ['petition.parliament.uk/petitions'], + target: '/petitions/all', + }, + { + title: 'Open petitions', + source: ['petition.parliament.uk/petitions'], + target: '/petitions/open', + }, + { + title: 'Recent petitions', + source: ['petition.parliament.uk/petitions'], + target: '/petitions/recent', + }, + { + title: 'Closed petitions', + source: ['petition.parliament.uk/petitions'], + target: '/petitions/closed', + }, + { + title: 'Rejected petitions', + source: ['petition.parliament.uk/petitions'], + target: '/petitions/rejected', + }, + { + title: 'Awaiting government response', + source: ['petition.parliament.uk/petitions'], + target: '/petitions/awaiting_response', + }, + { + title: 'Government responses', + source: ['petition.parliament.uk/petitions'], + target: '/petitions/with_response', + }, + { + title: 'Awaiting a debate in Parliament', + source: ['petition.parliament.uk/petitions'], + target: '/petitions/awaiting_debate', + }, + { + title: 'Debated in Parliament', + source: ['petition.parliament.uk/petitions'], + target: '/petitions/debated', + }, + { + title: 'Not debated in Parliament', + source: ['petition.parliament.uk/petitions'], + target: '/petitions/not_debated', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/parliament.uk/templates/description.art b/lib/routes/parliament.uk/templates/description.art new file mode 100644 index 00000000000000..eee77a05425830 --- /dev/null +++ b/lib/routes/parliament.uk/templates/description.art @@ -0,0 +1,7 @@ +{{ if intro }} + <blockquote>{{ intro }}</blockquote> +{{ /if }} + +{{ if description }} + <p>{{ description }}<p> +{{ /if }} \ No newline at end of file diff --git a/lib/routes/parliament/namespace.ts b/lib/routes/parliament/namespace.ts index c9835b111b8fc0..a95cddd59619ff 100644 --- a/lib/routes/parliament/namespace.ts +++ b/lib/routes/parliament/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Thailand Parliament', url: 'parliament.go.th', + lang: 'en', }; diff --git a/lib/routes/parliament/section77.ts b/lib/routes/parliament/section77.ts index 3e14ee2082577a..dcc140e279f563 100644 --- a/lib/routes/parliament/section77.ts +++ b/lib/routes/parliament/section77.ts @@ -22,9 +22,9 @@ export const route: Route = { maintainers: ['itpcc'], handler, description: `| Presented by MP \* | Presented by People \* | Hearing Ongoing | Hearing ended | Hearing result reported | Waiting for PM approval | Assigned into the session | Processed | PM Rejected | - | ------------------------ | ---------------------- | ------------------- | --------------- | ------------------------ | ----------------------- | ------------------------- | ---------- | ------------- | - | presentbymp | presentbyperson | openwsu | closewsu | reportwsu | substatus1 | substatus2 | substatus3 | closewsubypm | - | เสนอโดยสมาชิกสภาผู้แทนราษฏร | เสนอโดยประชาชน | กำลังเปิดรับฟังความคิดเห็น | ปิดรับฟังความคิดเห็น | รายงานผลการรับฟังความคิดเห็น | รอคำรับรองจากนายกรัฐมนตรี | บรรจุเข้าระเบียบวาระ | พิจารณาแล้ว | นายกฯ ไม่รับรอง | +| ------------------------ | ---------------------- | ------------------- | --------------- | ------------------------ | ----------------------- | ------------------------- | ---------- | ------------- | +| presentbymp | presentbyperson | openwsu | closewsu | reportwsu | substatus1 | substatus2 | substatus3 | closewsubypm | +| เสนอโดยสมาชิกสภาผู้แทนราษฏร | เสนอโดยประชาชน | กำลังเปิดรับฟังความคิดเห็น | ปิดรับฟังความคิดเห็น | รายงานผลการรับฟังความคิดเห็น | รอคำรับรองจากนายกรัฐมนตรี | บรรจุเข้าระเบียบวาระ | พิจารณาแล้ว | นายกฯ ไม่รับรอง | *Note:* For \`presentbymp\` and \`presentbyperson\`, it can also add: diff --git a/lib/routes/patagonia/namespace.ts b/lib/routes/patagonia/namespace.ts index dc1334c7a46852..664d3e0a65d7d2 100644 --- a/lib/routes/patagonia/namespace.ts +++ b/lib/routes/patagonia/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Patagonia', url: 'patagonia.com', + lang: 'en', }; diff --git a/lib/routes/patagonia/new-arrivals.ts b/lib/routes/patagonia/new-arrivals.ts index a5768bfc1cf181..09a1b7a5006537 100644 --- a/lib/routes/patagonia/new-arrivals.ts +++ b/lib/routes/patagonia/new-arrivals.ts @@ -36,8 +36,8 @@ export const route: Route = { maintainers: [], handler, description: `| Men's | Women's | Kids' & Baby | Packs & Gear | - | ----- | ------- | ------------ | ------------ | - | mens | womens | kids | luggage |`, +| ----- | ------- | ------------ | ------------ | +| mens | womens | kids | luggage |`, }; async function handler(ctx) { diff --git a/lib/routes/patreon/feed.ts b/lib/routes/patreon/feed.ts new file mode 100644 index 00000000000000..faf814be539c09 --- /dev/null +++ b/lib/routes/patreon/feed.ts @@ -0,0 +1,126 @@ +import { Route } from '@/types'; +import { CreatorData, MediaRelation, PostData } from './types'; + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +import { art } from '@/utils/render'; +import { config } from '@/config'; + +const __dirname = getCurrentPath(import.meta.url); + +export const route: Route = { + path: '/:creator', + categories: ['new-media'], + example: '/patreon/straightupsisters', + parameters: { creator: 'Patreon creator id, can be found in the url' }, + features: { + requireConfig: [ + { + name: 'PATREON_SESSION_ID', + optional: true, + description: 'The value of the session_id cookie after logging in to Patreon, required to access paid posts', + }, + ], + }, + radar: [ + { + source: ['patreon.com/:creator'], + }, + ], + name: 'Home', + maintainers: ['TonyRL'], + handler, +}; + +async function handler(ctx) { + const { creator } = ctx.req.param(); + + const baseUrl = 'https://www.patreon.com'; + const link = `${baseUrl}/${creator}`; + + const creatorData = (await cache.tryGet(`patreon:creator:${creator}`, async () => { + const response = await ofetch(link); + + const $ = cheerio.load(response); + const nextData = JSON.parse($('#__NEXT_DATA__').text()); + const bootstrapEnvelope = nextData.props.pageProps.bootstrapEnvelope; + + return { + meta: bootstrapEnvelope.meta, + id: bootstrapEnvelope.pageBootstrap.campaign.data.id, + attributes: bootstrapEnvelope.pageBootstrap.campaign.data.attributes, + }; + })) as CreatorData; + + if (!creatorData.id) { + throw new Error('Creator not found'); + } + + let headers = {}; + if (config.patreon?.sessionId) { + headers = { + Cookie: `session_id=${config.patreon.sessionId}`, + }; + } + + const posts = await ofetch<PostData>('https://www.patreon.com/api/posts', { + headers, + query: { + include: + 'campaign,access_rules,access_rules.tier.null,attachments_media,audio,audio_preview.null,drop,images,media,native_video_insights,poll.choices,poll.current_user_responses.user,poll.current_user_responses.choice,poll.current_user_responses.poll,user,user_defined_tags,ti_checks,video.null,content_unlock_options.product_variant.null', + 'fields[campaign]': 'currency,show_audio_post_download_links,avatar_photo_url,avatar_photo_image_urls,earnings_visibility,is_nsfw,is_monthly,name,url', + 'fields[post]': + 'change_visibility_at,comment_count,commenter_count,content,created_at,current_user_can_comment,current_user_can_delete,current_user_can_report,current_user_can_view,current_user_comment_disallowed_reason,current_user_has_liked,embed,image,insights_last_updated_at,is_paid,like_count,meta_image_url,min_cents_pledged_to_view,monetization_ineligibility_reason,post_file,post_metadata,published_at,patreon_url,post_type,pledge_url,preview_asset_type,thumbnail,thumbnail_url,teaser_text,title,upgrade_url,url,was_posted_by_campaign_owner,has_ti_violation,moderation_status,post_level_suspension_removal_date,pls_one_liners_by_category,video,video_preview,view_count,content_unlock_options,is_new_to_current_user,watch_state', + 'fields[post_tag]': 'tag_type,value', + 'fields[user]': 'image_url,full_name,url', + 'fields[access_rule]': 'access_rule_type,amount_cents', + 'fields[media]': 'id,image_urls,display,download_url,metadata,file_name', + 'fields[native_video_insights]': 'average_view_duration,average_view_pct,has_preview,id,last_updated_at,num_views,preview_views,video_duration', + 'fields[content-unlock-option]': 'content_unlock_type', + 'fields[product-variant]': 'price_cents,currency_code,checkout_url,is_hidden,published_at_datetime,content_type,orders_count,access_metadata', + 'filter[campaign_id]': creatorData.id, + 'filter[contains_exclusive_posts]': true, + 'filter[is_draft]': false, + sort: '-published_at', + 'json-api-use-default-includes': false, + 'json-api-version': '1.0', + }, + }); + + const items = posts.data.map(({ attributes, relationships }) => { + for (const [key, value] of Object.entries(relationships)) { + if (value.data) { + relationships[key] = Array.isArray(value.data) ? value.data.map((item) => posts.included.find((i) => i.id === item.id)) : posts.included.find((i) => i.id === value.data.id); + } + } + if (attributes.video_preview) { + relationships.video_preview = posts.included.find((i) => Number.parseInt(i.id) === attributes.video_preview?.media_id) as unknown as MediaRelation; + } + + return { + title: attributes.title, + description: art(path.join(__dirname, 'templates/description.art'), { + attributes, + relationships, + included: posts.included, + }), + link: attributes.url, + pubDate: parseDate(attributes.published_at), + image: attributes.thumbnail?.url ?? attributes.image?.url, + category: relationships.user_defined_tags?.map((tag) => tag.attributes.value), + }; + }); + + return { + title: creatorData.meta.title, + description: creatorData.meta.desc, + link, + image: creatorData.attributes.avatar_photo_url, + item: items, + allowEmpty: true, + }; +} diff --git a/lib/routes/patreon/namespace.ts b/lib/routes/patreon/namespace.ts new file mode 100644 index 00000000000000..d6f8fece4102de --- /dev/null +++ b/lib/routes/patreon/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Patreon', + url: 'www.patreon.com', + lang: 'en', +}; diff --git a/lib/routes/patreon/templates/description.art b/lib/routes/patreon/templates/description.art new file mode 100644 index 00000000000000..3a5da8e4e64b81 --- /dev/null +++ b/lib/routes/patreon/templates/description.art @@ -0,0 +1,41 @@ +{{ if attributes.post_type === 'image_file' }} + {{ each attributes.post_metadata.image_order mediaIdStr }} + {{ set img = included.find((i) => i.id === mediaIdStr) }} + {{ if img }} + <img src="{{ img.attributes.image_urls.original }}" alt="{{ img.attributes.file_name }}"><br> + {{ /if }} + {{ /each }} + +{{ else if attributes.post_type === 'video_external_file' }} + {{ if attributes.video_preview }} + <video controls preload="metadata" poster="{{ attributes.image.url }}"> + <source src="{{ relationships.video_preview.attributes.download_url }}" type="video/mp4"> + </video><br> + {{ /if }} + +{{ else if attributes.post_type === 'audio_file' || attributes.post_type === 'podcast' }} + <img src="{{ attributes.thumbnail.url }}"><br> + {{ set url = relationships.audio.attributes.download_url || relationships.audio_preview.attributes.download_url }} + <audio controls preload="metadata"> + <source src="{{ url }}" type="audio/mpeg"> + </audio><br> + +{{ else if attributes.post_type === 'video_embed' || attributes.post_type === 'link' }} + <img src="{{ attributes.image.url }}"><br> + +{{ else if attributes.post_type === 'text_only' }} + +{{ else }} +Post type: "{{ attributes.post_type }}" is not supported. <br> + +{{ /if }} + +{{ if attributes.content || attributes.teaser_text }} + {{@ attributes.content || attributes.teaser_text }} +{{ /if }} + +{{ if relationships.attachments_media }} + {{ each relationships.attachments_media media }} + <a href="{{ media.attributes.download_url }}">{{ media.attributes.file_name }}</a><br> + {{ /each }} +{{ /if }} diff --git a/lib/routes/patreon/types.ts b/lib/routes/patreon/types.ts new file mode 100644 index 00000000000000..4ad4ea71b96a08 --- /dev/null +++ b/lib/routes/patreon/types.ts @@ -0,0 +1,383 @@ +export interface CreatorData { + meta: { + desc: string; + height: null; + imageUrl: string; + isPrivate: boolean; + key: string; + openGraph: OpenGraph; + title: string; + url: string; + videoHeight: null; + videoUrl: null; + videoWidth: null; + viewport: string; + }; + id: string; + attributes: IncludedAttributes; +} + +interface OpenGraph { + desc: null; + imageUrl: null; + title: null; +} + +export interface PostData { + data: Datum[]; + included: IncludedItem[]; + links: Links; + meta: Meta; +} + +interface Datum { + attributes: Attributes; + id: string; + relationships: Relationships; + type: string; +} + +interface Attributes { + change_visibility_at: null; + comment_count: number; + commenter_count: number; + created_at: string; + current_user_can_comment: boolean; + current_user_can_delete: boolean; + current_user_can_report: boolean; + current_user_can_view: boolean; + current_user_comment_disallowed_reason: string; + has_ti_violation: boolean; + image: Image; + is_new_to_current_user: boolean; + is_paid: boolean; + like_count: number; + meta_image_url: string; + min_cents_pledged_to_view: number | null; + moderation_status: string; + patreon_url: string; + pledge_url: string; + pls_one_liners_by_category: any[]; // Items are `false`, so an empty array + post_level_suspension_removal_date: null; + post_metadata: PostMetadata; + post_type: string; + preview_asset_type: string | null; + published_at: string; + teaser_text: string | null; + title: string; + upgrade_url: string; + url: string; + video_preview: VideoPreview | null; + was_posted_by_campaign_owner: boolean; + thumbnail?: Thumbnail; + content?: string; + embed?: null; + post_file?: PostFile; +} + +interface Image { + height: number; + url: string; + width: number; + large_url?: string; + thumb_square_large_url?: string; + thumb_square_url?: string; + thumb_url?: string; + thumbnail: string; + default?: string; + default_blurred?: string; + default_blurred_small?: string; + default_small?: string; + original?: string; + thumbnail_large?: string; + thumbnail_small?: string; +} + +interface PostMetadata { + image_order?: string[]; + platform?: object; +} + +interface VideoPreview { + closed_captions_enabled: boolean; + default_thumbnail: DefaultThumbnail; + duration: number; + expires_at: string; + full_content_duration: number; + full_duration: number; + height: number; + media_id: number; + progress: Progress; + state: string; + url: string; + video_issues: object; + width: number; +} + +interface DefaultThumbnail { + url: string; +} + +interface Progress { + is_watched: boolean; + watch_state: string; +} + +interface PostFile { + height: number; + image_colors: ImageColors; + media_id: number; + state: string; + url: string; + width: number; +} + +interface ImageColors { + average_colors_of_corners: AverageColorsOfCorners; + dominant_color: string; + palette: string[]; + text_color: string; +} + +interface AverageColorsOfCorners { + bottom_left: string; + bottom_right: string; + top_left: string; + top_right: string; +} + +interface Thumbnail { + url: string; + large?: string; + large_2?: string; + square?: string; + gif_url?: string; + height?: number; + width?: number; +} + +interface Relationships { + access_rules: AccessRules; + audio: MediaRelation; + audio_preview: MediaRelation; + campaign: CampaignRelation; + content_unlock_options: ContentUnlockOptions; + drop: Drop; + images: Images; + media: Media; + poll: Poll; + user: UserRelation; + user_defined_tags: UserDefinedTags; + video: MediaRelation; + attachments_media?: AttachmentsMedia; + // Custom relationships + video_preview?: MediaRelation; +} + +interface AccessRules { + data: AccessRuleData[]; +} + +interface AccessRuleData { + id: string; + type: string; +} + +export interface MediaRelation { + data: MediaData | null; + links?: RelatedLink; +} + +interface MediaData { + id: string; + type: string; +} + +interface CampaignRelation { + data: CampaignData; + links: RelatedLink; +} + +interface CampaignData { + id: string; + type: string; +} + +interface RelatedLink { + related: string; +} + +interface ContentUnlockOptions { + data: any[]; // Empty array +} + +interface Drop { + data: null; +} + +interface Images { + data: MediaData[]; +} + +interface Media { + data: MediaData[]; +} + +interface Poll { + data: null; +} + +interface UserRelation { + data: UserData; + links: RelatedLink; +} + +interface UserData { + id: string; + type: string; +} + +interface UserDefinedTags { + data: UserDefinedTagData[]; + attributes: IncludedAttributes; +} + +interface UserDefinedTagData { + id: string; + type: string; +} + +interface AttachmentsMedia { + data: AttachmentMediaData[]; +} + +interface AttachmentMediaData { + id: string; + type: string; +} + +interface IncludedItem { + attributes: IncludedAttributes; + id: string; + type: string; + relationships?: IncludedRelationships; +} + +interface IncludedAttributes { + // Depending on the `type`, attributes vary + // For example, if `type` is 'user', attributes include: + full_name?: string; + image_url?: string | null; + avatar_photo_image_urls?: Image; + avatar_photo_url?: string; + currency?: string; + earnings_visibility?: string; + is_monthly?: boolean; + is_nsfw?: boolean; + name?: string; + show_audio_post_download_links?: boolean; + url?: string; + // Additional properties for other types + // For 'post_tag' type + tag_type?: string; + value?: string; + // For 'display' + display?: Display; + // For 'file' + file_name?: string | null; + image_urls?: Image | null; + metadata?: Metadata; + download_url?: string; + // For 'tier' + access_rule_type?: string; + amount_cents?: number | null; + post_count?: number; + amount?: number; + created_at?: string; + declined_patron_count?: number; + description?: string; + discord_role_ids?: null; + edited_at?: string; + is_free_tier?: boolean; + patron_amount_cents?: number; + patron_currency?: string; + published?: boolean; + published_at?: string; + remaining?: null; + requires_shipping?: boolean; + title?: string; + unpublished_at?: null; +} + +interface Display { + default_thumbnail?: DefaultThumbnail & { position?: number }; + url?: string; + closed_captions_enabled?: boolean; + expires_at?: string; + height?: number; + video_issues?: VideoIssues; + width?: number; + duration?: number; + full_content_duration?: number; + media_id?: number; + progress?: Progress; + state?: string; + image_colors?: ImageColors; +} + +interface VideoIssues { + processing_warning?: { + video_bitrate?: string; + video_codec?: string; + video_resolution?: string; + }; +} + +interface Metadata { + audio_preview_duration?: number; + audio_preview_start_time?: number; + video_preview_end_ms?: number | null; + video_preview_start_ms?: number | null; + duration?: number; + start_position?: number; + duration_s?: number; + orientation?: string; + variant?: string; + dimensions?: Dimensions; +} + +interface Dimensions { + h: number; + w: number; +} + +interface IncludedRelationships { + tier?: TierRelation; +} + +interface TierRelation { + data: TierData | null; + links?: RelatedLink; +} + +interface TierData { + id: string; + type: string; +} + +interface Links { + next: string; +} + +interface Meta { + pagination: Pagination; +} + +interface Pagination { + cursors: Cursors; + total: number; +} + +interface Cursors { + next: string; +} diff --git a/lib/routes/paulgraham/namespace.ts b/lib/routes/paulgraham/namespace.ts index c25c4be92720c2..714a4a6ebaccb0 100644 --- a/lib/routes/paulgraham/namespace.ts +++ b/lib/routes/paulgraham/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Paul Graham', url: 'paulgraham.com', + lang: 'en', }; diff --git a/lib/routes/pconline/focus.ts b/lib/routes/pconline/focus.ts new file mode 100644 index 00000000000000..a9a3a4c2e8f5e2 --- /dev/null +++ b/lib/routes/pconline/focus.ts @@ -0,0 +1,143 @@ +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 = 'https://www.pconline.com.cn'; +interface Item { + id: string; + title: string; + channelName: string; + wap_3g_url: string; + bigImage: string; + cover: string; + largeImage: string; + authorname: string; + authorImg: string; + pc_pubDate: string; + artType: string; + summary: string; + url: string; +} + +const categories = { + all: { + title: '全部', + path: '', + }, + tech: { + title: '科技', + path: 'tech/', + }, + finance: { + title: '财经', + path: 'finance/', + }, + life: { + title: '生活', + path: 'life/', + }, + company: { + title: '公司', + path: 'company/', + }, + character: { + title: '人物', + path: 'character/', + }, +}; + +const getContent = (item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got({ + method: 'get', + url: `https:${item.link}`, + responseType: 'arrayBuffer', + }); + + const utf8decoder = new TextDecoder('GBK'); + const html = utf8decoder.decode(detailResponse.data); + const $ = load(html); + item.description = $('.context-box .context-table tbody td').html(); + return item; + }); + +export const handler = async (ctx) => { + const { category = 'all' } = ctx.req.param(); + const cate = categories[category] || categories.all; + const currentUrl = `${rootUrl}/3g/other/focus/${cate.path}index.html`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const resString = response.data + .replace(/Module\.callback\((.*)\)/s, '$1') + .split('\n') + .filter((e) => e.indexOf('"tags":') !== 0) + .join('\n') + .replaceAll("'", '"'); + const tinyData = resString.replaceAll(/[\n\r]/g, ''); + const dataString = tinyData.replaceAll(',}', '}'); + const data = JSON.parse(dataString || ''); + const { articleList } = data; + const list = articleList.map((item: Item) => ({ + id: item.id, + title: item.title, + author: [ + { + name: item.authorname, + avatar: item.authorImg, + }, + ], + pubDate: parseDate(item.pc_pubDate), + link: item.url, + description: item.summary, + category: item.channelName, + image: item.cover, + })); + + const items = await Promise.all(list.map((item) => getContent(item))); + + return { + title: `太平洋科技-${cate.title}`, + link: currentUrl, + item: items, + }; +}; + +export const route: Route = { + path: '/focus/:category?', + categories: ['new-media', 'popular'], + example: '/pconline/focus', + parameters: { + category: { + description: '科技新闻的类别,获取最新的一页,分别:all, tech, finance, life, company, character', + default: 'all', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['pconline.com.cn/focus/', 'pconline.com.cn/'], + target: '/focus', + }, + ], + name: '科技新闻', + maintainers: ['CH563'], + handler, + description: `::: tip +| 全部 | 科技 | 财经 | 生活 | 公司 | 人物 | +| --- | --- | --- | --- | --- | --- | +| all | tech | finance | life | company | character | +:::`, +}; diff --git a/lib/routes/pconline/namespace.ts b/lib/routes/pconline/namespace.ts new file mode 100644 index 00000000000000..99ddb9396627d2 --- /dev/null +++ b/lib/routes/pconline/namespace.ts @@ -0,0 +1,11 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '太平洋科技', + url: 'pconline.com.cn', + description: ` +::: tip +太平洋科技是专业IT门户网站,为用户和经销商提供IT资讯和行情报价,涉及电脑,手机,数码产品,软件等. +:::`, + lang: 'zh-CN', +}; diff --git a/lib/routes/pencilnews/index.ts b/lib/routes/pencilnews/index.ts new file mode 100644 index 00000000000000..f39f9a687bb5c2 --- /dev/null +++ b/lib/routes/pencilnews/index.ts @@ -0,0 +1,70 @@ +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: '/', + categories: ['new-media'], + example: '/pencilnews', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '文章列表', + maintainers: ['defp'], + handler, + description: '获取铅笔道最新文章', +}; + +async function handler() { + const baseUrl = 'https://www.pencilnews.cn'; + const apiUrl = 'https://api.pencilnews.cn/articles'; + + const response = await got(apiUrl, { + searchParams: { + page: 0, + page_size: 20, + }, + }); + + const { + data: { articles }, + } = response.data; + + const items = await Promise.all( + articles.map((article) => { + const info = article.article_info; + const articleId = info.article_id; + const link = `${baseUrl}/p/${articleId}.html`; + + return cache.tryGet(link, async () => { + const detailResponse = await got(link); + const $ = load(detailResponse.data); + const content = $('.article_content').html(); + + return { + title: info.title, + description: content, + link, + author: article.author?.profile?.name || '', + pubDate: parseDate(info.create_at, 'YYYY-MM-DD HH:mm:ss'), + category: [], + guid: articleId, + }; + }); + }) + ); + + return { + title: '铅笔道', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/pencilnews/namespace.ts b/lib/routes/pencilnews/namespace.ts new file mode 100644 index 00000000000000..7de632d5c9b6a5 --- /dev/null +++ b/lib/routes/pencilnews/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '铅笔道', + url: 'www.pencilnews.cn', + categories: ['new-media'], + description: '铅笔道是一家专注于泛互联网领域的科技媒体', + lang: 'zh-CN', +}; diff --git a/lib/routes/penguin-random-house/namespace.ts b/lib/routes/penguin-random-house/namespace.ts index e8f75805d1484d..361ccacbc2acbb 100644 --- a/lib/routes/penguin-random-house/namespace.ts +++ b/lib/routes/penguin-random-house/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Penguin Random House', url: 'penguinrandomhouse.com', + lang: 'en', }; diff --git a/lib/routes/people/index.ts b/lib/routes/people/index.ts index 0970a07c011a7c..2400405f439f2f 100644 --- a/lib/routes/people/index.ts +++ b/lib/routes/people/index.ts @@ -32,7 +32,13 @@ async function handler(ctx) { responseType: 'buffer', }); - const $ = load(iconv.decode(response, 'gbk')); + // not seen Content-Type in response headers + // try to parse charset from meta tag + let decodedResponse = iconv.decode(response, 'utf-8'); + const parsedCharset = decodedResponse.match(/<meta.*?charset=["']?([^"'>]+)["']?/i); + const encoding = parsedCharset ? parsedCharset[1].toLowerCase() : 'utf-8'; + decodedResponse = encoding === 'utf-8' ? decodedResponse : iconv.decode(response, encoding); + const $ = load(decodedResponse); $('em').remove(); $('.bshare-more, .page_n, .page').remove(); @@ -41,7 +47,7 @@ async function handler(ctx) { $(e).parent().remove(); }); - let items = $('.p6, div.p2j_list, div.headingNews, div.ej_list_box, .fl, .leftItem') + let items = $('.p6, div.p2j_list, div.headingNews, div.ej_list_box, .leftItem') .find('a') .slice(0, limit) .toArray() @@ -64,12 +70,12 @@ async function handler(ctx) { responseType: 'buffer', }); - const data = iconv.decode(detailResponse, 'gbk'); + const data = iconv.decode(detailResponse, encoding); const content = load(data); content('.paper_num, #rwb_tjyd').remove(); - item.description = content('.rm_txt_con, .show_text').html(); + item.description = content('#rwb_zw').html(); item.pubDate = timezone(parseDate(data.match(/(\d{4}年\d{2}月\d{2}日\d{2}:\d{2})/)[1], 'YYYY年MM月DD日 HH:mm'), +8); } catch (error) { item.description = error; diff --git a/lib/routes/people/liuyan.ts b/lib/routes/people/liuyan.ts index 29ba22582f4f72..ab3cbb6f72b631 100644 --- a/lib/routes/people/liuyan.ts +++ b/lib/routes/people/liuyan.ts @@ -27,8 +27,8 @@ export const route: Route = { handler, url: 'liuyan.people.com.cn/', description: `| 全部 | 待回复 | 办理中 | 已办理 | - | ---- | ------ | ------ | ------ | - | 1 | 2 | 3 | 4 |`, +| ---- | ------ | ------ | ------ | +| 1 | 2 | 3 | 4 |`, }; async function handler(ctx) { diff --git a/lib/routes/people/namespace.ts b/lib/routes/people/namespace.ts index 75641eaf988c24..85ced65fa95c1e 100644 --- a/lib/routes/people/namespace.ts +++ b/lib/routes/people/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '人民网', url: 'people.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/peopo/namespace.ts b/lib/routes/peopo/namespace.ts index 5bc1f24bfabf82..808425121a5217 100644 --- a/lib/routes/peopo/namespace.ts +++ b/lib/routes/peopo/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PeoPo 公民新聞', url: 'peopo.org', + lang: 'zh-TW', }; diff --git a/lib/routes/peopo/topic.ts b/lib/routes/peopo/topic.ts index 874723103274bd..1043ac759e6dda 100644 --- a/lib/routes/peopo/topic.ts +++ b/lib/routes/peopo/topic.ts @@ -29,24 +29,24 @@ export const route: Route = { maintainers: [], handler, description: `| 分類 | ID | - | -------- | --- | - | 社會關懷 | 159 | - | 生態環保 | 113 | - | 文化古蹟 | 143 | - | 社區改造 | 160 | - | 教育學習 | 161 | - | 農業 | 163 | - | 生活休閒 | 162 | - | 媒體觀察 | 164 | - | 運動科技 | 165 | - | 政治經濟 | 166 | - | 北台灣 | 223 | - | 中台灣 | 224 | - | 南台灣 | 225 | - | 東台灣 | 226 | - | 校園中心 | 167 | - | 原住民族 | 227 | - | 天然災害 | 168 |`, +| -------- | --- | +| 社會關懷 | 159 | +| 生態環保 | 113 | +| 文化古蹟 | 143 | +| 社區改造 | 160 | +| 教育學習 | 161 | +| 農業 | 163 | +| 生活休閒 | 162 | +| 媒體觀察 | 164 | +| 運動科技 | 165 | +| 政治經濟 | 166 | +| 北台灣 | 223 | +| 中台灣 | 224 | +| 南台灣 | 225 | +| 東台灣 | 226 | +| 校園中心 | 167 | +| 原住民族 | 227 | +| 天然災害 | 168 |`, }; async function handler(ctx) { diff --git a/lib/routes/phoronix/index.ts b/lib/routes/phoronix/index.ts index e4413fd4b96eec..2893cd06de771a 100644 --- a/lib/routes/phoronix/index.ts +++ b/lib/routes/phoronix/index.ts @@ -114,7 +114,7 @@ const tryFetch = async (category, topic) => { export const route: Route = { path: '/:category?/:topic?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/phoronix/linux/KDE', parameters: { category: 'Category', diff --git a/lib/routes/phoronix/namespace.ts b/lib/routes/phoronix/namespace.ts index 43ea439ab59dc6..374537850971c6 100644 --- a/lib/routes/phoronix/namespace.ts +++ b/lib/routes/phoronix/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Phoronix', url: 'phoronix.com', + lang: 'en', }; diff --git a/lib/routes/pianyivps/namespace.ts b/lib/routes/pianyivps/namespace.ts index 5145b28a2b8490..94181323a4060c 100644 --- a/lib/routes/pianyivps/namespace.ts +++ b/lib/routes/pianyivps/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '便宜VPS网', url: 'pianyivps.com', + lang: 'zh-CN', }; diff --git a/lib/routes/pianyuan/namespace.ts b/lib/routes/pianyuan/namespace.ts index 6bb98737bb2bac..c893cd15832667 100644 --- a/lib/routes/pianyuan/namespace.ts +++ b/lib/routes/pianyuan/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '片源网', url: 'pianyuan.org', + lang: 'zh-CN', }; diff --git a/lib/routes/picnob/namespace.ts b/lib/routes/picnob/namespace.ts index 9a1fe65daf0568..4ada4ccd5f940b 100644 --- a/lib/routes/picnob/namespace.ts +++ b/lib/routes/picnob/namespace.ts @@ -2,8 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Instagram', - url: 'picnob.com', - description: `:::tip -It's highly recommended to deploy with Redis cache enabled. -:::`, + url: 'www.instagram.com', + lang: 'en', }; diff --git a/lib/routes/picnob/templates/desc.art b/lib/routes/picnob/templates/desc.art index 9140d9f63fe728..876dfa25a6f8c5 100644 --- a/lib/routes/picnob/templates/desc.art +++ b/lib/routes/picnob/templates/desc.art @@ -1,10 +1,17 @@ -{{ if item.type === 'video' }} - <video poster="{{ item.pic }}" controls> +{{ if item.type === 'video' || item.type === 'igtv' }} + <video poster="{{ item.pic }}" controls preload="metadata"> <source src="{{ item.video }}" type="video/mp4"> </video> {{ else if item.type === 'img_multi' }} {{ each item.images i }} - <img src="{{ i.url }}"> + {{ if i.isVideo }} + <video poster="{{ i.url }}" controls preload="metadata"> + <source src="{{ i.ori }}" type="video/mp4"> + </video> + {{ else }} + <img src="{{ i.url }}"> + {{ /if }} + <br> {{ /each }} {{ else if item.type === 'img_sig' }} <img src="{{ item.pic }}"> diff --git a/lib/routes/picnob/user.ts b/lib/routes/picnob/user.ts index 28ffd40f7c1292..d3fced83bfa9bb 100644 --- a/lib/routes/picnob/user.ts +++ b/lib/routes/picnob/user.ts @@ -1,21 +1,25 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; 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 { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; import path from 'node:path'; import { puppeteerGet } from './utils'; import puppeteer from '@/utils/puppeteer'; +import sanitizeHtml from 'sanitize-html'; export const route: Route = { - path: '/user/:id', - categories: ['social-media'], + path: '/user/:id/:type?', + categories: ['social-media', 'popular'], example: '/picnob/user/xlisa_olivex', - parameters: { id: 'Instagram id' }, + parameters: { + id: 'Instagram id', + type: 'Type of profile page (profile or tagged)', + }, features: { requireConfig: false, requirePuppeteer: true, @@ -26,104 +30,173 @@ export const route: Route = { }, radar: [ { - source: ['picnob.com/profile/:id/*'], + source: ['pixwox.com/profile/:id'], target: '/user/:id', }, + { + source: ['pixwox.com/profile/:id/tagged'], + target: '/user/:id/tagged', + }, ], name: 'User Profile - Picnob', - maintainers: ['TonyRL', 'micheal-death'], + maintainers: ['TonyRL', 'micheal-death', 'AiraNadih'], handler, + view: ViewType.Pictures, }; async function handler(ctx) { // NOTE: 'picnob' is still available, but all requests to 'picnob' will be redirected to 'pixwox' eventually const baseUrl = 'https://www.pixwox.com'; const id = ctx.req.param('id'); - const url = `${baseUrl}/profile/${id}/`; + const type = ctx.req.param('type') ?? 'profile'; + const profileUrl = `${baseUrl}/profile/${id}/${type === 'tagged' ? 'tagged/' : ''}`; const browser = await puppeteer(); // TODO: can't bypass cloudflare 403 error without puppeteer - let html; - let usePuppeteer = false; try { - const { data } = await got(url, { - headers: { - accept: 'text/html', - referer: 'https://www.google.com/', - }, - }); - html = data; - } catch (error: any) { - if (error.message.includes('403')) { - html = await puppeteerGet(url, browser); - usePuppeteer = true; - } - } - const $ = load(html); - const profileName = $('h1.fullname').text(); - const userId = $('input[name=userid]').attr('value'); - - let posts; - if (usePuppeteer) { - const data = await puppeteerGet(`${baseUrl}/api/posts?userid=${userId}`, browser); - posts = data.posts; - } else { - const { data } = await got(`${baseUrl}/api/posts`, { - headers: { - accept: 'application/json', - }, - searchParams: { - userid: userId, - }, - }); - posts = data.posts; - } - - const list = await Promise.all( - posts.items.map(async (item) => { - const { shortcode, type, sum_pure, time } = item; - const link = `${baseUrl}/post/${shortcode}/`; - if (type === 'img_multi') { - item.images = await cache.tryGet(link, async () => { - let html; - if (usePuppeteer) { - html = await puppeteerGet(link, browser); - } else { - const { data } = await got(link); - html = data; - } - const $ = load(html); - return [ - ...new Set( - $('.post_slide a') - .toArray() - .map((a: any) => { - a = $(a); - return { - ori: a.attr('href'), - url: a.find('img').attr('data-src'), - }; - }) - ), - ]; + const profile = (await cache.tryGet(`picnob:user:${id}`, async () => { + let html; + let usePuppeteer = false; + try { + const data = await ofetch(profileUrl, { + headers: { + accept: 'text/html', + referer: 'https://www.google.com/', + }, }); + html = data; + } catch { + html = await puppeteerGet(profileUrl, browser); + usePuppeteer = true; + } + const $ = load(html); + const name = $('h1.fullname').text(); + const userId = $('input[name=userid]').attr('value'); + + if (!userId) { + throw new Error('Failed to get user ID'); } return { - title: sum_pure, - description: art(path.join(__dirname, 'templates/desc.art'), { item }), - link, - pubDate: parseDate(time, 'X'), + name, + userId, + description: $('.info .sum').text(), + image: $('.ava .pic img').attr('src'), + usePuppeteer, }; - }) - ); - await browser.close(); - - return { - title: `${profileName} (@${id}) - Picnob`, - description: $('.info .sum').text(), - link: url, - image: $('.ava .pic img').attr('src'), - item: list, - }; + })) as { + name: string; + userId: string; + description: string; + image: string; + usePuppeteer: boolean; + }; + + let profileTitle; + let endpoint; + if (type === 'tagged') { + profileTitle = `${profile.name} (@${id}) tagged posts - Picnob`; + endpoint = 'tagged'; + } else { + profileTitle = `${profile.name} (@${id}) public posts - Picnob`; + endpoint = 'posts'; + } + + const apiUrl = `${baseUrl}/api/${endpoint}`; + + let responseData; + try { + if (profile.usePuppeteer) { + responseData = await puppeteerGet(`${apiUrl}?userid=${profile.userId}`, browser); + responseData = typeof responseData === 'string' ? JSON.parse(responseData) : responseData; + } else { + const data = await ofetch(apiUrl, { + headers: { + accept: 'application/json', + }, + query: { + userid: profile.userId, + }, + }); + responseData = typeof data === 'string' ? JSON.parse(data) : data; + } + } catch { + responseData = await puppeteerGet(`${apiUrl}?userid=${profile.userId}`, browser); + responseData = typeof responseData === 'string' ? JSON.parse(responseData) : responseData; + } + + const posts = responseData?.posts; + + if (!posts?.items?.length) { + throw new Error('No posts found'); + } + + if (type === 'tagged') { + posts.items = posts.items.map((post, index) => { + const taggedPost = responseData.tagged.items[index] || {}; + return { ...taggedPost, ...post }; + }); + } + + const list = await Promise.all( + posts.items.map(async (item) => { + const { shortcode, sum, sum_pure, type, time } = item; + + const link = `${baseUrl}/post/${shortcode}/`; + if (type === 'img_multi') { + item.images = await cache.tryGet(link, async () => { + let html; + if (profile.usePuppeteer) { + html = await puppeteerGet(link, browser); + } else { + const data = await ofetch(link); + html = data; + } + const $ = load(html); + return [ + ...new Set( + $('.post_slide a') + .toArray() + .map((a: any) => { + a = $(a); + return { + ori: a.attr('href'), + url: a.find('img').attr('data-src'), + isVideo: !!a.find('.icon_play').length, + }; + }) + ), + ]; + }); + } + + return { + title: sanitizeHtml(sum.split('\n')[0], { allowedTags: [], allowedAttributes: {} }) || sum_pure, + description: art(path.join(__dirname, 'templates/desc.art'), { + item: { + ...item, + // Fix linebreaks + sum: sum.replaceAll('\n', '<br>'), + }, + }), + link, + guid: shortcode, + pubDate: parseDate(time, 'X'), + }; + }) + ); + + return { + title: profileTitle, + description: profile.description, + link: profileUrl, + image: profile.image, + item: list, + }; + } catch (error) { + await browser.close(); + throw error; + } finally { + await browser.close(); + } } diff --git a/lib/routes/picuki/namespace.ts b/lib/routes/picuki/namespace.ts index b74e2b1dabd3a0..4ada4ccd5f940b 100644 --- a/lib/routes/picuki/namespace.ts +++ b/lib/routes/picuki/namespace.ts @@ -2,8 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Instagram', - url: 'www.picuki.com', - description: `:::tip -It's highly recommended to deploy with Redis cache enabled. -:::`, + url: 'www.instagram.com', + lang: 'en', }; diff --git a/lib/routes/picuki/profile.ts b/lib/routes/picuki/profile.ts index 98ca59f3c3fdc9..a19cca2341021a 100644 --- a/lib/routes/picuki/profile.ts +++ b/lib/routes/picuki/profile.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { DataItem, Route } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); @@ -16,19 +16,37 @@ function deVideo(media) { let media_deVideo = ''; $('img,video').each((_, medium) => { const tag = medium.name; - medium = $(medium); - const poster = medium.attr('poster'); + const mediumElement = $(medium); + const poster = mediumElement.attr('poster'); // 如果有 poster 属性,表明它是视频,将它替换为它的 poster;如果不是,就原样返回 - media_deVideo += poster ? `<img src='${poster}' alt='video poster'>` : tag === 'img' ? medium.toString() : ''; + media_deVideo += (() => { + if (poster) { + return `<img src='${poster}' alt='video poster'>`; + } else if (tag === 'img') { + return mediumElement.toString(); + } else { + return ''; + } + })(); }); return media_deVideo; } export const route: Route = { - path: '/profile/:id/:functionalFlag?', + path: '/profile/:id/:type?/:functionalFlag?', categories: ['social-media'], example: '/picuki/profile/stefaniejoosten', - parameters: { id: 'Instagram id', functionalFlag: 'functional flag, see the table below' }, + parameters: { + id: 'Instagram user id', + type: 'Type of profile page (profile or tagged)', + functionalFlag: `functional flag, see the table below +| functionalFlag | Video embedding | Fetching Instagram Stories | +| -------------- | --------------------------------------- | -------------------------- | +| 0 | off, only show video poster as an image | off | +| 1 (default) | on | off | +| 10 | on | on | +`, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -42,20 +60,19 @@ export const route: Route = { source: ['www.picuki.com/profile/:id'], target: '/profile/:id', }, + { + source: ['www.picuki.com/profile-tagged/:id'], + target: '/profile/:id/tagged', + }, ], name: 'User Profile - Picuki', - maintainers: ['hoilc', 'Rongronggg9', 'devinmugen'], + maintainers: ['hoilc', 'Rongronggg9', 'devinmugen', 'NekoAria'], handler, - description: `| functionalFlag | Video embedding | Fetching Instagram Stories | - | -------------- | --------------------------------------- | -------------------------- | - | 0 | off, only show video poster as an image | off | - | 1 (default) | on | off | - | 10 | on | on | - - :::warning + description: ` +::: warning Instagram Stories do not have a reliable guid. It is possible that your RSS reader show the same story more than once. Though, every Story expires after 24 hours, so it may be not so serious. - :::`, +:::`, }; async function handler(ctx) { @@ -63,61 +80,58 @@ async function handler(ctx) { const browser = await puppeteer(); const id = ctx.req.param('id'); - const displayVideo = ctx.req.param('functionalFlag') !== '0'; - const includeStories = ctx.req.param('functionalFlag') === '10'; + const type = ctx.req.param('type') ?? 'profile'; + const functionalFlag = ctx.req.param('functionalFlag') ?? '1'; + const displayVideo = functionalFlag !== '0'; + const includeStories = functionalFlag === '10'; - const profileUrl = `https://www.picuki.com/profile/${id}`; + const profileUrl = `https://www.picuki.com/${type === 'tagged' ? 'profile-tagged' : 'profile'}/${id}`; - const data = await cache.tryGet( - `picuki-${id}-profile-${includeStories}`, - async () => { - const _r = await puppeteerGet(profileUrl, browser, includeStories); - return _r; - }, - config.cache.routeExpire, - false - ); + const key = `picuki-${id}-${type}-${includeStories}`; + const data = await cache.tryGet(key, () => puppeteerGet(profileUrl, browser, includeStories), config.cache.routeExpire, false); const $ = load(data); const profileName = $('.profile-name-bottom').text(); + const profileTitle = type === 'tagged' ? `${profileName} (@${id}) tagged posts - Picuki` : `${profileName} (@${id}) public posts - Picuki`; const profileImg = $('.profile-avatar > img').attr('src'); const profileDescription = $('.profile-description').text(); - const list = $('ul.box-photos [data-s="media"]').get(); + const list = $('ul.box-photos [data-s="media"]').toArray(); + + let items: DataItem[] = []; - let items = []; + let description: string; if (includeStories) { const stories_wrapper = $('.stories_wrapper'); if (stories_wrapper.length) { const storyItems = $(stories_wrapper) .find('.item') - .get() + .toArray() .map((item) => { const $item = $(item); - let title = $item.find('.stories_count'); - title = title.length ? title.text() : ''; - const pubDate = title ? chrono.parseDate(title) : new Date(); + const titleElement = $item.find('.stories_count'); + const title = titleElement.length ? titleElement.text() : ''; + const pubDate = chrono.parseDate(title) || new Date(); const postBox = $item.find('.launchLightbox'); const poster = postBox.attr('data-video-poster'); const href = postBox.attr('href'); const type = postBox.attr('data-post-type'); // video / image const origin = postBox.attr('data-origin'); const storiesBackground = $item.find('.stories_background'); - const storiesBackgroundUrl = storiesBackground && storiesBackground.css('background-image').match(/url\('?(.*)'?\)/); - const storiesBackgroundUrlSrc = storiesBackgroundUrl && storiesBackgroundUrl[1]; - let description; + const storiesBackgroundUrl = storiesBackground?.css('background-image')?.match(/url\('?(.*)'?\)/); + const storiesBackgroundUrlSrc = storiesBackgroundUrl?.[1]; if (type === 'video') { description = art(path.join(__dirname, 'templates/video.art'), { videoSrcs: [href, origin].filter(Boolean), - videoPoster: poster || storiesBackgroundUrlSrc || '', + videoPoster: poster ?? storiesBackgroundUrlSrc ?? '', }); } else if (type === 'image') { - description = `<img src="${href || poster || storiesBackgroundUrlSrc || ''}" alt="Instagram Story">`; + description = `<img src="${href ?? poster ?? storiesBackgroundUrlSrc ?? ''}" alt="Instagram Story">`; } return { - title: 'Instagram Story' + (pubDate ? '' : `: ${title}`), + title: title ? `Instagram Story: ${title}` : 'Instagram Story', author: `@${id}`, description, link: href, @@ -145,8 +159,8 @@ async function handler(ctx) { $('.single-photo img').each((_, item) => { html += $(item).toString(); }); - $('.single-photo video').each((_, item) => { - item = $(item); + $('.single-photo video').each((_, element) => { + const item = $(element); let videoSrc = item.attr('src'); if (videoSrc === undefined) { videoSrc = item.children().attr('src'); @@ -154,8 +168,9 @@ async function handler(ctx) { const videoPoster = item.attr('poster'); let origin = item.parent().attr('onclick'); if (origin) { - origin = origin.match(/window\.open\('([^']*)'/); - origin = origin && origin[1]; + const regex = /window\.open\('([^']*)'/; + const match = regex.exec(origin); + origin = match?.[1]; } html += art(path.join(__dirname, 'templates/video.art'), { videoSrcs: [videoSrc, origin].filter(Boolean), @@ -168,15 +183,15 @@ async function handler(ctx) { items = [ ...items, ...(await Promise.all( - list.map(async (post) => { - post = $(post); + list.map(async (item) => { + const post = $(item); const postLink = post.find('.photo > a').attr('href'); const postTime = post.find('.time'); - const pubDate = postTime ? chrono.parseDate(postTime.text()) : new Date(); + const pubDate = postTime?.text() ? (chrono.parseDate(postTime.text()) ?? new Date()) : new Date(); const media_displayVideo = await getMedia(postLink); const postText = post - .find('.photo-description') + .find('.photo-action-description') .text() .trim() .replaceAll(/[^\S\n]+/g, ' ') @@ -202,7 +217,7 @@ async function handler(ctx) { await browser.close(); return { - title: `${profileName} (@${id}) - Picuki`, + title: profileTitle, link: profileUrl, image: profileImg, description: profileDescription, diff --git a/lib/routes/pikabu/namespace.ts b/lib/routes/pikabu/namespace.ts index 6cdfe65fd4f0f9..f3481530300021 100644 --- a/lib/routes/pikabu/namespace.ts +++ b/lib/routes/pikabu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Pikabu', url: 'pikabu.ru', + lang: 'ru', }; diff --git a/lib/routes/pincong/namespace.ts b/lib/routes/pincong/namespace.ts index d87c741de2f68a..46dcc44e3b8e2a 100644 --- a/lib/routes/pincong/namespace.ts +++ b/lib/routes/pincong/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '品葱', url: 'pincong.rocks', + lang: 'zh-CN', }; diff --git a/lib/routes/pincong/utils.ts b/lib/routes/pincong/utils.ts index b5d901d73bf48e..db5744aaca4aca 100644 --- a/lib/routes/pincong/utils.ts +++ b/lib/routes/pincong/utils.ts @@ -14,7 +14,7 @@ const puppeteerGet = (url, cache) => waitUntil: 'domcontentloaded', }); const html = await page.evaluate(() => document.documentElement.innerHTML); - browser.close(); + await browser.close(); return html; }); diff --git a/lib/routes/pingwest/namespace.ts b/lib/routes/pingwest/namespace.ts index 9fbdae4e91ef6d..33834877c6dbc8 100644 --- a/lib/routes/pingwest/namespace.ts +++ b/lib/routes/pingwest/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '品玩', url: 'pingwest.com', + lang: 'zh-CN', }; diff --git a/lib/routes/pingwest/status.ts b/lib/routes/pingwest/status.ts index 9031f3895c26be..c40ead2d741413 100644 --- a/lib/routes/pingwest/status.ts +++ b/lib/routes/pingwest/status.ts @@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/status', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/pingwest/status', parameters: {}, features: { diff --git a/lib/routes/pingwest/tag.ts b/lib/routes/pingwest/tag.ts index 60e1807b08bedc..21630ecb523d27 100644 --- a/lib/routes/pingwest/tag.ts +++ b/lib/routes/pingwest/tag.ts @@ -6,7 +6,7 @@ import utils from './utils'; export const route: Route = { path: '/tag/:tag/:type/:option?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/pingwest/tag/ChinaJoy/1', parameters: { tag: '话题名或话题id, 可从话题页url中得到', type: '内容类型', option: '参数, 默认无' }, features: { @@ -22,17 +22,17 @@ export const route: Route = { handler, description: `内容类型 - | 最新 | 热门 | - | ---- | ---- | - | 1 | 2 | +| 最新 | 热门 | +| ---- | ---- | +| 1 | 2 | 参数 - \`fulltext\`,全文输出,例如:\`/pingwest/tag/ChinaJoy/1/fulltext\` - :::tip +::: tip 该路由一次最多显示 30 条文章 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/pingwest/user.ts b/lib/routes/pingwest/user.ts index 6f57f22453f810..907b28e5a8f734 100644 --- a/lib/routes/pingwest/user.ts +++ b/lib/routes/pingwest/user.ts @@ -6,7 +6,7 @@ import utils from './utils'; export const route: Route = { path: '/user/:uid/:type?/:option?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/pingwest/user/7781550877/article', parameters: { uid: '用户id, 可从用户主页中得到', type: '内容类型, 默认为`article`', option: '参数' }, features: { @@ -28,9 +28,9 @@ export const route: Route = { handler, description: `内容类型 - | 文章 | 动态 | - | ------- | ----- | - | article | state | +| 文章 | 动态 | +| ------- | ----- | +| article | state | 参数 diff --git a/lib/routes/pinterest/namespace.ts b/lib/routes/pinterest/namespace.ts new file mode 100644 index 00000000000000..b115b7fac4be39 --- /dev/null +++ b/lib/routes/pinterest/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Pinterest', + url: 'www.pinterest.com', + lang: 'en', +}; diff --git a/lib/routes/pinterest/types.ts b/lib/routes/pinterest/types.ts new file mode 100644 index 00000000000000..ca3b8c8885a5c7 --- /dev/null +++ b/lib/routes/pinterest/types.ts @@ -0,0 +1,330 @@ +export interface BoardsFeedResource { + node_id: string; + name: string; + created_at: string; + section_count: number; + images: BoardImages; + image_cover_hd_url: string; + archived_by_me_at: null; + access: any[]; + image_cover_url: string; + follower_count: number; + event_date: null; + should_show_shop_feed: boolean; + should_show_board_collaborators: boolean; + collaborator_requests_enabled: boolean; + is_temporarily_disabled: boolean; + collaborating_users: any[]; + followed_by_me: boolean; + viewer_collaborator_join_requested: boolean; + cover_pin: CoverPin; + place_saves_count: number; + board_order_modified_at: string; + owner: Owner; + allow_homefeed_recommendations: boolean; + pin_count: number; + collaborator_count: number; + has_custom_cover: boolean; + privacy: string; + should_show_more_ideas: boolean; + event_start_date: null; + url: string; + collaborated_by_me: boolean; + is_collaborative: boolean; + cover_images: CoverImages; + is_ads_only: boolean; + type: string; + description: string; +} + +interface BoardImages { + '170x': ImageItem[]; +} + +interface Images { + orig: ImageItem; + [x: string]: ImageItem; +} + +interface ImageItem { + url: string; + width: number; + height: number; + dominant_color: string; +} + +interface CoverPin { + pin_id: string; + timestamp: number; + image_signature: string; + crop?: number[]; + size?: number[]; + scale?: number; + image_url?: string; + custom_cover?: boolean; + image_size?: number[] | null; +} + +interface Owner { + node_id: string; + explicitly_followed_by_me: boolean; + is_partner: boolean; + is_ads_only_profile: boolean; + is_private_profile: boolean; + ads_only_profile_site: null; + domain_verified: boolean; + type: string; + image_medium_url: string; + is_default_image: boolean; + id: string; + full_name: string; + username: string; + is_verified_merchant: boolean; + verified_identity: object; +} + +interface Pinner extends Owner { + image_large_url: string; + image_small_url: string; +} + +interface CoverImages { + '200x150': ImageItem; + '222x': ImageItem; +} + +export interface UserActivityPinsResource { + node_id: string; + is_stale_product: boolean; + attribution: null; + access: any[]; + images: Images; + comment_count: number; + digital_media_source_type: null; + promoted_is_removable: boolean; + is_eligible_for_pdp: boolean; + sponsorship: null; + story_pin_data_id: string; + description_html: string; + shopping_flags: any[]; + is_uploaded: boolean; + campaign_id: null; + is_playable: boolean; + manual_interest_tags: null; + video_status: null; + seo_url: string; + image_signature: string; + is_eligible_for_web_closeup: boolean; + ad_match_reason: number; + is_oos_product: boolean; + dominant_color: string; + aggregated_pin_data: AggregatedPinData; + creator_analytics: null; + is_repin: boolean; + done_by_me: boolean; + board: Board; + view_tags: any[]; + video_status_message: null; + description: string; + domain: string; + is_downstream_promotion: boolean; + is_video: boolean; + promoter: null; + embed: null; + comments: Comments; + collection_pin: null; + is_promoted: boolean; + grid_title: string; + is_whitelisted_for_tried_it: boolean; + promoted_is_lead_ad: boolean; + debug_info_html: null; + link: null; + is_native: boolean; + id: string; + promoted_lead_form: null; + type: string; + rich_summary: null; + title: string; + alt_text: null; + created_at: string; + is_quick_promotable: boolean; + pinner: Pinner; + image_crop: ImageCrop; + reaction_counts: object; + tracking_params: string; + is_eligible_for_related_products: boolean; + product_pin_data: null; + privacy: string; + price_currency: string; + has_required_attribution_provider: boolean; + seo_title: string; + price_value: number; + insertion_id: null; + seo_noindex_reason: null; + carousel_data: null; + videos: null; + story_pin_data: StoryPinData; + grid_description: string; + repin_count: number; + native_creator: Owner; + should_open_in_stream: boolean; + additional_hide_reasons: any[]; + method: string; +} + +interface AggregatedPinData { + node_id: string; + is_shop_the_look: boolean; + aggregated_stats: AggregatedStats; + did_it_data: DidItData; + creator_analytics: null; + has_xy_tags: boolean; + id: string; +} + +interface AggregatedStats { + saves: number; + done: number; +} + +interface DidItData { + type: string; + details_count: number; + recommend_scores: RecommendScore[]; + rating: number; + tags: any[]; + user_count: number; + videos_count: number; + images_count: number; + recommended_count: number; + responses_count: number; +} + +interface RecommendScore { + score: number; + count: number; +} + +interface Board { + node_id: string; + layout: string; + type: string; + privacy: string; + followed_by_me: boolean; + image_thumbnail_url: string; + name: string; + collaborated_by_me: boolean; + owner: Owner; + is_collaborative: boolean; + url: string; + id: string; +} + +interface Comments { + uri: string; + data: any[]; + bookmark: null; +} + +interface ImageCrop { + min_y: number; + max_y: number; +} + +interface StoryPinData { + node_id: string; + page_count: number; + type: string; + has_affiliate_products: boolean; + static_page_count: number; + pages: Page[]; + metadata: Metadata; + is_deleted: boolean; + total_video_duration: number; + pages_preview: PagePreview[]; + has_product_pins: boolean; + id: string; + last_edited: null; +} + +interface Page { + blocks: Block[]; +} + +interface Block { + type: string; + block_type: number; + text: string; + block_style: BlockStyle; + image_signature: string; + image: null; + tracking_id: string; +} + +interface BlockStyle { + x_coord: number; + corner_radius: number; + rotation: number; + height: number; + width: number; + y_coord: number; +} + +interface Metadata { + basics: null; + is_compatible: boolean; + root_user_id: string; + canvas_aspect_ratio: number; + diy_data: null; + compatible_version: string; + is_editable: boolean; + root_pin_id: string; + pin_title: string | null; + template_type: null; + showreel_data: null; + recipe_data: null; + is_promotable: boolean; + version: string; + pin_image_signature: string; +} + +interface PagePreview { + blocks: Block[]; +} + +export interface UserProfile extends Owner { + image_xlarge_url: string; + impressum_url: null; + seo_canonical_domain: string; + following_count: number; + last_pin_save_time: string; + first_name: string; + eligible_profile_tabs: EligibleProfileTab[]; + seo_noindex_reason: null; + board_count: number; + instagram_data: null; + profile_cover: ProfileCover; + seo_description: string; + follower_count: number; + interest_following_count: null; + is_inspirational_merchant: boolean; + about: string; + partner: null; + website_url: null; + domain_url: null; + seo_title: string; + indexed: boolean; + is_primary_website_verified: boolean; +} + +interface EligibleProfileTab { + id: string; + type: string; + tab_type: number; + name: string; + is_default: boolean; +} + +interface ProfileCover { + id: string; +} diff --git a/lib/routes/pinterest/user.ts b/lib/routes/pinterest/user.ts new file mode 100644 index 00000000000000..568e64146d15c6 --- /dev/null +++ b/lib/routes/pinterest/user.ts @@ -0,0 +1,113 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { BoardsFeedResource, UserActivityPinsResource, UserProfile } from './types'; + +export const route: Route = { + path: '/user/:username/:type?', + categories: ['picture'], + example: '/pinterest/user/howieserious', + parameters: { + username: 'Username', + type: { + description: 'Type, default to `_created`', + default: '_created', + options: [ + { value: '_created', label: 'Created' }, + { value: '_saved', label: 'Saved' }, + ], + }, + }, + radar: [ + { + source: ['www.pinterest.com/:id/:type?', 'www.pinterest.com/:id'], + target: '/user/:id/:type?', + }, + ], + name: 'User', + maintainers: ['TonyRL'], + handler, +}; + +const baseUrl = 'https://www.pinterest.com'; + +async function handler(ctx) { + const { username, type = '_created' } = ctx.req.param(); + + const profile = await getUserResource(username); + const response = type === '_created' ? await getUserActivityPinsResource(username, profile.id) : await getBoardsFeedResource(username); + + const items = + type === '_created' + ? (response as UserActivityPinsResource[]).map((item) => ({ + title: item.title || item.seo_title, + description: `${item.grid_description}<br><img src="${item.images.orig.url}">`, + link: `${baseUrl}${item.seo_url}`, + author: item.pinner.full_name, + pubDate: parseDate(item.created_at), + image: item.images.orig.url, + })) + : (response as BoardsFeedResource[]).map((item) => ({ + title: item.name, + description: item.description + (item.images?.['170x'] ? '<br>' + item.images['170x'].map((img) => `<img src="${img.url}">`).join('') : ''), + link: `${baseUrl}${item.url}`, + author: item.owner.full_name, + pubDate: parseDate(item.created_at), + image: item.image_cover_hd_url, + })); + + return { + title: profile.seo_title, + description: profile.seo_description, + image: profile.image_xlarge_url ?? profile.image_medium_url, + link: `${baseUrl}/${username}/`, + item: items, + }; +} + +const getUserResource = (username: string) => + cache.tryGet(`pinterest:user:${username}`, async () => { + const response = await ofetch(`${baseUrl}/resource/UserResource/get/`, { + headers: { + 'x-pinterest-pws-handler': 'www/[username]/_created.js', + }, + query: { + source_url: `/${username}/_created`, + data: JSON.stringify({ options: { username, field_set_key: 'unauth_profile' }, context: {} }), + _: Date.now(), + }, + }); + + return response.resource_response.data; + }) as Promise<UserProfile>; + +const getUserActivityPinsResource = async (username: string, userId: string) => { + const response = await ofetch(`${baseUrl}/resource/UserActivityPinsResource/get/`, { + headers: { + 'x-pinterest-pws-handler': 'www/[username]/_created.js', + }, + query: { + source_url: `/${username}/_created`, + data: JSON.stringify({ options: { exclude_add_pin_rep: true, field_set_key: 'grid_item', is_own_profile_pins: false, user_id: userId, username }, context: {} }), + _: Date.now(), + }, + }); + + return response.resource_response.data as UserActivityPinsResource[]; +}; + +const getBoardsFeedResource = async (username: string) => { + const response = await ofetch(`${baseUrl}/resource/BoardsFeedResource/get/`, { + headers: { + 'x-pinterest-pws-handler': 'www/[username]/_saved.js', + }, + query: { + source_url: `/${username}/_saved`, + data: JSON.stringify({ options: { field_set_key: 'profile_grid_item', filter_stories: false, sort: 'last_pinned_to', username }, context: {} }), + _: Date.now(), + }, + }); + + return (response.resource_response.data as BoardsFeedResource[]).filter((item) => item.node_id); +}; diff --git a/lib/routes/pixabay/namespace.ts b/lib/routes/pixabay/namespace.ts index cfbd60f5fa8c2f..8b3d4638e89ead 100644 --- a/lib/routes/pixabay/namespace.ts +++ b/lib/routes/pixabay/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Pixabay', url: 'pixabay.com', + lang: 'en', }; diff --git a/lib/routes/pixabay/search.ts b/lib/routes/pixabay/search.ts index 6e0a378f3bec05..199287609df612 100644 --- a/lib/routes/pixabay/search.ts +++ b/lib/routes/pixabay/search.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,9 +10,20 @@ import path from 'node:path'; export const route: Route = { path: '/search/:q/:order?', - categories: ['picture'], + categories: ['picture', 'popular'], + view: ViewType.Pictures, example: '/pixabay/search/cat', - parameters: { q: 'Search term', order: 'Order, `popular` or `latest`, `latest` by default' }, + parameters: { + q: 'Search term', + order: { + description: 'Order', + options: [ + { value: 'popular', label: 'popular' }, + { value: 'latest', label: 'latest' }, + ], + default: 'latest', + }, + }, features: { requireConfig: [ { diff --git a/lib/routes/pixiv/api/get-illust-detail.ts b/lib/routes/pixiv/api/get-illust-detail.ts new file mode 100644 index 00000000000000..64f524f1f82941 --- /dev/null +++ b/lib/routes/pixiv/api/get-illust-detail.ts @@ -0,0 +1,22 @@ +import got from '../pixiv-got'; +import { maskHeader } from '../constants'; +import queryString from 'query-string'; + +/** + * 获取插画详细信息 + * @param {string} illust_id 插画作品 id + * @param {string} token pixiv oauth token + * @returns {Promise<got.AxiosResponse<{illust: IllustDetail}>>} + */ +export default function getIllustDetail(illust_id: string, token: string) { + return got('https://app-api.pixiv.net/v1/illust/detail', { + headers: { + ...maskHeader, + Authorization: 'Bearer ' + token, + }, + searchParams: queryString.stringify({ + illust_id, + filter: 'for_ios', + }), + }); +} diff --git a/lib/routes/pixiv/api/get-ranking.ts b/lib/routes/pixiv/api/get-ranking.ts index 4632763659fa5a..5c169df44ec383 100644 --- a/lib/routes/pixiv/api/get-ranking.ts +++ b/lib/routes/pixiv/api/get-ranking.ts @@ -13,7 +13,7 @@ const allowMode = new Set(['day', 'week', 'month', 'day_male', 'day_female', 'da * @returns {Promise<got.AxiosResponse<{illusts: illust[]}>>} */ export default function getRanking(mode, date, token) { - assert(allowMode.has(mode), 'Mode not allow.'); + assert.ok(allowMode.has(mode), 'Mode not allow.'); return got('https://app-api.pixiv.net/v1/illust/ranking', { headers: { ...maskHeader, diff --git a/lib/routes/pixiv/bookmarks.ts b/lib/routes/pixiv/bookmarks.ts index 10b009faab5730..a49019f2eea0b9 100644 --- a/lib/routes/pixiv/bookmarks.ts +++ b/lib/routes/pixiv/bookmarks.ts @@ -58,7 +58,7 @@ async function handler(ctx) { title: illust.title, author: illust.user.name, pubDate: parseDate(illust.create_date), - description: `<p>画师:${illust.user.name} - 阅览数:${illust.total_view} - 收藏数:${illust.total_bookmarks}</p>${images.join('')}`, + description: `${illust.caption}<br><p>画师:${illust.user.name} - 阅览数:${illust.total_view} - 收藏数:${illust.total_bookmarks}</p>${images.join('')}`, link: `https://www.pixiv.net/artworks/${illust.id}`, }; }), diff --git a/lib/routes/pixiv/illustfollow.ts b/lib/routes/pixiv/illustfollow.ts index 4fb4e6dc7a2855..92c8d875deea61 100644 --- a/lib/routes/pixiv/illustfollow.ts +++ b/lib/routes/pixiv/illustfollow.ts @@ -34,9 +34,9 @@ export const route: Route = { maintainers: ['ClarkeCheng'], handler, url: 'www.pixiv.net/bookmark_new_illust.php', - description: `:::warning + description: `::: warning Only for self-hosted - :::`, +:::`, }; async function handler() { @@ -61,8 +61,9 @@ async function handler() { title: illust.title, author: illust.user.name, pubDate: parseDate(illust.create_date), - description: `<p>画师:${illust.user.name} - 阅览数:${illust.total_view} - 收藏数:${illust.total_bookmarks}</p>${images.join('')}`, + description: `${illust.caption}<br><p>画师:${illust.user.name} - 阅览数:${illust.total_view} - 收藏数:${illust.total_bookmarks}</p>${images.join('')}`, link: `https://www.pixiv.net/artworks/${illust.id}`, + category: illust.tags.map((tag) => tag.name), }; }), }; diff --git a/lib/routes/pixiv/namespace.ts b/lib/routes/pixiv/namespace.ts index 130b407d983303..2d3d0153d2c7b8 100644 --- a/lib/routes/pixiv/namespace.ts +++ b/lib/routes/pixiv/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'pixiv', url: 'www.pixiv.net', + lang: 'ja', }; diff --git a/lib/routes/pixiv/novel-api/content/nsfw.ts b/lib/routes/pixiv/novel-api/content/nsfw.ts new file mode 100644 index 00000000000000..86eab24e670f0f --- /dev/null +++ b/lib/routes/pixiv/novel-api/content/nsfw.ts @@ -0,0 +1,73 @@ +import { JSDOM, VirtualConsole } from 'jsdom'; +import cache from '@/utils/cache'; +import got from '../../pixiv-got'; +import { maskHeader } from '../../constants'; +import queryString from 'query-string'; +import { parseNovelContent } from './utils'; +import type { NovelContent, NSFWNovelDetail } from './types'; +import { parseDate } from '@/utils/parse-date'; + +export async function getNSFWNovelContent(novelId: string, token: string): Promise<NovelContent> { + return (await cache.tryGet(`https://app-api.pixiv.net/webview/v2/novel:${novelId}`, async () => { + const response = await got('https://app-api.pixiv.net/webview/v2/novel', { + headers: { + ...maskHeader, + Authorization: 'Bearer ' + token, + }, + searchParams: queryString.stringify({ + id: novelId, + viewer_version: '20221031_ai', + }), + }); + + const virtualConsole = new VirtualConsole().on('error', () => void 0); + + const { window } = new JSDOM(response.data, { + runScripts: 'dangerously', + virtualConsole, + }); + + const novelDetail = window.pixiv?.novel as NSFWNovelDetail; + + window.close(); + + if (!novelDetail) { + throw new Error('No novel data found'); + } + + const images = Object.fromEntries( + Object.entries(novelDetail.images) + .filter(([, image]) => image?.urls?.original) + .map(([id, image]) => [id, image.urls.original]) + ); + + const parsedContent = await parseNovelContent(novelDetail.text, images, token); + + return { + id: novelDetail.id, + title: novelDetail.title, + description: novelDetail.caption, + content: parsedContent, + + userId: novelDetail.userId, + userName: null, // Not provided in NSFW API + + bookmarkCount: novelDetail.rating.bookmark, + viewCount: novelDetail.rating.view, + likeCount: novelDetail.rating.like, + + createDate: parseDate(novelDetail.cdate), + updateDate: null, // Not provided in NSFW API + + isOriginal: novelDetail.isOriginal, + aiType: novelDetail.aiType, + tags: novelDetail.tags, + + coverUrl: novelDetail.coverUrl, + images, + + seriesId: novelDetail.seriesId || null, + seriesTitle: novelDetail.seriesTitle || null, + }; + })) as NovelContent; +} diff --git a/lib/routes/pixiv/novel-api/content/sfw.ts b/lib/routes/pixiv/novel-api/content/sfw.ts new file mode 100644 index 00000000000000..c58d2489d9a4e7 --- /dev/null +++ b/lib/routes/pixiv/novel-api/content/sfw.ts @@ -0,0 +1,63 @@ +import got from '@/utils/got'; +import cache from '@/utils/cache'; +import pixivUtils from '../../utils'; +import { parseNovelContent } from './utils'; +import { NovelContent, SFWNovelDetail } from './types'; +import { parseDate } from '@/utils/parse-date'; + +const baseUrl = 'https://www.pixiv.net'; + +export async function getSFWNovelContent(novelId: string): Promise<NovelContent> { + const url = `${baseUrl}/ajax/novel/${novelId}`; + return (await cache.tryGet(url, async () => { + const response = await got(url, { + headers: { + referer: `${baseUrl}/novel/show.php?id=${novelId}`, + }, + }); + + const novelDetail = response.data as SFWNovelDetail; + + if (!novelDetail) { + throw new Error('No novel data found'); + } + + const body = novelDetail.body; + const images: Record<string, string> = {}; + + if (novelDetail.body.textEmbeddedImages) { + for (const [id, image] of Object.entries(novelDetail.body.textEmbeddedImages)) { + images[id] = pixivUtils.getProxiedImageUrl(image.urls.original); + } + } + + const parsedContent = await parseNovelContent(novelDetail.body.content, images); + + return { + id: body.id, + title: body.title, + description: body.description, + content: parsedContent, + + userId: body.userId, + userName: body.userName, + + bookmarkCount: body.bookmarkCount, + viewCount: body.viewCount, + likeCount: body.likeCount, + + createDate: parseDate(body.createDate), + updateDate: parseDate(body.uploadDate), + + isOriginal: body.isOriginal, + aiType: body.aiType, + tags: body.tags.tags.map((tag) => tag.tag), + + coverUrl: body.coverUrl, + images, + + seriesId: body.seriesNavData?.seriesId?.toString() || null, + seriesTitle: body.seriesNavData?.title || null, + }; + })) as NovelContent; +} diff --git a/lib/routes/pixiv/novel-api/content/types.ts b/lib/routes/pixiv/novel-api/content/types.ts new file mode 100644 index 00000000000000..752878c73a40a3 --- /dev/null +++ b/lib/routes/pixiv/novel-api/content/types.ts @@ -0,0 +1,254 @@ +export interface NovelContent { + id: string; + title: string; + description: string; + content: string; + + userId: string; + userName: string | null; + + bookmarkCount: number; + viewCount: number; + likeCount: number; + + createDate: Date; + updateDate: Date | null; + + tags: string[]; + + coverUrl: string; + images: Record<string, string>; + + seriesId: string | null; + seriesTitle: string | null; +} + +export interface SFWNovelDetail { + error: boolean; + message: string; + body: { + bookmarkCount: number; + commentCount: number; + markerCount: number; + createDate: string; + uploadDate: string; + description: string; + id: string; + title: string; + likeCount: number; + pageCount: number; + userId: string; + userName: string; + viewCount: number; + isOriginal: boolean; + isBungei: boolean; + xRestrict: number; + restrict: number; + content: string; + coverUrl: string; + suggestedSettings: { + viewMode: number; + themeBackground: number; + themeSize: null; + themeSpacing: null; + }; + isBookmarkable: boolean; + bookmarkData: null; + likeData: boolean; + pollData: null; + marker: null; + tags: { + authorId: string; + isLocked: boolean; + tags: Array<{ + tag: string; + locked: boolean; + deletable: boolean; + userId: string; + userName: string; + }>; + writable: boolean; + }; + seriesNavData: { + seriesType: string; + seriesId: number; + title: string; + isConcluded: boolean; + isReplaceable: boolean; + isWatched: boolean; + isNotifying: boolean; + order: number; + next: { + title: string; + order: number; + id: string; + available: boolean; + } | null; + prev: null; + } | null; + descriptionBoothId: null; + descriptionYoutubeId: null; + comicPromotion: null; + fanboxPromotion: null; + contestBanners: any[]; + contestData: null; + request: null; + imageResponseOutData: any[]; + imageResponseData: any[]; + imageResponseCount: number; + userNovels: { + [key: string]: { + id: string; + title: string; + genre: string; + xRestrict: number; + restrict: number; + url: string; + tags: string[]; + userId: string; + userName: string; + profileImageUrl: string; + textCount: number; + wordCount: number; + readingTime: number; + useWordCount: boolean; + description: string; + isBookmarkable: boolean; + bookmarkData: null; + bookmarkCount: number | null; + isOriginal: boolean; + marker: null; + titleCaptionTranslation: { + workTitle: null; + workCaption: null; + }; + createDate: string; + updateDate: string; + isMasked: boolean; + aiType: number; + seriesId?: string; + seriesTitle?: string; + isUnlisted: boolean; + } | null; + }; + hasGlossary: boolean; + zoneConfig: { + [key: string]: { + url: string; + }; + }; + extraData: { + meta: { + title: string; + description: string; + canonical: string; + descriptionHeader: string; + ogp: { + description: string; + image: string; + title: string; + type: string; + }; + twitter: { + description: string; + image: string; + title: string; + card: string; + }; + }; + }; + titleCaptionTranslation: { + workTitle: null; + workCaption: null; + }; + isUnlisted: boolean; + language: string; + textEmbeddedImages: { + [key: string]: { + novelImageId: string; + sl: string; + urls: { + '240mw': string; + '480mw': string; + '1200x1200': string; + '128x128': string; + original: string; + }; + }; + }; + commentOff: number; + characterCount: number; + wordCount: number; + useWordCount: boolean; + readingTime: number; + genre: string; + aiType: number; + noLoginData: { + breadcrumbs: { + successor: any[]; + current: { + ja: string; + }; + }; + zengoWorkData: { + nextWork: { + id: string; + title: string; + } | null; + prevWork: { + id: string; + title: string; + } | null; + }; + }; + }; +} + +export interface NSFWNovelDetail { + id: string; + title: string; + seriesId: string | null; + seriesTitle: string | null; + seriesIsWatched: boolean | null; + userId: string; + coverUrl: string; + tags: string[]; + caption: string; + cdate: string; + rating: { + like: number; + bookmark: number; + view: number; + }; + text: string; + marker: null; + illusts: string[]; + images: { + [key: string]: { + novelImageId: string; + sl: string; + urls: { + '240mw': string; + '480mw': string; + '1200x1200': string; + '128x128': string; + original: string; + }; + }; + }; + seriesNavigation: { + nextNovel: null; + prevNovel: { + id: number; + viewable: boolean; + contentOrder: string; + title: string; + coverUrl: string; + viewableMessage: null; + } | null; + } | null; + glossaryItems: string[]; + replaceableItemIds: string[]; + aiType: number; + isOriginal: boolean; +} diff --git a/lib/routes/pixiv/novel-api/content/utils.ts b/lib/routes/pixiv/novel-api/content/utils.ts new file mode 100644 index 00000000000000..cbb0d4e0a35dea --- /dev/null +++ b/lib/routes/pixiv/novel-api/content/utils.ts @@ -0,0 +1,134 @@ +import { load } from 'cheerio'; +import getIllustDetail from '../../api/get-illust-detail'; +import pixivUtils from '../../utils'; +import logger from '@/utils/logger'; + +export function convertPixivProtocolExtended(caption: string): string { + const protocolMap = new Map([ + [/pixiv:\/\/novels\/(\d+)/g, 'https://www.pixiv.net/novel/show.php?id=$1'], + [/pixiv:\/\/illusts\/(\d+)/g, 'https://www.pixiv.net/artworks/$1'], + [/pixiv:\/\/users\/(\d+)/g, 'https://www.pixiv.net/users/$1'], + [/pixiv:\/\/novel\/series\/(\d+)/g, 'https://www.pixiv.net/novel/series/$1'], + ]); + + let convertedText = caption; + for (const [pattern, replacement] of protocolMap) { + convertedText = convertedText.replace(pattern, replacement); + } + return convertedText; +} + +// docs: https://www.pixiv.help/hc/ja/articles/235584168-小説作品の本文内に使える特殊タグとは +export async function parseNovelContent(content: string, images: Record<string, string>, token?: string): Promise<string> { + try { + // 如果有 token,處理 pixiv 圖片引用 + // If token exists, process pixiv image references + if (token) { + const imageMatches = [...content.matchAll(/\[pixivimage:(\d+)(?:-(\d+))?\]/g)]; + const imageIdToUrl = new Map<string, string>(); + + // 批量獲取圖片資訊 + // Batch fetch image information + await Promise.all( + imageMatches.map(async ([, illustId, pageNum]) => { + if (!illustId) { + return; + } + + try { + const illust = (await getIllustDetail(illustId, token)).data.illust; + const pixivimages = pixivUtils.getImgs(illust).map((img) => img.match(/src="([^"]+)"/)?.[1] || ''); + + const imageUrl = pixivimages[Number(pageNum) || 0]; + if (imageUrl) { + imageIdToUrl.set(pageNum ? `${illustId}-${pageNum}` : illustId, imageUrl); + } + } catch (error) { + // 記錄錯誤但不中斷處理 + // Log error but don't interrupt processing + logger.warn(`Failed to fetch illust detail for ID ${illustId}: ${error instanceof Error ? error.message : String(error)}`); + } + }) + ); + + // 替換 pixiv 圖片引用為 img 標籤 + // Replace pixiv image references with img tags + content = content.replaceAll(/\[pixivimage:(\d+)(?:-(\d+))?\]/g, (match, illustId, pageNum) => { + const key = pageNum ? `${illustId}-${pageNum}` : illustId; + const imageUrl = imageIdToUrl.get(key); + return imageUrl ? `<img src="${imageUrl}" alt="pixiv illustration ${illustId}${pageNum ? ` page ${pageNum}` : ''}">` : match; + }); + } else { + /* + * 處理 get-novels-sfw 的情況 + * 當沒有 PIXIV_REFRESHTOKEN 時,將 [pixivimage:(\d+)] 格式轉換為 artwork 連結 + * 因無法獲取 Pixiv 作品詳情,改為提供直接連結到原始作品頁面 + * + * Handle get-novels-sfw case + * When PIXIV_REFRESHTOKEN is not available, convert [pixivimage:(\d+)] format to artwork link + * Provide direct link to original artwork page since artwork details cannot be retrieved + */ + content = content.replaceAll(/\[pixivimage:(\d+)(?:-(\d+))?\]/g, (_, illustId) => `<a href="https://www.pixiv.net/artworks/${illustId}" target="_blank" rel="noopener noreferrer">Pixiv Artwork #${illustId}</a>`); + } + + // 處理作者上傳的圖片 + // Process author uploaded images + content = content.replaceAll(/\[uploadedimage:(\d+)\]/g, (match, imageId) => { + if (images[imageId]) { + return `<img src="${pixivUtils.getProxiedImageUrl(images[imageId])}" alt="novel illustration ${imageId}">`; + } + return match; + }); + + // 基本格式處理 + // Basic formatting + content = content + // 換行轉換為 HTML 換行 + // Convert newlines to HTML breaks + .replaceAll('\n', '<br>') + // 連續換行轉換為段落 + // Convert consecutive breaks to paragraphs + .replaceAll(/(<br>){2,}/g, '</p><p>') + // ruby 標籤(為日文漢字標註讀音) + // ruby tags (for Japanese kanji readings) + .replaceAll(/\[\[rb:(.*?)>(.*?)\]\]/g, '<ruby>$1<rt>$2</rt></ruby>') + // 外部連結 + // external links + .replaceAll(/\[\[jumpuri:(.*?)>(.*?)\]\]/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>') + // 頁面跳轉,但由於 [newpage] 使用 hr 分隔,沒有頁數,沒必要跳轉,所以只顯示文字 + // Page jumps, but since [newpage] uses hr separators, without the page numbers, jumping isn't needed, so just display text + .replaceAll(/\[jump:(\d+)\]/g, 'Jump to page $1') + // 章節標題 + // chapter titles + .replaceAll(/\[chapter:(.*?)\]/g, '<h2>$1</h2>') + // 分頁符 + // page breaks + .replaceAll('[newpage]', '<hr>'); + + // 使用 cheerio 進行 HTML 清理和優化 + // Use cheerio for HTML cleanup and optimization + const $content = load(`<article><p>${content}</p></article>`); + + // 處理嵌套段落:移除多餘的嵌套 + // Handle nested paragraphs: remove unnecessary nesting + $content('p p').each((_, elem) => { + const $elem = $content(elem); + $elem.replaceWith($elem.html() || ''); + }); + + // 處理段落中的標題:確保正確的 HTML 結構 + // Handle headings in paragraphs: ensure correct HTML structure + $content('p h2').each((_, elem) => { + const $elem = $content(elem); + const $parent = $elem.parent('p'); + const html = $elem.prop('outerHTML'); + if ($parent.length && html) { + $parent.replaceWith(`</p>${html}<p>`); + } + }); + + return $content.html() || ''; + } catch (error) { + throw new Error(`Error parsing novel content: ${error instanceof Error ? error.message : String(error)}`); + } +} diff --git a/lib/routes/pixiv/novel-api/series/nsfw.ts b/lib/routes/pixiv/novel-api/series/nsfw.ts new file mode 100644 index 00000000000000..bb230fe19c6516 --- /dev/null +++ b/lib/routes/pixiv/novel-api/series/nsfw.ts @@ -0,0 +1,84 @@ +import got from '../../pixiv-got'; +import { maskHeader } from '../../constants'; +import { getNSFWNovelContent } from '../content/nsfw'; +import pixivUtils from '../../utils'; +import { AppNovelSeries, SeriesDetail, SeriesFeed } from './types'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import { getToken } from '../../token'; +import { config } from '@/config'; +import cache from '@/utils/cache'; +import queryString from 'query-string'; + +const baseUrl = 'https://www.pixiv.net'; + +async function getNovelSeries(seriesId: string, offset: number, token: string): Promise<AppNovelSeries> { + const rsp = await got('https://app-api.pixiv.net/v2/novel/series', { + headers: { + ...maskHeader, + Authorization: 'Bearer ' + token, + }, + searchParams: queryString.stringify({ + series_id: seriesId, + last_order: offset, + }), + }); + return rsp.data as AppNovelSeries; +} + +export async function getNSFWSeriesNovels(seriesId: string, limit: number = 10): Promise<SeriesFeed> { + if (limit > 30) { + limit = 30; + } + + if (!config.pixiv || !config.pixiv.refreshToken) { + throw new ConfigNotFoundError('This user is an R18 creator, PIXIV_REFRESHTOKEN is required.\npixiv RSS is disabled due to the lack of relevant config.\n該用戶爲 R18 創作者,需要 PIXIV_REFRESHTOKEN。'); + } + + const token = await getToken(cache.tryGet); + if (!token) { + throw new ConfigNotFoundError('pixiv not login'); + } + + const seriesResponse = await got(`${baseUrl}/ajax/novel/series/${seriesId}`, { + headers: { + ...maskHeader, + Authorization: 'Bearer ' + token, + }, + }); + const seriesData = seriesResponse.data as SeriesDetail; + + let offset = seriesData.body.total - limit; + if (offset < 0) { + offset = 0; + } + const appSeriesData = await getNovelSeries(seriesId, offset, token); + + const items = await Promise.all( + appSeriesData.novels.map(async (novel) => { + const novelContent = await getNSFWNovelContent(novel.id, token); + return { + title: novel.title, + description: ` + <img src="${pixivUtils.getProxiedImageUrl(novelContent.coverUrl)}" /> + <div> + <p>${novelContent.description}</p> + <hr> + ${novelContent.content} + </div> + `, + link: `${baseUrl}/novel/show.php?id=${novel.id}`, + pubDate: novel.create_date, + author: novel.user.name, + category: novelContent.tags, + }; + }) + ); + + return { + title: appSeriesData.novel_series_detail.title, + description: appSeriesData.novel_series_detail.caption, + link: `${baseUrl}/novel/series/${seriesId}`, + image: pixivUtils.getProxiedImageUrl(seriesData.body.cover.urls.original), + item: items, + }; +} diff --git a/lib/routes/pixiv/novel-api/series/sfw.ts b/lib/routes/pixiv/novel-api/series/sfw.ts new file mode 100644 index 00000000000000..d9db33df495b42 --- /dev/null +++ b/lib/routes/pixiv/novel-api/series/sfw.ts @@ -0,0 +1,70 @@ +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { getSFWNovelContent } from '../content/sfw'; +import pixivUtils from '../../utils'; +import { SeriesContentResponse, SeriesFeed } from './types'; + +const baseUrl = 'https://www.pixiv.net'; + +export async function getSFWSeriesNovels(seriesId: string, limit: number = 10): Promise<SeriesFeed> { + const seriesPage = await got(`${baseUrl}/novel/series/${seriesId}`); + const $ = load(seriesPage.data); + + const title = $('meta[property="og:title"]').attr('content') || ''; + const description = $('meta[property="og:description"]').attr('content') || ''; + const image = $('meta[property="og:image"]').attr('content') || ''; + + const response = await got(`${baseUrl}/ajax/novel/series/${seriesId}/content_titles`, { + headers: { + referer: `${baseUrl}/novel/series/${seriesId}`, + }, + }); + + const data = response.data as SeriesContentResponse; + + if (data.error) { + throw new Error(data.message || 'Failed to get series data'); + } + + const chapters = data.body.slice(-Math.abs(limit)); + const chapterStartNum = Math.max(data.body.length - limit + 1, 1); + + const items = await Promise.all( + chapters + .map(async (chapter, index) => { + if (!chapter.available) { + return { + title: `#${chapterStartNum + index} ${chapter.title}`, + description: `PIXIV_REFRESHTOKEN is required to view the full content.<br>需要 PIXIV_REFRESHTOKEN 才能查看完整內文。`, + link: `${baseUrl}/novel/show.php?id=${chapter.id}`, + }; + } + + const novelContent = await getSFWNovelContent(chapter.id); + return { + title: `#${chapterStartNum + index} ${novelContent.title}`, + description: ` + <img src="${pixivUtils.getProxiedImageUrl(novelContent.coverUrl)}" /> + <div> + <p>${novelContent.description}</p> + <hr> + ${novelContent.content} + </div> + `, + link: `${baseUrl}/novel/show.php?id=${novelContent.id}`, + pubDate: novelContent.createDate, + author: novelContent.userName || `User ID: ${novelContent.userId}`, + category: novelContent.tags, + }; + }) + .reverse() + ); + + return { + title, + description, + image: pixivUtils.getProxiedImageUrl(image), + link: `${baseUrl}/novel/series/${seriesId}`, + item: items, + }; +} diff --git a/lib/routes/pixiv/novel-api/series/types.ts b/lib/routes/pixiv/novel-api/series/types.ts new file mode 100644 index 00000000000000..de946d0556e065 --- /dev/null +++ b/lib/routes/pixiv/novel-api/series/types.ts @@ -0,0 +1,94 @@ +export interface SeriesChapter { + id: string; + title: string; + available: boolean; +} + +export interface SeriesContentResponse { + error: boolean; + message: string; + body: SeriesChapter[]; +} + +export interface SeriesDetail { + error: boolean; + message: string; + body: { + id: string; + userId: string; + userName: string; + title: string; + caption: string; + description?: string; + tags: string[]; + publishedContentCount: number; + createDate: string; + updateDate: string; + firstNovelId: string; + latestNovelId: string; + xRestrict: number; + isOriginal: boolean; + total: number; + cover: { + urls: { + original: string; + small?: string; + regular?: string; + original_square?: string; + }; + }; + extraData: { + meta: { + title: string; + description: string; + canonical: string; + ogp: { + description: string; + image: string; + title: string; + type: string; + }; + }; + }; + }; +} + +export interface SeriesFeed { + title: string; + description: string; + image: string; + link: string; + item: Array<{ + title: string; + description: string; + link: string; + pubDate?: Date; + author?: string; + category?: string[]; + }>; +} + +export interface AppUser { + id: number; + name: string; +} + +export interface AppNovelSeriesDetail { + id: string; + title: string; + caption: string; + content_count: number; + is_concluded: boolean; + is_original: boolean; + user: AppUser; +} + +export interface AppNovelSeries { + novel_series_detail: AppNovelSeriesDetail; + novels: { + id: string; + title: string; + create_date: Date; + user: AppUser; + }[]; +} diff --git a/lib/routes/pixiv/novel-api/user-novels/nsfw.ts b/lib/routes/pixiv/novel-api/user-novels/nsfw.ts new file mode 100644 index 00000000000000..b55f188e004e75 --- /dev/null +++ b/lib/routes/pixiv/novel-api/user-novels/nsfw.ts @@ -0,0 +1,82 @@ +import got from '../../pixiv-got'; +import { maskHeader } from '../../constants'; +import queryString from 'query-string'; +import { config } from '@/config'; +import pixivUtils from '../../utils'; +import { getNSFWNovelContent } from '../content/nsfw'; +import { parseDate } from '@/utils/parse-date'; +import { convertPixivProtocolExtended } from '../content/utils'; +import type { NSFWNovelsResponse, NovelList } from './types'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import cache from '@/utils/cache'; +import { getToken } from '../../token'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +function getNovels(user_id: string, token: string): Promise<NSFWNovelsResponse> { + return got('https://app-api.pixiv.net/v1/user/novels', { + headers: { + ...maskHeader, + Authorization: 'Bearer ' + token, + }, + searchParams: queryString.stringify({ + user_id, + filter: 'for_ios', + }), + }); +} + +export async function getNSFWUserNovels(id: string, fullContent: boolean = false, limit: number = 100): Promise<NovelList> { + if (!config.pixiv || !config.pixiv.refreshToken) { + throw new ConfigNotFoundError('This user is an R18 creator, PIXIV_REFRESHTOKEN is required.\npixiv RSS is disabled due to the lack of relevant config.\n該用戶爲 R18 創作者,需要 PIXIV_REFRESHTOKEN。'); + } + + const token = await getToken(cache.tryGet); + if (!token) { + throw new ConfigNotFoundError('pixiv not login'); + } + + const response = await getNovels(id, token); + const novels = limit ? response.data.novels.slice(0, limit) : response.data.novels; + + if (novels.length === 0) { + throw new InvalidParameterError(`${id} is not a valid user ID, or the user has no novels.\n${id} 不是有效的用戶 ID,或者該用戶沒有小說作品。`); + } + + const username = novels[0].user.name; + + const items = await Promise.all( + novels.map(async (novel) => { + const baseItem = { + title: novel.series?.title ? `${novel.series.title} - ${novel.title}` : novel.title, + description: ` + <img src="${pixivUtils.getProxiedImageUrl(novel.image_urls.large)}" /> + <div> + <p>${convertPixivProtocolExtended(novel.caption)}</p> + </div>`, + author: novel.user.name, + pubDate: parseDate(novel.create_date), + link: `https://www.pixiv.net/novel/show.php?id=${novel.id}`, + category: novel.tags.map((t) => t.name), + }; + + if (!fullContent) { + return baseItem; + } + + const { content } = await getNSFWNovelContent(novel.id, token); + + return { + ...baseItem, + description: `${baseItem.description}<hr>${content}`, + }; + }) + ); + + return { + title: `${username}'s novels - pixiv`, + description: `${username} 的 pixiv 最新小说`, + image: pixivUtils.getProxiedImageUrl(novels[0].user.profile_image_urls.medium), + link: `https://www.pixiv.net/users/${id}/novels`, + item: items, + }; +} diff --git a/lib/routes/pixiv/novel-api/user-novels/sfw.ts b/lib/routes/pixiv/novel-api/user-novels/sfw.ts new file mode 100644 index 00000000000000..8b60cac8e92b68 --- /dev/null +++ b/lib/routes/pixiv/novel-api/user-novels/sfw.ts @@ -0,0 +1,74 @@ +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; +import pixivUtils from '../../utils'; +import { getSFWNovelContent } from '../content/sfw'; +import type { SFWNovelsResponse, NovelList } from './types'; + +const baseUrl = 'https://www.pixiv.net'; + +export async function getSFWUserNovels(id: string, fullContent: boolean = false, limit: number = 100): Promise<NovelList> { + const url = `${baseUrl}/users/${id}/novels`; + const { data: allData } = await got(`${baseUrl}/ajax/user/${id}/profile/all`, { + headers: { + referer: url, + }, + }); + + const novels = Object.keys(allData.body.novels) + .sort((a, b) => Number(b) - Number(a)) + .slice(0, Number.parseInt(String(limit), 10)); + + if (novels.length === 0) { + throw new Error('No novels found for this user, or is an R18 creator, fallback to ConfigNotFoundError'); + } + + const searchParams = new URLSearchParams(); + for (const novel of novels) { + searchParams.append('ids[]', novel); + } + + const { data } = (await got(`${baseUrl}/ajax/user/${id}/profile/novels`, { + headers: { + referer: url, + }, + searchParams, + })) as SFWNovelsResponse; + + const items = await Promise.all( + Object.values(data.body.works).map(async (item) => { + const baseItem = { + title: item.title, + description: ` + <img src=${pixivUtils.getProxiedImageUrl(item.url)} /> + <div> + <p>${item.description}</p> + </div> + `, + link: `${baseUrl}/novel/show.php?id=${item.id}`, + author: item.userName, + pubDate: parseDate(item.createDate), + updated: parseDate(item.updateDate), + category: item.tags, + }; + + if (!fullContent) { + return baseItem; + } + + const { content } = await getSFWNovelContent(item.id); + + return { + ...baseItem, + description: `${baseItem.description}<hr>${content}`, + }; + }) + ); + + return { + title: data.body.extraData.meta.title, + description: data.body.extraData.meta.ogp.description, + image: pixivUtils.getProxiedImageUrl(Object.values(data.body.works)[0].profileImageUrl), + link: url, + item: items, + }; +} diff --git a/lib/routes/pixiv/novel-api/user-novels/types.ts b/lib/routes/pixiv/novel-api/user-novels/types.ts new file mode 100644 index 00000000000000..1ca3da4f81f8ba --- /dev/null +++ b/lib/routes/pixiv/novel-api/user-novels/types.ts @@ -0,0 +1,133 @@ +export interface SFWNovelsResponse { + data: { + error: boolean; + message: string; + body: { + works: Record<string, SFWNovelWork>; + extraData: { + meta: { + title: string; + description: string; + canonical: string; + ogp: { + description: string; + image: string; + title: string; + type: string; + }; + twitter: { + description: string; + image: string; + title: string; + card: string; + }; + alternateLanguages: { + ja: string; + en: string; + }; + descriptionHeader: string; + }; + }; + }; + }; +} + +export interface SFWNovelWork { + id: string; + title: string; + genre: string; + xRestrict: number; + restrict: number; + url: string; + tags: string[]; + userId: string; + userName: string; + profileImageUrl: string; + textCount: number; + wordCount: number; + readingTime: number; + useWordCount: boolean; + description: string; + isBookmarkable: boolean; + bookmarkData: null; + bookmarkCount: number; + isOriginal: boolean; + marker: null; + titleCaptionTranslation: { + workTitle: null; + workCaption: null; + }; + createDate: string; + updateDate: string; + isMasked: boolean; + aiType: number; + seriesId: string; + seriesTitle: string; + isUnlisted: boolean; +} + +export interface NSFWNovelsResponse { + data: { + user: { + id: number; + name: string; + account: string; + profile_image_urls: { + medium: string; + }; + is_followed: boolean; + is_access_blocking_user: boolean; + }; + novels: NSFWNovelWork[]; + }; +} + +export interface NSFWNovelWork { + id: string; + title: string; + caption: string; + restrict: number; + x_restrict: number; + image_urls: { + square_medium: string; + medium: string; + large: string; + }; + create_date: string; + tags: Array<{ + name: string; + translated_name: string | null; + added_by_uploaded_user: boolean; + }>; + text_length: number; + user: { + id: number; + name: string; + account: string; + profile_image_urls: { + medium: string; + }; + }; + series?: { + id?: number; + title?: string; + }; + total_bookmarks: number; + total_view: number; + total_comments: number; +} + +export interface NovelList { + title: string; + description: string; + image: string; + link: string; + item: Array<{ + title: string; + description: string; + author: string; + pubDate: Date; + link: string; + category: string[]; + }>; +} diff --git a/lib/routes/pixiv/novel-series.ts b/lib/routes/pixiv/novel-series.ts new file mode 100644 index 00000000000000..6db9b3928689ad --- /dev/null +++ b/lib/routes/pixiv/novel-series.ts @@ -0,0 +1,52 @@ +import { Data, Route } from '@/types'; +import { config } from '@/config'; +import { getNSFWSeriesNovels } from './novel-api/series/nsfw'; +import { getSFWSeriesNovels } from './novel-api/series/sfw'; + +export const route: Route = { + path: '/novel/series/:id', + categories: ['social-media'], + example: '/pixiv/novel/series/11586857', + parameters: { + id: 'Series id, can be found in URL', + }, + features: { + requireConfig: [ + { + name: 'PIXIV_REFRESHTOKEN', + optional: true, + description: ` +refresh_token after Pixiv login, required for accessing R18 novels +Pixiv 登錄後的 refresh_token,用於獲取 R18 小說 +[https://docs.rsshub.app/deploy/config#pixiv](https://docs.rsshub.app/deploy/config#pixiv)`, + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Novel Series', + maintainers: ['SnowAgar25', 'keocheung'], + handler, + radar: [ + { + source: ['www.pixiv.net/novel/series/:id'], + target: '/novel/series/:id', + }, + ], +}; + +const hasPixivAuth = () => Boolean(config.pixiv && config.pixiv.refreshToken); + +async function handler(ctx): Promise<Data> { + const id = ctx.req.param('id'); + const { limit } = ctx.req.query(); + + if (hasPixivAuth()) { + return await getNSFWSeriesNovels(id, limit); + } + + return await getSFWSeriesNovels(id, limit); +} diff --git a/lib/routes/pixiv/novels.ts b/lib/routes/pixiv/novels.ts index 9d9ccdf5b9d3a3..345f815d7283e1 100644 --- a/lib/routes/pixiv/novels.ts +++ b/lib/routes/pixiv/novels.ts @@ -1,15 +1,37 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import { parseDate } from '@/utils/parse-date'; -const baseUrl = 'https://www.pixiv.net'; +import { Data, Route, ViewType } from '@/types'; +import { fallback, queryToBoolean } from '@/utils/readable-social'; +import { config } from '@/config'; +import { getNSFWUserNovels } from './novel-api/user-novels/nsfw'; +import { getSFWUserNovels } from './novel-api/user-novels/sfw'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { - path: '/user/novels/:id', + path: '/user/novels/:id/:full_content?', categories: ['social-media'], + view: ViewType.Articles, example: '/pixiv/user/novels/27104704', - parameters: { id: "User id, available in user's homepage URL" }, + parameters: { + id: "User id, available in user's homepage URL", + full_content: { + description: 'Enable or disable the display of full content. ', + options: [ + { value: 'true', label: 'true' }, + { value: 'false', label: 'false' }, + ], + default: 'false', + }, + }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'PIXIV_REFRESHTOKEN', + optional: true, + description: ` +Pixiv 登錄後的 refresh_token,用於獲取 R18 小說 +refresh_token after Pixiv login, required for accessing R18 novels +[https://docs.rsshub.app/deploy/config#pixiv](https://docs.rsshub.app/deploy/config#pixiv)`, + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -18,54 +40,57 @@ export const route: Route = { }, radar: [ { - source: ['www.pixiv.net/users/:id/novels'], + title: 'User Novels (簡介 Basic info)', + source: ['www.pixiv.net/users/:id/novels', 'www.pixiv.net/users/:id'], + target: '/user/novels/:id', + }, + { + title: 'User Novels (全文 Full text)', + source: ['www.pixiv.net/users/:id/novels', 'www.pixiv.net/users/:id'], + target: '/user/novels/:id/true', }, ], name: 'User Novels', - maintainers: ['TonyRL'], + maintainers: ['TonyRL', 'SnowAgar25'], handler, + description: ` +| 小說類型 Novel Type | full_content | PIXIV_REFRESHTOKEN | 返回內容 Content | +|-------------------|--------------|-------------------|-----------------| +| Non R18 | false | 不需要 Not Required | 簡介 Basic info | +| Non R18 | true | 不需要 Not Required | 全文 Full text | +| R18 | false | 需要 Required | 簡介 Basic info | +| R18 | true | 需要 Required | 全文 Full text | + +Default value for \`full_content\` is \`false\` if not specified. + +Example: +- \`/pixiv/user/novels/79603797\` → 簡介 Basic info +- \`/pixiv/user/novels/79603797/true\` → 全文 Full text`, }; -async function handler(ctx) { +const hasPixivAuth = () => Boolean(config.pixiv && config.pixiv.refreshToken); + +async function handler(ctx): Promise<Data> { const id = ctx.req.param('id'); - const { limit = 100 } = ctx.req.query(); - const url = `${baseUrl}/users/${id}/novels`; - const { data: allData } = await got(`${baseUrl}/ajax/user/${id}/profile/all`, { - headers: { - referer: url, - }, - }); + const fullContent = fallback(undefined, queryToBoolean(ctx.req.param('full_content')), false); + const { limit } = ctx.req.query(); - const novels = Object.keys(allData.body.novels) - .sort((a, b) => b - a) - .slice(0, Number.parseInt(limit, 10)); - const searchParams = new URLSearchParams(); - for (const novel of novels) { - searchParams.append('ids[]', novel); + if (hasPixivAuth()) { + return await getNSFWUserNovels(id, fullContent, limit); } - const { data } = await got(`${baseUrl}/ajax/user/${id}/profile/novels`, { - headers: { - referer: url, - }, - searchParams, + const nonR18Result = await getSFWUserNovels(id, fullContent, limit).catch((error) => { + if (error.name === 'Error') { + return null; + } + throw error; }); - const items = Object.values(data.body.works).map((item) => ({ - title: item.seriesTitle || item.title, - description: item.description || item.title, - link: `${baseUrl}/novel/series/${item.id}`, - author: item.userName, - pubDate: parseDate(item.createDate), - updated: parseDate(item.updateDate), - category: item.tags, - })); + if (nonR18Result) { + return nonR18Result; + } - return { - title: data.body.extraData.meta.title, - description: data.body.extraData.meta.ogp.description, - image: Object.values(data.body.works)[0].profileImageUrl, - link: url, - item: items, - }; + throw new ConfigNotFoundError( + 'This user may not have any novel works, or is an R18 creator, PIXIV_REFRESHTOKEN is required.\npixiv RSS is disabled due to the lack of relevant config.\n該用戶可能沒有小說作品,或者該用戶爲 R18 創作者,需要 PIXIV_REFRESHTOKEN。' + ); } diff --git a/lib/routes/pixiv/ranking.ts b/lib/routes/pixiv/ranking.ts index a1fd17eb8242ba..bf1f6dffa2d14f 100644 --- a/lib/routes/pixiv/ranking.ts +++ b/lib/routes/pixiv/ranking.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import { getToken } from './token'; import getRanking from './api/get-ranking'; @@ -60,9 +60,74 @@ const alias = { export const route: Route = { path: '/ranking/:mode/:date?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.Pictures, example: '/pixiv/ranking/week', - parameters: { mode: 'rank type', date: 'format: `2018-4-25`' }, + parameters: { + mode: { + description: 'rank type', + options: [ + { + value: 'day', + label: 'daily rank', + }, + { + value: 'week', + label: 'weekly rank', + }, + { + value: 'month', + label: 'monthly rank', + }, + { + value: 'day_male', + label: 'male rank', + }, + { + value: 'day_felame', + label: 'female rank', + }, + { + value: 'day_ai', + label: 'AI-generated work Rankings', + }, + { + value: 'week_original', + label: 'original rank', + }, + { + value: 'week_rookie', + label: 'rookie user rank', + }, + { + value: 'day_r18', + label: 'R-18 daily rank', + }, + { + value: 'day_r18_ai', + label: 'R-18 AI-generated work', + }, + { + value: 'day_male_r18', + label: 'R-18 male rank', + }, + { + value: 'day_female_r18', + label: 'R-18 female rank', + }, + { + value: 'week_r18', + label: 'R-18 weekly rank', + }, + { + value: 'week_r18g', + label: 'R-18G rank', + }, + ], + default: 'day', + }, + date: 'format: `2018-4-25`', + }, features: { requireConfig: false, requirePuppeteer: false, @@ -74,13 +139,6 @@ export const route: Route = { name: 'Rankings', maintainers: ['EYHN'], handler, - description: `| daily rank | weekly rank | monthly rank | male rank | female rank | AI-generated work Rankings | original rank | rookie user rank | - | ---------- | ----------- | ------------ | --------- | ----------- | -------------------------- | -------------- | ---------------- | - | day | week | month | day\_male | day\_female | day\_ai | week\_original | week\_rookie | - - | R-18 daily rank | R-18 AI-generated work | R-18 male rank | R-18 female rank | R-18 weekly rank | R-18G rank | - | --------------- | ---------------------- | -------------- | ---------------- | ---------------- | ---------- | - | day\_r18 | day\_r18\_ai | day\_male\_r18 | day\_female\_r18 | week\_r18 | week\_r18g |`, }; async function handler(ctx) { @@ -111,9 +169,15 @@ async function handler(ctx) { return { title: `#${index + 1} ${illust.title}`, pubDate: parseDate(illust.create_date), - description: `<p>画师:${illust.user.name} - 阅览数:${illust.total_view} - 收藏数:${illust.total_bookmarks}</p><br>${images.join('')}`, + description: `${illust.caption}<br><p>画师:${illust.user.name} - 阅览数:${illust.total_view} - 收藏数:${illust.total_bookmarks}</p><br>${images.join('')}`, link: `https://www.pixiv.net/artworks/${illust.id}`, - author: illust.user.name, + author: [ + { + name: illust.user.name, + url: `https://www.pixiv.net/users/${illust.user.id}`, + avatar: illust.user.profile_image_urls.medium, + }, + ], category: illust.tags.map((tag) => tag.name), }; }), diff --git a/lib/routes/pixiv/search.ts b/lib/routes/pixiv/search.ts index e4eb0f76dff914..f0ea18a99b81fa 100644 --- a/lib/routes/pixiv/search.ts +++ b/lib/routes/pixiv/search.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import { getToken } from './token'; import searchPopularIllust from './api/search-popular-illust'; @@ -9,10 +9,59 @@ import { parseDate } from '@/utils/parse-date'; import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { - path: '/search/:keyword/:order?/:mode?', - categories: ['social-media'], - example: '/pixiv/search/Nezuko/popular/2', - parameters: { keyword: 'keyword', order: 'rank mode, empty or other for time order, popular for popular order', mode: 'filte R18 content' }, + path: '/search/:keyword/:order?/:mode?/:include_ai?', + categories: ['social-media', 'popular'], + view: ViewType.Pictures, + example: '/pixiv/search/Nezuko/popular', + parameters: { + keyword: 'keyword', + order: { + description: 'rank mode, empty or other for time order, popular for popular order', + default: 'date', + options: [ + { + label: 'time order', + value: 'date', + }, + { + label: 'popular order', + value: 'popular', + }, + ], + }, + mode: { + description: 'filte R18 content', + default: 'no', + options: [ + { + label: 'only not R18', + value: 'safe', + }, + { + label: 'only R18', + value: 'r18', + }, + { + label: 'no filter', + value: 'no', + }, + ], + }, + include_ai: { + description: 'whether AI-generated content is included', + default: 'yes', + options: [ + { + label: 'does not include AI-generated content', + value: 'no', + }, + { + label: 'include AI-generated content', + value: 'yes', + }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -24,9 +73,6 @@ export const route: Route = { name: 'Keyword', maintainers: ['DIYgod'], handler, - description: `| only not R18 | only R18 | no filter | - | ------------ | -------- | -------------- | - | safe | r18 | empty or other |`, }; async function handler(ctx) { @@ -37,6 +83,7 @@ async function handler(ctx) { const keyword = ctx.req.param('keyword'); const order = ctx.req.param('order') || 'date'; const mode = ctx.req.param('mode'); + const includeAI = ctx.req.param('include_ai'); const token = await getToken(cache.tryGet); if (!token) { @@ -52,6 +99,10 @@ async function handler(ctx) { illusts = illusts.filter((item) => item.x_restrict === 1); } + if (includeAI === 'no' || includeAI === '0') { + illusts = illusts.filter((item) => item.illust_ai_type <= 1); + } + return { title: `${keyword} 的 pixiv ${order === 'popular' ? '热门' : ''}内容`, link: `https://www.pixiv.net/tags/${keyword}/artworks`, @@ -61,7 +112,7 @@ async function handler(ctx) { title: illust.title, author: illust.user.name, pubDate: parseDate(illust.create_date), - description: `<p>画师:${illust.user.name} - 阅览数:${illust.total_view} - 收藏数:${illust.total_bookmarks}</p>${images.join('')}`, + description: `${illust.caption}<br><p>画师:${illust.user.name} - 阅览数:${illust.total_view} - 收藏数:${illust.total_bookmarks}</p>${images.join('')}`, link: `https://www.pixiv.net/artworks/${illust.id}`, }; }), diff --git a/lib/routes/pixiv/user.ts b/lib/routes/pixiv/user.ts index 08243ad2e38c33..ae027f7ff7b5b4 100644 --- a/lib/routes/pixiv/user.ts +++ b/lib/routes/pixiv/user.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import { getToken } from './token'; import getIllusts from './api/get-illusts'; @@ -9,7 +9,8 @@ import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { path: '/user/:id', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.Pictures, example: '/pixiv/user/15288095', parameters: { id: "user id, available in user's homepage URL" }, features: { @@ -49,6 +50,7 @@ async function handler(ctx) { return { title: `${username} 的 pixiv 动态`, link: `https://www.pixiv.net/users/${id}`, + image: pixivUtils.getProxiedImageUrl(illusts[0].user.profile_image_urls.medium), description: `${username} 的 pixiv 最新动态`, item: illusts.map((illust) => { const images = pixivUtils.getImgs(illust); diff --git a/lib/routes/pixiv/utils.ts b/lib/routes/pixiv/utils.ts index 64b0ba0c5c533d..5d3e71b968b28c 100644 --- a/lib/routes/pixiv/utils.ts +++ b/lib/routes/pixiv/utils.ts @@ -2,16 +2,19 @@ import { config } from '@/config'; export default { getImgs(illust) { - const images = []; + const images: string[] = []; if (illust.meta_pages?.length) { for (const page of illust.meta_pages) { const original = page.image_urls.original.replace('https://i.pximg.net', config.pixiv.imgProxy); - images.push(`<p><img src="${original}"/></p>`); + images.push(`<p><img src="${original}" width="${page.width}" height="${page.height}" /></p>`); } } else if (illust.meta_single_page.original_image_url) { const original = illust.meta_single_page.original_image_url.replace('https://i.pximg.net', config.pixiv.imgProxy); - images.push(`<p><img src="${original}"/></p>`); + images.push(`<p><img src="${original}" width="${illust.width}" height="${illust.height}" /></p>`); } return images; }, + getProxiedImageUrl(originalUrl: string): string { + return originalUrl.replace('https://i.pximg.net', config.pixiv.imgProxy || ''); + }, }; diff --git a/lib/routes/pixivision/index.ts b/lib/routes/pixivision/index.ts new file mode 100644 index 00000000000000..1593ca5811bdbd --- /dev/null +++ b/lib/routes/pixivision/index.ts @@ -0,0 +1,85 @@ +import { Route, DataItem, Data, ViewType } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { processContent } from './utils'; + +export const route: Route = { + path: '/:lang/:category?', + categories: ['anime', 'popular'], + view: ViewType.Articles, + example: '/pixivision/zh-tw', + parameters: { lang: 'Language', category: 'Category' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Category', + maintainers: ['SnowAgar25'], + description: `::: tip + \`https://www.pixivision.net/zh-tw/c/interview\` → \`/pixivision/zh-tw/interview\` +:::`, + radar: [ + { + source: ['www.pixivision.net/:lang'], + target: '/:lang', + }, + { + source: ['www.pixivision.net/:lang/c/:category'], + target: '/:lang/:category', + }, + ], + handler, +}; + +async function handler(ctx): Promise<Data> { + const { lang, category } = ctx.req.param(); + const baseUrl = 'https://www.pixivision.net'; + const url = category ? `${baseUrl}/${lang}/c/${category}` : `${baseUrl}/${lang}`; + + const headers = { + headers: { + Cookie: `user_lang=${lang.replace('-', '_')}`, // zh-tw → zh_tw + }, + }; + + const { data: response } = await got(url, headers); + const $ = load(response); + + const list = $('li.article-card-container a[data-gtm-action="ClickTitle"]') + .toArray() + .map((elem) => ({ + title: $(elem).text(), + link: new URL($(elem).attr('href') ?? '', baseUrl).href, + })); + + const items = await Promise.all( + list.map(async (item) => { + const result = await cache.tryGet(item.link, async () => { + const { data: articleData } = await got(item.link, headers); + const $article = load(articleData); + + const processedDescription = processContent($article, lang); + + return { + title: item.title, + description: processedDescription, + link: item.link, + pubDate: parseDate($article('time').attr('datetime') ?? ''), + } as DataItem; + }); + return result; + }) + ); + + return { + title: `${$('.ssc__header').length ? $('.ssc__header').text() : 'New'} - pixivision`, + link: url, + item: items.filter((item): item is DataItem => !!item), + }; +} diff --git a/lib/routes/pixivision/namespace.ts b/lib/routes/pixivision/namespace.ts new file mode 100644 index 00000000000000..208a8b96099103 --- /dev/null +++ b/lib/routes/pixivision/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'pixivision', + url: 'www.pixivision.net', + lang: 'ja', +}; diff --git a/lib/routes/pixivision/utils.ts b/lib/routes/pixivision/utils.ts new file mode 100644 index 00000000000000..4d8c8e08896b87 --- /dev/null +++ b/lib/routes/pixivision/utils.ts @@ -0,0 +1,86 @@ +import { CheerioAPI } from 'cheerio'; +import { config } from '@/config'; + +const multiImagePrompt = { + en: (count) => `${count} images in total`, + zh: (count) => `共${count}张图`, + 'zh-tw': (count) => `共${count}張圖`, + ko: (count) => `총 ${count}개의 이미지`, + ja: (count) => `計${count}枚の画像`, +}; + +export function processContent($: CheerioAPI, lang: string): string { + // 移除作者頭像 + $('.am__work__user-icon-container').remove(); + + // 插畫標題&作者 + $('.am__work__title').attr('style', 'display: inline;'); + $('.am__work__user-name').attr('style', 'display: inline; margin-left: 10px;'); + + // 處理多張圖片的提示 + $('.mic__label').each((_, elem) => { + const $label = $(elem); + const count = $label.text(); + const $workContainer = $label.parentsUntil('.am__work').last().parent(); + const $titleContainer = $workContainer.find('.am__work__title-container'); + + $titleContainer.append(`<p style="float: right; margin: 0;">${multiImagePrompt[lang](count)}</p>`); + $label.remove(); + }); + + // 插畫間隔 + $('.article-item, ._feature-article-body__pixiv_illust').after('<br>'); + + // Remove Label & Tags + $('.arc__thumbnail-label').remove(); + $('.arc__footer-container').remove(); + + // pixivision card + $('article._article-card').each((_, article) => { + const $article = $(article); + + const $thumbnail = $article.find('._thumbnail'); + const thumbnailStyle = $thumbnail.attr('style'); + const bgImageMatch = thumbnailStyle?.match(/url\((.*?)\)/); + const imageUrl = bgImageMatch ? bgImageMatch[1] : ''; + + $thumbnail.remove(); + + if (imageUrl) { + $article.prepend(`<img src="${imageUrl}" alt="Article thumbnail">`); + } + }); + + // 處理 tweet + $('.fab__script').each((_, elem) => { + const $elem = $(elem); + const $link = $elem.find('blockquote > a'); + const href = $link.attr('href'); + + if (href) { + const match = href.match(/\/status\/(\d+)/); + if (match) { + const tweetId = match[1]; + $elem.html(` + <iframe + scrolling="no" + frameborder="0" + allowtransparency="true" + allowfullscreen="true" + class="" + style="position: static; visibility: visible; display: block; width: 550px; height: 1000px; flex-grow: 1;" + title="X Post" + src="https://platform.twitter.com/embed/Tweet.html?id=${tweetId}" + ></iframe> + `); + $elem.find('blockquote').remove(); + } + } + }); + + return ( + $('.am__body') + .html() + ?.replace(/https:\/\/i\.pximg\.net/g, config.pixiv.imgProxy || '') || '' + ); +} diff --git a/lib/routes/piyao/namespace.ts b/lib/routes/piyao/namespace.ts index 1a308490f16bd5..3fe2955b4cae06 100644 --- a/lib/routes/piyao/namespace.ts +++ b/lib/routes/piyao/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国互联网联合辟谣平台', url: 'piyao.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/pkmer/namespace.ts b/lib/routes/pkmer/namespace.ts index 5c46a75fa75d69..a6c5a53851cc18 100644 --- a/lib/routes/pkmer/namespace.ts +++ b/lib/routes/pkmer/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PKMer', url: 'pkmer.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/pku/bbs/hot.ts b/lib/routes/pku/bbs/hot.ts index f5bdc41faa46ed..e85f7bbfdb6253 100644 --- a/lib/routes/pku/bbs/hot.ts +++ b/lib/routes/pku/bbs/hot.ts @@ -28,9 +28,9 @@ export const route: Route = { maintainers: ['wooddance'], handler, url: 'bbs.pku.edu.cn/v2/hot-topic.php', - description: `:::warning + description: `::: warning 论坛部分帖子正文内容的获取需要用户登录后的 Cookie 值,详情见部署页面的配置模块。 - :::`, +:::`, }; async function handler() { @@ -44,7 +44,7 @@ async function handler() { const listItems = $('#list-content .list-item') .map(function () { return { - url: new URL($(this).find('> a.link').attr('href'), r.url).href, + url: new URL($(this).find('> a.link').attr('href'), 'https://bbs.pku.edu.cn/v2/').href, title: $(this).find('.title').text(), }; }) diff --git a/lib/routes/pku/hr.ts b/lib/routes/pku/hr.ts index 99d904d055e774..77700a0706c505 100644 --- a/lib/routes/pku/hr.ts +++ b/lib/routes/pku/hr.ts @@ -26,11 +26,11 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'hr.pku.edu.cn/', - description: `:::tip + description: `::: tip 分类字段处填写的是对应北京大学人事处分类页网址中介于 **\`http://hr.pku.edu.cn/\`** 和 **/index.htm** 中间的一段,并将其中的 \`/\` 修改为 \`-\`。 如 [北京大学人事处 - 人才招聘 - 教师 - 教学科研人员](https://hr.pku.edu.cn/rczp/js/jxkyry/index.htm) 的网址为 \`https://hr.pku.edu.cn/rczp/js/jxkyry/index.htm\` 其中介于 **\`http://hr.pku.edu.cn/\`** 和 **\`/index.ht\`** 中间的一段为 \`rczp/js/jxkyry\`。随后,并将其中的 \`/\` 修改为 \`-\`,可以得到 \`rczp-js-jxkyry\`。所以最终我们的路由为 [\`/pku/hr/rczp-js-jxkyry\`](https://rsshub.app/pku/hr/rczp-js-jxkyry) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/pku/namespace.ts b/lib/routes/pku/namespace.ts index f312715891f0fc..2dd941a01f565e 100644 --- a/lib/routes/pku/namespace.ts +++ b/lib/routes/pku/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京大学', url: 'admission.pku.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/pku/scc/recruit.ts b/lib/routes/pku/scc/recruit.ts index 9248466e7fdd2f..94050606740c0a 100644 --- a/lib/routes/pku/scc/recruit.ts +++ b/lib/routes/pku/scc/recruit.ts @@ -30,8 +30,8 @@ export const route: Route = { maintainers: ['DylanXie123'], handler, description: `| xwrd | tzgg | zpxx | sxxx | cyxx | - | -------- | -------- | -------- | -------- | -------- | - | 新闻热点 | 通知公告 | 招聘信息 | 实习信息 | 创业信息 |`, +| -------- | -------- | -------- | -------- | -------- | +| 新闻热点 | 通知公告 | 招聘信息 | 实习信息 | 创业信息 |`, }; async function handler(ctx) { diff --git a/lib/routes/playno1/av.ts b/lib/routes/playno1/av.ts index 7b8f7384ab7b7d..2aa82c5b9ea5c5 100644 --- a/lib/routes/playno1/av.ts +++ b/lib/routes/playno1/av.ts @@ -23,13 +23,13 @@ export const route: Route = { name: 'AV', maintainers: ['TonyRL'], handler, - description: `:::warning + description: `::: warning 目前观测到该博客可能禁止日本 IP 访问。建议部署在日本区以外的服务器上。 ::: - | 全部文章 | AV 新聞 | AV 導覽 | - | -------- | ------- | ------- | - | 78 | 3 | 5 |`, +| 全部文章 | AV 新聞 | AV 導覽 | +| -------- | ------- | ------- | +| 78 | 3 | 5 |`, }; async function handler(ctx) { diff --git a/lib/routes/playno1/namespace.ts b/lib/routes/playno1/namespace.ts index f7d64bf8ddde4c..881561b909fdd1 100644 --- a/lib/routes/playno1/namespace.ts +++ b/lib/routes/playno1/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PLAYNO.1 玩樂達人', url: 'stno1.playno1.com', + lang: 'zh-TW', }; diff --git a/lib/routes/playno1/st.ts b/lib/routes/playno1/st.ts index 5261557aafdbfe..bda62fa98cd322 100644 --- a/lib/routes/playno1/st.ts +++ b/lib/routes/playno1/st.ts @@ -30,8 +30,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| 全部文章 | 情趣體驗報告 | 情趣新聞 | 情趣研究所 | - | -------- | ------------ | -------- | ---------- | - | all | experience | news | graduate |`, +| -------- | ------------ | -------- | ---------- | +| all | experience | news | graduate |`, }; async function handler(ctx) { diff --git a/lib/routes/playpcesor/namespace.ts b/lib/routes/playpcesor/namespace.ts index ec98208b9ac318..b2ee6fa2d7d4e8 100644 --- a/lib/routes/playpcesor/namespace.ts +++ b/lib/routes/playpcesor/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '电脑玩物', url: 'playpcesor.com', + lang: 'zh-CN', }; diff --git a/lib/routes/plurk/anonymous.ts b/lib/routes/plurk/anonymous.ts index 453e768671b73e..25f2cb72d682f1 100644 --- a/lib/routes/plurk/anonymous.ts +++ b/lib/routes/plurk/anonymous.ts @@ -5,7 +5,7 @@ import { baseUrl, getPlurk } from './utils'; export const route: Route = { path: '/anonymous', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/plurk/anonymous', parameters: {}, features: { diff --git a/lib/routes/plurk/hotlinks.ts b/lib/routes/plurk/hotlinks.ts index 503ef59a6adff4..2c45a216de8a24 100644 --- a/lib/routes/plurk/hotlinks.ts +++ b/lib/routes/plurk/hotlinks.ts @@ -5,7 +5,7 @@ import { baseUrl, getPlurk } from './utils'; export const route: Route = { path: '/hotlinks', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/plurk/hotlinks', parameters: {}, features: { diff --git a/lib/routes/plurk/namespace.ts b/lib/routes/plurk/namespace.ts index 85196dbde410c7..b322ad34bf2e82 100644 --- a/lib/routes/plurk/namespace.ts +++ b/lib/routes/plurk/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Plurk', url: 'plurk.com', + lang: 'en', }; diff --git a/lib/routes/plurk/news.ts b/lib/routes/plurk/news.ts index 29a9a1c1dda878..831c9afb972f4e 100644 --- a/lib/routes/plurk/news.ts +++ b/lib/routes/plurk/news.ts @@ -5,7 +5,7 @@ import { baseUrl, fetchFriends, getPlurk } from './utils'; export const route: Route = { path: '/news/:lang?', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/plurk/news/:lang?', parameters: { lang: 'Language, see the table above, `en` by default' }, features: { diff --git a/lib/routes/plurk/search.ts b/lib/routes/plurk/search.ts index 58764cb4048f60..ae6ad44a1cdfa5 100644 --- a/lib/routes/plurk/search.ts +++ b/lib/routes/plurk/search.ts @@ -6,7 +6,7 @@ import { baseUrl, getPlurk } from './utils'; export const route: Route = { path: '/search/:keyword', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/plurk/search/FGO', parameters: { keyword: 'Search keyword' }, features: { diff --git a/lib/routes/plurk/top.ts b/lib/routes/plurk/top.ts index 1ce1924a1774d1..710ee464668def 100644 --- a/lib/routes/plurk/top.ts +++ b/lib/routes/plurk/top.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 { baseUrl, getPlurk } from './utils'; @@ -8,7 +8,8 @@ const categoryList = new Set(['topReplurks', 'topFavorites', 'topResponded']); export const route: Route = { path: '/top/:category?/:lang?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/plurk/top/topReplurks', parameters: { category: 'Category, see the table below, `topReplurks` by default', lang: 'Language, see the table below, `en` by default' }, features: { @@ -23,12 +24,12 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| Top Replurks | Top Favorites | Top Responded | - | ------------ | ------------- | ------------- | - | topReplurks | topFavorites | topResponded | +| ------------ | ------------- | ------------- | +| topReplurks | topFavorites | topResponded | - | English | 中文(繁體) | - | ------- | ------------ | - | en | zh |`, +| English | 中文(繁體) | +| ------- | ------------ | +| en | zh |`, }; async function handler(ctx) { diff --git a/lib/routes/plurk/topic.ts b/lib/routes/plurk/topic.ts index 7f4f6e8814bfd1..08240ffed2d7ea 100644 --- a/lib/routes/plurk/topic.ts +++ b/lib/routes/plurk/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 { load } from 'cheerio'; @@ -6,7 +6,8 @@ import { baseUrl, fetchFriends, getPlurk } from './utils'; export const route: Route = { path: '/topic/:topic', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/plurk/topic/standwithukraine', parameters: { topic: 'Topic ID, can be found in URL' }, features: { diff --git a/lib/routes/plurk/user.ts b/lib/routes/plurk/user.ts index ac5f24f0297fc6..b23bec103500e3 100644 --- a/lib/routes/plurk/user.ts +++ b/lib/routes/plurk/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,7 +6,8 @@ import { baseUrl, fetchFriends, getPlurk } from './utils'; export const route: Route = { path: '/user/:user', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/plurk/user/plurkoffice', parameters: { user: 'User ID, can be found in URL' }, features: { diff --git a/lib/routes/pnas/index.ts b/lib/routes/pnas/index.ts index 735b01ad80005a..f66160ae02b79b 100644 --- a/lib/routes/pnas/index.ts +++ b/lib/routes/pnas/index.ts @@ -108,7 +108,7 @@ async function handler(ctx) { ) ); - browser.close(); + await browser.close(); return { title: `${$('.banner-widget__content h1').text()} - PNAS`, diff --git a/lib/routes/pnas/namespace.ts b/lib/routes/pnas/namespace.ts index 350e1d68aa573a..2478bc863d094d 100644 --- a/lib/routes/pnas/namespace.ts +++ b/lib/routes/pnas/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Proceedings of The National Academy of Sciences', url: 'pnas.org', + lang: 'en', }; diff --git a/lib/routes/podwise/collections.ts b/lib/routes/podwise/collections.ts new file mode 100644 index 00000000000000..77de1718e5caa9 --- /dev/null +++ b/lib/routes/podwise/collections.ts @@ -0,0 +1,47 @@ +import { Route, ViewType } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 + +export const route: Route = { + path: '/explore', + categories: ['multimedia', 'popular'], + view: ViewType.Audios, + example: '/podwise/explore', + radar: [ + { + source: ['podwise.ai', 'podwise.ai/explore'], + }, + ], + name: 'Collections', + maintainers: ['lyling'], + handler: async () => { + const link = 'https://podwise.ai/explore'; + const response = await ofetch(link); + const $ = load(response); + const content = $('#navigator').next(); + // header/[div => content]/footer, content p(2) + const collectinDescription = content.find('p').eq(1).text(); + + const items = content + .find('.group') + .toArray() + .map((item) => { + item = $(item); + const title = item.find('a').first().text(); + const link = item.find('a').first().attr('href'); + const description = item.find('p').first().text(); + return { + title, + link: `https://podwise.ai${link}`, + description, + }; + }); + + return { + title: $('title').text(), + description: collectinDescription, + item: items, + link, + }; + }, +}; diff --git a/lib/routes/podwise/episodes.ts b/lib/routes/podwise/episodes.ts new file mode 100644 index 00000000000000..6066427cd940e0 --- /dev/null +++ b/lib/routes/podwise/episodes.ts @@ -0,0 +1,119 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +dayjs.extend(duration); + +export const route: Route = { + path: '/explore/:type', + categories: ['multimedia'], + example: '/podwise/explore/latest', + parameters: { type: 'latest or all episodes.' }, + radar: [ + { + source: ['podwise.ai/explore/:type'], + }, + ], + name: 'Episodes', + maintainers: ['lyling'], + handler: async (ctx) => { + const type = ctx.req.param('type'); + const link = `https://podwise.ai/explore/${type}`; + const response = await ofetch(link); + const $ = load(response); + const content = $('#navigator').next(); + // header/[div => content]/footer, content>div(2)>h1 + const title = content.find('h1').first().text(); + const description = content.find('p').eq(1).text(); + + const list = content + .find('.group') + .toArray() + .map((item) => { + item = $(item); + const link = item.find('a').first().attr('href'); + const description = item.find('p').first().text(); + const pubDate = item.find('a').next().children('span').text(); + + return { + link: `https://podwise.ai${link}`, + description, + pubDate: timezone(parseDate(pubDate, 'DD MMM YYYY', 'en'), 8), + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + + item.description = $('summary').first().html(); + + // duration + const $cover = $('img[alt="Podcast cover"]').eq(1); + const $duration = $cover.next().find('span').eq(2); + + const nextData = JSON.parse( + $('script:contains("podName")') + .first() + .text() + .match(/self\.__next_f\.push\((.+)\)/)?.[1] ?? '' + ); + const podcastData = JSON.parse(nextData[1].slice(2))[1][3].children[3].episode; + + // rss feed + item.title = podcastData.title; + item.author = podcastData.podName; + + // podcast feed + item.itunes_item_image = podcastData.cover; + item.itunes_duration = parseDuration($duration.text()); + item.enclosure_url = podcastData.link; + // item.enclosure_length: nothing can convert to. + item.enclosure_type = toEnclosureType(podcastData.linkType); + + return item; + }) + ) + ); + + return { + title, + description, + link, + item: items, + }; + }, +}; + +function parseDuration(durationStr) { + const matches = durationStr.match(/(\d+h)?(\d+m)?/); + const hours = matches[1] ? Number.parseInt(matches[1]) : 0; + const minutes = matches[2] ? Number.parseInt(matches[2]) : 0; + + // 使用 dayjs 的 duration 创建持续时间对象 + return dayjs + .duration({ + hours, + minutes, + }) + .asSeconds(); +} + +function toEnclosureType(linkType: string): string { + switch (linkType) { + case 'mp3': + return 'audio/mpeg'; + case 'm4a': + return 'audio/x-m4a'; + case 'mp4': + return 'video/mp4'; + default: + return linkType; + } +} diff --git a/lib/routes/podwise/namespace.ts b/lib/routes/podwise/namespace.ts new file mode 100644 index 00000000000000..50d1eccf829ae0 --- /dev/null +++ b/lib/routes/podwise/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Podwise', + url: 'podwise.ai', + zh: { + name: 'Podwise', + }, + lang: 'en', +}; diff --git a/lib/routes/pornhub/category-url.ts b/lib/routes/pornhub/category-url.ts index 5a2af089439e8a..0e50634b20f885 100644 --- a/lib/routes/pornhub/category-url.ts +++ b/lib/routes/pornhub/category-url.ts @@ -6,7 +6,7 @@ import { headers, parseItems } from './utils'; import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { - path: '/:language?/category_url/:url?', + path: '/category_url/:url?/:language?', categories: ['multimedia'], example: '/pornhub/category_url/video%3Fc%3D15%26o%3Dmv%26t%3Dw%26cc%3Djp', parameters: { language: 'language, see below', url: 'relative path after `pornhub.com/`, need to be URL encoded' }, diff --git a/lib/routes/pornhub/category.ts b/lib/routes/pornhub/category.ts index dba121779686c8..c3f8fe392a797a 100644 --- a/lib/routes/pornhub/category.ts +++ b/lib/routes/pornhub/category.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 { parseDate } from '@/utils/parse-date'; @@ -7,7 +7,8 @@ import { config } from '@/config'; export const route: Route = { path: '/category/:caty', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Videos, example: '/pornhub/category/popular-with-women', parameters: { caty: 'category, see [categories](https://www.pornhub.com/webmasters/categories)' }, features: { diff --git a/lib/routes/pornhub/model.ts b/lib/routes/pornhub/model.ts index da5f44b01deda0..32296bc855b793 100644 --- a/lib/routes/pornhub/model.ts +++ b/lib/routes/pornhub/model.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType, Data } from '@/types'; import got from '@/utils/got'; import { load } from 'cheerio'; import { isValidHost } from '@/utils/valid-host'; @@ -6,8 +6,9 @@ import { headers, parseItems } from './utils'; import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { - path: '/:language?/model/:username/:sort?', - categories: ['multimedia'], + path: '/model/:username/:language?/:sort?', + categories: ['multimedia', 'popular'], + view: ViewType.Videos, example: '/pornhub/model/stacy-starando', parameters: { language: 'language, see below', username: 'username, part of the url e.g. `pornhub.com/model/stacy-starando`', sort: 'sorting method, see below' }, features: { @@ -24,12 +25,12 @@ export const route: Route = { target: '/model/:username', }, ], - name: 'Verified amateur / Model', + name: 'Model', maintainers: ['I2IMk', 'queensferryme'], handler, }; -async function handler(ctx) { +async function handler(ctx): Promise<Data> { const { language = 'www', username, sort = '' } = ctx.req.param(); const link = `https://${language}.pornhub.com/model/${username}/videos${sort ? `?o=${sort}` : ''}`; if (!isValidHost(language)) { @@ -43,12 +44,10 @@ async function handler(ctx) { .map((e) => parseItems($(e))); return { - title: $('title').first().text(), + title: $('h1').first().text(), description: $('section.aboutMeSection').text().trim(), link, - image: $('#coverPictureDefault').attr('src'), - logo: $('#getAvatar').attr('src'), - icon: $('#getAvatar').attr('src'), + image: $('#getAvatar').attr('src'), language: $('html').attr('lang'), item: items, }; diff --git a/lib/routes/pornhub/namespace.ts b/lib/routes/pornhub/namespace.ts index a44ea7fb143e6e..bad0b8dc128dc3 100644 --- a/lib/routes/pornhub/namespace.ts +++ b/lib/routes/pornhub/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PornHub', url: 'pornhub.com', + lang: 'en', }; diff --git a/lib/routes/pornhub/pornstar.ts b/lib/routes/pornhub/pornstar.ts index 9815004bb2c3c1..ea8fa0853742ba 100644 --- a/lib/routes/pornhub/pornstar.ts +++ b/lib/routes/pornhub/pornstar.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType, Data } from '@/types'; import got from '@/utils/got'; import { load } from 'cheerio'; import { isValidHost } from '@/utils/valid-host'; @@ -6,10 +6,54 @@ import { headers, parseItems } from './utils'; import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { - path: '/:language?/pornstar/:username/:sort?', - categories: ['multimedia'], - example: '/pornhub/pornstar/june-liu', - parameters: { language: 'language, see below', username: 'username, part of the url e.g. `pornhub.com/pornstar/june-liu`', sort: 'sorting method, see below' }, + path: '/pornstar/:username/:language?/:sort?', + categories: ['multimedia', 'popular'], + view: ViewType.Videos, + example: '/pornhub/pornstar/june-liu/www/mr', + parameters: { + username: { + description: 'username, part of the url e.g. `pornhub.com/pornstar/june-liu`', + }, + language: { + description: 'language', + options: [ + { value: 'www', label: 'English' }, + { value: 'de', label: 'Deutsch' }, + { value: 'es', label: 'Español' }, + { value: 'fr', label: 'Français' }, + { value: 'it', label: 'Italiano' }, + { value: 'ja', label: '日本語' }, + { value: 'pt', label: 'Português' }, + { value: 'pl', label: 'Polski' }, + { value: 'rt', label: 'Русский' }, + { value: 'nl', label: 'Dutch' }, + { value: 'cs', label: 'Czech' }, + { value: 'cn', label: '中文(简体)' }, + ], + default: 'www', + }, + sort: { + description: 'sorting method, leave empty for `Best`', + options: [ + { + label: 'Most Recent', + value: 'mr', + }, + { + label: 'Most Viewed', + value: 'mv', + }, + { + label: 'Top Rated', + value: 'tr', + }, + { + label: 'Longest', + value: 'lg', + }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -24,36 +68,40 @@ export const route: Route = { target: '/pornstar/:username', }, ], - name: 'Verified model / Pornstar', + name: 'Pornstar', maintainers: ['I2IMk', 'queensferryme'], handler, - description: `**\`sort\`** - - | Most Recent | Most Viewed | Top Rated | Longest | Best | - | ----------- | ----------- | --------- | ------- | ---- | - | mr | mv | tr | lg | |`, }; -async function handler(ctx) { +async function handler(ctx): Promise<Data> { const { language = 'www', username, sort = 'mr' } = ctx.req.param(); - const link = `https://${language}.pornhub.com/pornstar/${username}/videos?o=${sort}`; + let link = `https://${language}.pornhub.com/pornstar/${username}?o=${sort}`; if (!isValidHost(language)) { throw new InvalidParameterError('Invalid language'); } const { data: response } = await got(link, { headers }); - const $ = load(response); - const items = $('#mostRecentVideosSection .videoBox') - .toArray() - .map((e) => parseItems($(e))); + let $ = load(response); + let items; + + if ($('.withBio').length === 0) { + link = `https://${language}.pornhub.com/pornstar/${username}/videos?o=${sort}`; + const { data: response } = await got(link, { headers }); + $ = load(response); + items = $('#mostRecentVideosSection .videoBox') + .toArray() + .map((e) => parseItems($(e))); + } else { + items = $('#pornstarsVideoSection .videoBox') + .toArray() + .map((e) => parseItems($(e))); + } return { - title: $('title').first().text(), + title: $('h1').first().text(), description: $('section.aboutMeSection').text().trim(), link, - image: $('#coverPictureDefault').attr('src'), - logo: $('#getAvatar').attr('src'), - icon: $('#getAvatar').attr('src'), + image: $('#getAvatar').attr('src'), language: $('html').attr('lang'), item: items, }; diff --git a/lib/routes/pornhub/search.ts b/lib/routes/pornhub/search.ts index 07dd7bb1b3c3fd..290e5010ec083f 100644 --- a/lib/routes/pornhub/search.ts +++ b/lib/routes/pornhub/search.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 { defaultDomain, renderDescription } from './utils'; export const route: Route = { path: '/search/:keyword', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Videos, example: '/pornhub/search/stepsister', parameters: { keyword: 'keyword' }, features: { diff --git a/lib/routes/pornhub/users.ts b/lib/routes/pornhub/users.ts index e3137b60199309..ade011d9a7d80c 100644 --- a/lib/routes/pornhub/users.ts +++ b/lib/routes/pornhub/users.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, Data } from '@/types'; import got from '@/utils/got'; import { load } from 'cheerio'; import { isValidHost } from '@/utils/valid-host'; @@ -6,7 +6,7 @@ import { headers, parseItems } from './utils'; import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { - path: '/:language?/users/:username', + path: '/users/:username/:language?', categories: ['multimedia'], example: '/pornhub/users/pornhubmodels', parameters: { language: 'language, see below', username: 'username, part of the url e.g. `pornhub.com/users/pornhubmodels`' }, @@ -29,7 +29,7 @@ export const route: Route = { handler, }; -async function handler(ctx) { +async function handler(ctx): Promise<Data> { const { language = 'www', username } = ctx.req.param(); const link = `https://${language}.pornhub.com/users/${username}/videos`; if (!isValidHost(language)) { @@ -43,12 +43,10 @@ async function handler(ctx) { .map((e) => parseItems($(e))); return { - title: $('title').first().text(), + title: $('.profileUserName a').text(), description: $('.aboutMeText').text().trim(), link, - image: $('#coverPictureDefault').attr('src'), - logo: $('#getAvatar').attr('src'), - icon: $('#getAvatar').attr('src'), + image: $('#getAvatar').attr('src'), language: $('html').attr('lang'), allowEmpty: true, item: items, diff --git a/lib/routes/pornhub/utils.ts b/lib/routes/pornhub/utils.ts index b3b8c68e4e85c8..e94989b2462aa0 100644 --- a/lib/routes/pornhub/utils.ts +++ b/lib/routes/pornhub/utils.ts @@ -27,7 +27,7 @@ const parseItems = (e) => ({ previewVideo: e.find('img').data('mediabook'), }), author: e.find('.usernameWrap a').text(), - pubDate: dayjs(extractDateFromImageUrl(e.find('img').data('mediumthumb'))) || parseRelativeDate(e.find('.added').text()), + pubDate: dayjs(extractDateFromImageUrl(e.find('img').data('mediumthumb'))).toDate() || parseRelativeDate(e.find('.added').text()), }); export { defaultDomain, headers, renderDescription, parseItems }; diff --git a/lib/routes/postman/namespace.ts b/lib/routes/postman/namespace.ts index c35f2c0cb6b57a..7ac98f75fd21cd 100644 --- a/lib/routes/postman/namespace.ts +++ b/lib/routes/postman/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Postman', url: 'postman.com', + lang: 'en', }; diff --git a/lib/routes/priconne-redive/namespace.ts b/lib/routes/priconne-redive/namespace.ts index 06e85aafe95a3f..c4c39ee6383d44 100644 --- a/lib/routes/priconne-redive/namespace.ts +++ b/lib/routes/priconne-redive/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PRINCESS CONNECT! Re Dive プリンセスコネクト!Re Dive', url: 'priconne-redive.jp', + lang: 'ja', }; diff --git a/lib/routes/priconne-redive/news.ts b/lib/routes/priconne-redive/news.ts index 278885b8231459..1c6b5777918379 100644 --- a/lib/routes/priconne-redive/news.ts +++ b/lib/routes/priconne-redive/news.ts @@ -37,9 +37,9 @@ export const route: Route = { url: 'priconne-redive.jp/news', description: `服务器 - | 国服 | 台服 | 日服 | - | ----- | ----- | ---- | - | zh-cn | zh-tw | jp |`, +| 国服 | 台服 | 日服 | +| ----- | ----- | ---- | +| zh-cn | zh-tw | jp |`, }; async function handler(ctx) { diff --git a/lib/routes/producthunt/namespace.ts b/lib/routes/producthunt/namespace.ts index 2132b871f11f83..29f5c1bfc97994 100644 --- a/lib/routes/producthunt/namespace.ts +++ b/lib/routes/producthunt/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: 'Product Hunt', url: 'www.producthunt.com', description: `> 官方 Feed 地址为: [https://www.producthunt.com/feed](https://www.producthunt.com/feed)`, + lang: 'en', }; diff --git a/lib/routes/producthunt/templates/descImg.art b/lib/routes/producthunt/templates/descImg.art deleted file mode 100644 index f66fdd4f6f7fa5..00000000000000 --- a/lib/routes/producthunt/templates/descImg.art +++ /dev/null @@ -1,5 +0,0 @@ -<br> -{{ each descData.structuredData.screenshot }} - <img src="{{ $value }}"> -{{ /each }} - diff --git a/lib/routes/producthunt/templates/description.art b/lib/routes/producthunt/templates/description.art new file mode 100644 index 00000000000000..8139b84c88c422 --- /dev/null +++ b/lib/routes/producthunt/templates/description.art @@ -0,0 +1,19 @@ +{{ if headerImage }} + <img src="https://ph-files.imgix.net/{{ headerImage }}"><br> +{{ /if }} + +{{ if description }} +<div>{{ description }}</div> +{{ /if }} + +{{ if media }} + {{ each media m }} + {{ if m.mediaType === 'image' }} + <img src="https://ph-files.imgix.net/{{ m.imageUuid }}"><br> + {{ else if m.mediaType === 'video' }} + {{ if m.metadata.platform === 'youtube' }} + <iframe id="ytplayer" type="text/html" width="640" height="360" src="https://www.youtube-nocookie.com/embed/{{ m.metadata.videoId }}" frameborder="0" allowfullscreen></iframe> + {{ /if }} + {{ /if }} + {{ /each }} +{{ /if }} diff --git a/lib/routes/producthunt/today.ts b/lib/routes/producthunt/today.ts index e33fd0fa9ce5fc..02d59d9720c404 100644 --- a/lib/routes/producthunt/today.ts +++ b/lib/routes/producthunt/today.ts @@ -3,7 +3,7 @@ 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 { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; @@ -13,7 +13,6 @@ export const route: Route = { path: '/today', categories: ['other'], example: '/producthunt/today', - parameters: {}, features: { requireConfig: false, requirePuppeteer: false, @@ -34,33 +33,61 @@ export const route: Route = { }; async function handler() { - const response = await got('https://www.producthunt.com/'); + const response = await ofetch('https://www.producthunt.com/'); - const data = JSON.parse(load(response.data)('#__NEXT_DATA__').html()); + const $ = load(response); + const match = $('script:contains("ApolloSSRDataTransport")') + .text() + .match(/"events":(\[.+\])\}\)/)?.[1] + .replaceAll('undefined', 'null'); - const list = Object.values(data.props.apolloState) - .filter((o) => o.__typename === 'Post') - // only includes new post, not product - .filter((o) => Object.hasOwn(o, 'redirectToProduct') && o.redirectToProduct === null); + const data = JSON.parse(match); + const todayList = data.find((event) => event.type === 'data' && event.result.data.homefeed).result.data.homefeed.edges.find((edge) => edge.node.id === 'FEATURED-0').node; + // 0: Top Products Launching Today + // 1: Yesterday's Top Products + // 2: Last Week's Top Products + // 3: Last Month's Top Products + + const list = todayList.items + .filter((i) => i.__typename === 'Post') + .map((item) => ({ + title: item.name, + link: `https://www.producthunt.com/posts/${item.slug}`, + slug: item.slug, + description: item.tagline, + pubDate: parseDate(item.createdAt), + image: `https://ph-files.imgix.net/${item.thumbnailImageUuid}`, + categories: item.topics.edges.map((topic) => topic.node.name), + })); const items = await Promise.all( list.map((item) => - cache.tryGet(item.slug, async () => { - const detailresponse = await got(`https://www.producthunt.com/posts/${item.slug}`); + cache.tryGet(item.link, async () => { + const response = await ofetch('https://www.producthunt.com/frontend/graphql', { + method: 'POST', + body: { + operationName: 'PostPage', + variables: { + slug: item.slug, + }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: '3d56ad0687ad82922d71fca238e5081853609dd7b16207a5aa042dee884edaea', + }, + }, + }, + }); + const post = response.data.post; - const data = JSON.parse(load(detailresponse.data)('#__NEXT_DATA__').html()); - const descData = data.props.apolloState[`Post${item.id}`]; + item.author = post.user.name; + item.description = art(path.join(__dirname, 'templates/description.art'), { + headerImage: post.headerImage?.uuid, + description: post.description, + media: post.media, + }); - return { - title: `${item.slug} - ${item.tagline}`, - description: - descData.description + - art(path.join(__dirname, 'templates/descImg.art'), { - descData, - }), - link: `https://www.producthunt.com/posts/${item.slug}`, - pubDate: parseDate(descData.createdAt), - }; + return item; }) ) ); diff --git a/lib/routes/ps/monthly-games.ts b/lib/routes/ps/monthly-games.ts index a3c583fc16decc..277139be1ef6b3 100644 --- a/lib/routes/ps/monthly-games.ts +++ b/lib/routes/ps/monthly-games.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 { art } from '@/utils/render'; export const route: Route = { path: '/monthly-games', - categories: ['game'], + categories: ['game', 'popular'], + view: ViewType.Notifications, example: '/ps/monthly-games', parameters: {}, features: { @@ -37,17 +38,17 @@ async function handler() { const { data: response } = await got(baseUrl); const $ = load(response); - const list = $('.cmp-experiencefragment--your-latest-monthly-games .box') + const list = $('#monthly-games .box--light ') .toArray() - .map((item) => { - item = $(item); + .map((e) => { + const item = $(e); return { title: item.find('h3').text(), description: art(path.join(__dirname, 'templates/monthly-games.art'), { img: item.find('.media-block__img source').attr('srcset'), text: item.find('h3 + p').text(), }), - link: item.find('.button a').attr('href'), + link: item.find('.btn--cta').attr('href'), }; }); diff --git a/lib/routes/ps/namespace.ts b/lib/routes/ps/namespace.ts index 420fe0c37b57dd..91fa76573bef71 100644 --- a/lib/routes/ps/namespace.ts +++ b/lib/routes/ps/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PlayStation Store', url: 'www.playstation.com', + lang: 'en', }; diff --git a/lib/routes/psyche/namespace.ts b/lib/routes/psyche/namespace.ts new file mode 100644 index 00000000000000..18dcc85cdff01d --- /dev/null +++ b/lib/routes/psyche/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Psyche', + url: 'psyche.co', + lang: 'en', +}; diff --git a/lib/routes/psyche/templates/essay.art b/lib/routes/psyche/templates/essay.art new file mode 100644 index 00000000000000..5a2fab892134eb --- /dev/null +++ b/lib/routes/psyche/templates/essay.art @@ -0,0 +1,3 @@ +<img src="{{ banner }}" alt=""> +{{@ authorsBio }} +{{@ content}} \ No newline at end of file diff --git a/lib/routes/psyche/templates/video.art b/lib/routes/psyche/templates/video.art new file mode 100644 index 00000000000000..d1f546c3981a3f --- /dev/null +++ b/lib/routes/psyche/templates/video.art @@ -0,0 +1,10 @@ +{{ set video = article.hosterId }} +{{ if article.hoster === 'vimeo' }} + {{ set video = "https://player.vimeo.com/video/" + video + "?dnt=1"}} +{{ else if article.hoster == 'youtube' }} + {{ set video = "https://www.youtube-nocookie.com/embed/" + video }} +{{ /if }} + +<iframe width="672" height="377" src="{{ video }}" frameborder="0" allowfullscreen></iframe> +{{@ article.credits}} +{{@ article.description}} diff --git a/lib/routes/psyche/topic.ts b/lib/routes/psyche/topic.ts new file mode 100644 index 00000000000000..d3ad02d4c2d3d4 --- /dev/null +++ b/lib/routes/psyche/topic.ts @@ -0,0 +1,46 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import { getData } from './utils'; + +export const route: Route = { + path: '/topic/:topic', + categories: ['new-media', 'popular'], + example: '/psyche/topic/therapeia', + parameters: { topic: 'Topic' }, + radar: [ + { + source: ['psyche.co/:topic'], + }, + ], + name: 'Topics', + maintainers: ['emdoe'], + handler, + description: 'Supported categories: Therapeia, Eudaimonia, and Poiesis.', +}; + +async function handler(ctx) { + const url = `https://psyche.co/${ctx.req.param('topic')}`; + const response = await ofetch(url); + const $ = load(response); + + const data = JSON.parse($('script#__NEXT_DATA__').text()); + const articles = data.props.pageProps.articles; + const prefix = `https://psyche.co/_next/data/${data.buildId}`; + const list = Object.keys(articles).flatMap((type) => + articles[type].edges.map((item) => ({ + title: item.node.title, + link: `https://psyche.co/${type}/${item.node.slug}`, + json: `${prefix}/${type}/${item.node.slug}.json`, + })) + ); + + const items = await getData(list); + + return { + title: `Psyche | ${data.props.pageProps.section.title}`, + link: url, + description: data.props.pageProps.section.metaDescription, + item: items, + }; +} diff --git a/lib/routes/psyche/type.ts b/lib/routes/psyche/type.ts new file mode 100644 index 00000000000000..f4dbaeca42c5b7 --- /dev/null +++ b/lib/routes/psyche/type.ts @@ -0,0 +1,53 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import { getData } from './utils'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/type/:type', + categories: ['new-media'], + example: '/psyche/type/ideas', + parameters: { type: 'Type' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['psyche.co/:type'], + }, + ], + name: 'Types', + maintainers: ['emdoe'], + handler, + description: `Supported types: Ideas, Guides, and Films.`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type'); + const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1); + + const url = `https://psyche.co/${type}`; + const response = await ofetch(url); + const $ = load(response); + + const data = JSON.parse($('script#__NEXT_DATA__').text()); + const prefix = `https://psyche.co/_next/data/${data.buildId}`; + const list = data.props.pageProps.articles.map((item) => ({ + title: item.title, + link: `${url}/${item.slug}`, + json: `${prefix}/${type}/${item.slug}.json`, + })); + + const items = await getData(list); + + return { + title: `Psyche | ${capitalizedType}`, + link: url, + item: items, + }; +} diff --git a/lib/routes/psyche/utils.ts b/lib/routes/psyche/utils.ts new file mode 100644 index 00000000000000..61bd61c46e8eb7 --- /dev/null +++ b/lib/routes/psyche/utils.ts @@ -0,0 +1,107 @@ +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 { art } from '@/utils/render'; +import path from 'node:path'; + +const getImageById = async (id) => { + const response = await ofetch('https://api.aeonmedia.co/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: 'query getImageById($id: ID!) { image(id: $id) { id url alt caption width height } }', + variables: { id, site: 'Aeon' }, + operationName: 'getImageById', + }), + }); + + return response.data.image.url; +}; + +function format(article) { + const type = article.type.toLowerCase(); + + let block = ''; + let banner = ''; + let authorsBio = ''; + + switch (type) { + case 'film': + block = art(path.join(__dirname, 'templates/video.art'), { article }); + + break; + + case 'guide': { + banner = article.imageSquare?.url; + authorsBio = article.authors.map((author) => author.bio).join(' '); + + const sectionNames = ['Need To Know', 'What To Do', 'Key Points', 'Learn More', 'Links & Books']; + const sections = Object.keys(article).filter((key) => key.startsWith('section') && key !== 'section'); + const content = sections + .map((section) => { + const capture = load(article[section]); + capture('p.pullquote').remove(); + const sectionName = sectionNames.shift(); + return `<h2>${sectionName}</h2>` + capture.html(); + }) + .join(''); + + block = art(path.join(__dirname, 'templates/essay.art'), { banner, authorsBio, content }); + + break; + } + case 'idea': { + banner = article.imageLandscape?.url; + authorsBio = article.authors.map((author) => author.bio).join(' '); + + const capture = load(article.body); + capture('p.pullquote').remove(); + block = art(path.join(__dirname, 'templates/essay.art'), { banner, authorsBio, content: capture.html() }); + + break; + } + default: + break; + } + return block; +} + +const getData = async (list) => { + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const data = await ofetch(item.json); + const article = data.pageProps.article; + item.pubDate = new Date(article.publishedAt).toUTCString(); + const content = format(article); + const capture = load(content); + await Promise.all( + capture('dl > dt') + .toArray() + .map(async (item) => { + const id = capture(item).text(); + const image = await getImageById(id); + capture(item).replaceWith(`<img src="${image}" alt="${id}">`); + }) + ); + + let authors = ''; + authors = article.type === 'film' ? article.creditsShort : article.authors.map((author) => author.name).join(', '); + + item.description = capture.html(); + item.author = authors; + + return item; + }) + ) + ); + + return items; +}; + +export { getData }; diff --git a/lib/routes/pts/namespace.ts b/lib/routes/pts/namespace.ts index df77e836ccf296..12bc3701f48763 100644 --- a/lib/routes/pts/namespace.ts +++ b/lib/routes/pts/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '公視新聞網', url: 'news.pts.org.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/publico/ciencias.ts b/lib/routes/publico/ciencias.ts new file mode 100644 index 00000000000000..9bd842ff86eaf0 --- /dev/null +++ b/lib/routes/publico/ciencias.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +import getItems from './items-processor'; + +export const route: Route = { + path: '/ciencias/:subsection?', + parameters: { + subsection: { + description: "Filter by subsection. Check the subsections available on the newspaper's website.", + }, + }, + categories: ['traditional-media'], + example: '/publico/ciencias', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publico.es/ciencias'], + target: '/ciencias', + }, + ], + name: 'Ciencias', + maintainers: ['adrianrico97'], + handler, +}; + +async function handler(ctx) { + const { subsection } = ctx.req.param(); + + const rootUrl = 'https://www.publico.es'; + const currentUrl = subsection ? `${rootUrl}/ciencias/${subsection}` : `${rootUrl}/ciencias`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + const title = $('.article-section h1').text(); + const items = getItems($); + + return { + title: `${title} | Público`, + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/publico/culturas.ts b/lib/routes/publico/culturas.ts new file mode 100644 index 00000000000000..dd344c549fc80d --- /dev/null +++ b/lib/routes/publico/culturas.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +import getItems from './items-processor'; + +export const route: Route = { + path: '/culturas/:subsection?', + parameters: { + subsection: { + description: "Filter by subsection. Check the subsections available on the newspaper's website.", + }, + }, + categories: ['traditional-media'], + example: '/publico/culturas', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publico.es/culturas'], + target: '/culturas', + }, + ], + name: 'Culturas', + maintainers: ['adrianrico97'], + handler, +}; + +async function handler(ctx) { + const { subsection } = ctx.req.param(); + + const rootUrl = 'https://www.publico.es'; + const currentUrl = subsection ? `${rootUrl}/culturas/${subsection}` : `${rootUrl}/culturas`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + const title = $('.article-section h1').text(); + const items = getItems($); + + return { + title: `${title} | Público`, + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/publico/economia.ts b/lib/routes/publico/economia.ts new file mode 100644 index 00000000000000..be998d28c71662 --- /dev/null +++ b/lib/routes/publico/economia.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +import getItems from './items-processor'; + +export const route: Route = { + path: '/economia/:subsection?', + parameters: { + subsection: { + description: "Filter by subsection. Check the subsections available on the newspaper's website.", + }, + }, + categories: ['traditional-media'], + example: '/publico/economia', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publico.es/economia'], + target: '/economia', + }, + ], + name: 'Economia', + maintainers: ['adrianrico97'], + handler, +}; + +async function handler(ctx) { + const { subsection } = ctx.req.param(); + + const rootUrl = 'https://www.publico.es'; + const currentUrl = subsection ? `${rootUrl}/economia/${subsection}` : `${rootUrl}/economia`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + const title = $('.article-section h1').text(); + const items = getItems($); + + return { + title: `${title} | Público`, + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/publico/internacional.ts b/lib/routes/publico/internacional.ts new file mode 100644 index 00000000000000..7ea2b15c07f889 --- /dev/null +++ b/lib/routes/publico/internacional.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +import getItems from './items-processor'; + +export const route: Route = { + path: '/internacional/:subsection?', + parameters: { + subsection: { + description: "Filter by subsection. Check the subsections available on the newspaper's website.", + }, + }, + categories: ['traditional-media'], + example: '/publico/internacional', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publico.es/internacional'], + target: '/internacional', + }, + ], + name: 'Internacional', + maintainers: ['adrianrico97'], + handler, +}; + +async function handler(ctx) { + const { subsection } = ctx.req.param(); + + const rootUrl = 'https://www.publico.es'; + const currentUrl = subsection ? `${rootUrl}/internacional/${subsection}` : `${rootUrl}/internacional`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + const title = $('.article-section h1').text(); + const items = getItems($); + + return { + title: `${title} | Público`, + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/publico/items-processor.ts b/lib/routes/publico/items-processor.ts new file mode 100644 index 00000000000000..0582e3796d54ba --- /dev/null +++ b/lib/routes/publico/items-processor.ts @@ -0,0 +1,20 @@ +export default function getItems(data) { + const items = data('.category-list li') + .toArray() + .map((item) => { + item = data(item); + const title = item.find('h2').text(); + const link = item.find('a').attr('href'); + const author = item.find('p').text(); + const image = item.find('picture img').attr('src'); + + return { + title, + link, + description: `<img src="${image}" alt="${title}">`, + author, + }; + }); + + return items; +} diff --git a/lib/routes/publico/mujer.ts b/lib/routes/publico/mujer.ts new file mode 100644 index 00000000000000..e5d2438b87267a --- /dev/null +++ b/lib/routes/publico/mujer.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +import getItems from './items-processor'; + +export const route: Route = { + path: '/mujer/:subsection?', + parameters: { + subsection: { + description: "Filter by subsection. Check the subsections available on the newspaper's website.", + }, + }, + categories: ['traditional-media'], + example: '/publico/mujer', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publico.es/mujer'], + target: '/mujer', + }, + ], + name: 'Mujer', + maintainers: ['adrianrico97'], + handler, +}; + +async function handler(ctx) { + const { subsection } = ctx.req.param(); + + const rootUrl = 'https://www.publico.es'; + const currentUrl = subsection ? `${rootUrl}/mujer/${subsection}` : `${rootUrl}/mujer`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + const title = $('.article-section h1').text(); + const items = getItems($); + + return { + title: `${title} | Público`, + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/publico/namespace.ts b/lib/routes/publico/namespace.ts new file mode 100644 index 00000000000000..e4acb1b3d06a5f --- /dev/null +++ b/lib/routes/publico/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Público', + url: 'publico.es', +}; diff --git a/lib/routes/publico/opinion.ts b/lib/routes/publico/opinion.ts new file mode 100644 index 00000000000000..6cd7595d77fa93 --- /dev/null +++ b/lib/routes/publico/opinion.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +import getItems from './items-processor'; + +export const route: Route = { + path: '/opinion/:subsection?', + parameters: { + subsection: { + description: "Filter by subsection. Check the subsections available on the newspaper's website.", + }, + }, + categories: ['traditional-media'], + example: '/publico/opinion', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publico.es/opinion'], + target: '/opinion', + }, + ], + name: 'Opinión', + maintainers: ['adrianrico97'], + handler, +}; + +async function handler(ctx) { + const { subsection } = ctx.req.param(); + + const rootUrl = 'https://www.publico.es'; + const currentUrl = subsection ? `${rootUrl}/opinion/${subsection}` : `${rootUrl}/opinion`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + const title = $('.article-section h1').text(); + const items = getItems($); + + return { + title: `${title} | Público`, + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/publico/politica.ts b/lib/routes/publico/politica.ts new file mode 100644 index 00000000000000..75e464d143ec5e --- /dev/null +++ b/lib/routes/publico/politica.ts @@ -0,0 +1,56 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +import getItems from './items-processor'; + +export const route: Route = { + path: '/politica/:subsection?', + parameters: { + subsection: { + description: "Filter by subsection. Check the subsections available on the newspaper's website.", + }, + }, + categories: ['traditional-media'], + example: '/publico/politica', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publico.es/politica'], + target: '/politica', + }, + ], + name: 'Política', + maintainers: ['adrianrico97'], + handler, +}; + +async function handler(ctx) { + const { subsection } = ctx.req.param(); + + const rootUrl = 'https://www.publico.es'; + const currentUrl = subsection ? `${rootUrl}/politica/${subsection}` : `${rootUrl}/politica`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + + const title = $('.article-section h1').text(); + const items = getItems($); + + return { + title: `${title} | Público`, + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/publico/public.ts b/lib/routes/publico/public.ts new file mode 100644 index 00000000000000..6bb15a7dd5137f --- /dev/null +++ b/lib/routes/publico/public.ts @@ -0,0 +1,48 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +import getItems from './items-processor'; + +export const route: Route = { + path: '/public', + categories: ['traditional-media'], + example: '/publico/public', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publico.es/public'], + target: '/public', + }, + ], + name: 'Public', + maintainers: ['adrianrico97'], + handler, +}; + +async function handler() { + const rootUrl = 'https://www.publico.es'; + const currentUrl = `${rootUrl}/public`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + + const items = getItems($); + + return { + title: 'public | Público', + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/publico/sociedad.ts b/lib/routes/publico/sociedad.ts new file mode 100644 index 00000000000000..ff02395d83a04b --- /dev/null +++ b/lib/routes/publico/sociedad.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +import getItems from './items-processor'; + +export const route: Route = { + path: '/sociedad/:subsection?', + parameters: { + subsection: { + description: "Filter by subsection. Check the subsections available on the newspaper's website.", + }, + }, + categories: ['traditional-media'], + example: '/publico/sociedad', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publico.es/sociedad'], + target: '/sociedad', + }, + ], + name: 'Sociedad', + maintainers: ['adrianrico97'], + handler, +}; + +async function handler(ctx) { + const { subsection } = ctx.req.param(); + + const rootUrl = 'https://www.publico.es'; + const currentUrl = subsection ? `${rootUrl}/sociedad/${subsection}` : `${rootUrl}/sociedad`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + + const items = getItems($); + + return { + title: 'Medio Ambiente | Sociedad | Público', + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/publico/tremending.ts b/lib/routes/publico/tremending.ts new file mode 100644 index 00000000000000..7692662a1b8ef3 --- /dev/null +++ b/lib/routes/publico/tremending.ts @@ -0,0 +1,48 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +import getItems from './items-processor'; + +export const route: Route = { + path: '/tremending', + categories: ['traditional-media'], + example: '/publico/tremending', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['publico.es/tremending'], + target: '/tremending', + }, + ], + name: 'Tremending', + maintainers: ['adrianrico97'], + handler, +}; + +async function handler() { + const rootUrl = 'https://www.publico.es'; + const currentUrl = `${rootUrl}/tremending`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + + const items = getItems($); + + return { + title: 'Tremending | Público', + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/pubmed/namespace.ts b/lib/routes/pubmed/namespace.ts index b5d0d680147099..6475c2b65b8937 100644 --- a/lib/routes/pubmed/namespace.ts +++ b/lib/routes/pubmed/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PubMed', url: 'pubmed.ncbi.nlm.nih.gov', + lang: 'en', }; diff --git a/lib/routes/pubscholar/explore.ts b/lib/routes/pubscholar/explore.ts new file mode 100644 index 00000000000000..66c24df3531c80 --- /dev/null +++ b/lib/routes/pubscholar/explore.ts @@ -0,0 +1,62 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { baseUrl, uuidv4, getArticleLink, getSignedHeaders } from './utils'; +import md5 from '@/utils/md5'; +import { Resource } from './types'; +import sanitizeHtml from 'sanitize-html'; + +export const route: Route = { + path: '/explore/:category?/:keyword?', + name: 'Explore', + maintainers: ['TonyRL'], + example: '/pubscholar/explore', + parameters: { + category: 'Category, see the table below, `articles` by default', + keyword: 'Search Keyword', + }, + handler, + description: `| Articles / 论文 | Patents / 专利 | Reports / 领域快报 | Information / 动态快讯 | Datasets / 科学数据 | Books / 图书 | +| --------------- | -------------- | ------------------ | ---------------------- | ------------------- | ------------ | +| articles | patents | bulletins | reports | sciencedata | books |`, +}; + +async function handler(ctx) { + const { category = 'articles', keyword } = ctx.req.param(); + const uuid = uuidv4(); + + const response = await ofetch<Resource>(`${baseUrl}/hky/open/resources/api/v1/${category}`, { + method: 'POST', + headers: { + ...getSignedHeaders(), + Cookie: `XSRF-TOKEN=${uuid}`, + 'X-XSRF-TOKEN': uuid, + }, + body: { + page: 1, + size: 10, + order_field: 'date', + order_direction: 'desc', + user_id: md5(Date.now().toString()), + lang: 'zh', + query: keyword, + strategy: null, + orderField: 'default', + }, + }); + + const list = response.content.map((item) => ({ + title: (item.is_free || item.links.some((l) => l.is_open_access) ? '「Open Access」' : '') + sanitizeHtml(item.title, { allowedTags: [], allowedAttributes: {} }), + description: item.abstracts + `<br>${item.links.map((link) => `<a href="${link.url}">${link.is_open_access ? '「Open Access」' : ''}${link.name}</a>`).join('<br>')}`, + author: item.author.join('; '), + pubDate: parseDate(item.date), + category: item.keywords.map((keyword) => sanitizeHtml(keyword, { allowedTags: [], allowedAttributes: {} })), + link: `${baseUrl}/${category}/${getArticleLink(item.id)}`, + })); + + return { + title: 'PubScholar 公益学术平台', + link: `${baseUrl}/explore`, + item: list, + }; +} diff --git a/lib/routes/pubscholar/namespace.ts b/lib/routes/pubscholar/namespace.ts new file mode 100644 index 00000000000000..be38778befbcdb --- /dev/null +++ b/lib/routes/pubscholar/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'PubScholar 公益学术平台', + url: 'pubscholar.cn', + categories: ['journal'], + lang: 'zh-CN', +}; diff --git a/lib/routes/pubscholar/types.ts b/lib/routes/pubscholar/types.ts new file mode 100644 index 00000000000000..8d51adfbe17cd3 --- /dev/null +++ b/lib/routes/pubscholar/types.ts @@ -0,0 +1,41 @@ +interface Link { + is_open_access: boolean; + name: string; + url: string; +} + +interface Content { + date: string; + attachments: any[]; + keywords: string[]; + year: number; + source: string; + title: string; + type: string; + abstracts_abbreviation: string; + major: string; + school: any[]; + first_page: string; + local_links: any[]; + links: Link[]; + id: string; + graduation_institution: any[]; + cn_type: string; + article_type: string; + issue: string; + abstracts: string; + author: string[]; + last_page: string; + degree: string; + tutor: any[]; + semantic_entities: object; + volume: string; + source_list: string[]; + is_free: boolean; +} + +export interface Resource { + total: number; + is_last: boolean; + content: Content[]; +} diff --git a/lib/routes/pubscholar/utils.ts b/lib/routes/pubscholar/utils.ts new file mode 100644 index 00000000000000..8eac6238799d36 --- /dev/null +++ b/lib/routes/pubscholar/utils.ts @@ -0,0 +1,44 @@ +import crypto from 'node:crypto'; +import CryptoJS from 'crypto-js'; + +const salt = '6m6pingbinwaktg227gngifoocrfbo95'; +const key = CryptoJS.enc.Utf8.parse('eRtYuIoPaSdFgHqW'); +const iv = CryptoJS.enc.Utf8.parse('Nmc09JkLzX8765Vb'); + +export const baseUrl = 'https://pubscholar.cn'; +export const sha1 = (str: string) => crypto.createHash('sha1').update(str).digest('hex'); +export const uuidv4 = () => crypto.randomUUID(); + +const generateNonce = (length: number): string => { + if (!length) { + return null; + } + + let nonce = ''; + while (nonce.length < length) { + const randomString = Math.random().toString(36).slice(2).toUpperCase(); + nonce += randomString; + } + + return nonce.slice(0, length); +}; + +export const getSignedHeaders = () => { + const nonce = generateNonce(6); + const timestamp = Date.now().toString(); + const signature = sha1([salt, timestamp, nonce].sort().join('')); + return { + nonce, + timestamp, + signature, + }; +}; + +export const getArticleLink = (id: string) => { + const ciphertext = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(id), key, { + iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }).ciphertext.toString(); + return ciphertext; +}; diff --git a/lib/routes/pumc/namespace.ts b/lib/routes/pumc/namespace.ts index 64ec7ab6d8169a..53079ed1c22af6 100644 --- a/lib/routes/pumc/namespace.ts +++ b/lib/routes/pumc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京协和医学院', url: 'mdadmission.pumc.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/putty/namespace.ts b/lib/routes/putty/namespace.ts index 6ef5b65a2a01a3..2b9b2cd105d5f7 100644 --- a/lib/routes/putty/namespace.ts +++ b/lib/routes/putty/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PuTTY', url: 'www.chiark.greenend.org.uk', + lang: 'en', }; diff --git a/lib/routes/pwc/namespace.ts b/lib/routes/pwc/namespace.ts index 0d4baf77a5d643..6f1df12dcc98e8 100644 --- a/lib/routes/pwc/namespace.ts +++ b/lib/routes/pwc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PwC Strategy&', url: 'strategyand.pwc.com', + lang: 'en', }; diff --git a/lib/routes/pwc/sustainability.ts b/lib/routes/pwc/sustainability.ts index eea06115ca69cb..91c45fd85cc6bd 100644 --- a/lib/routes/pwc/sustainability.ts +++ b/lib/routes/pwc/sustainability.ts @@ -1,22 +1,45 @@ import { Route } from '@/types'; -import { load } from 'cheerio'; -import logger from '@/utils/logger'; import { parseDate } from '@/utils/parse-date'; -import puppeteer from '@/utils/puppeteer'; +import ofetch from '@/utils/ofetch'; + +interface PlayerOptions { + link: string; + image: string; + flashplayer: string; + width: string; + height: string; + aspectratio: string; + title: string; + description: string; + autostart: boolean; +} + +interface Element { + index: number; + href: string; + relativeHref: string; + text: string; + thumbnailText: string; + title: string; + image: string; + tags: string[]; + filterTags: string; + publishDate: string; + playerOptions: PlayerOptions; + damSize: string; + template: string; + itemUrl: string; + itemWidth: string; + itemHeight: string; + isPage: boolean; + isVideo: boolean; + itemVideoTranscriptLink: string; +} export const route: Route = { path: '/strategyand/sustainability', categories: ['other'], example: '/pwc/strategyand/sustainability', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: true, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, radar: [ { source: ['strategyand.pwc.com/at/en/functions/sustainability-strategy/publications.html', 'strategyand.pwc.com/'], @@ -33,47 +56,28 @@ async function handler() { const feedLang = 'en'; const feedDescription = 'Sustainability Publications from PwC Strategy&'; - const browser = await puppeteer(); - const page = await browser.newPage(); - logger.http(`Requesting ${baseUrl}`); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); - }); - await page.goto(baseUrl, { - waitUntil: 'domcontentloaded', - }); - const response = await page.content(); - page.close(); - - const $ = load(response); - - const list = $('div#wrapper article') - .toArray() - .map((item) => { - item = $(item); - const a = item.find('a').first(); - const div = item.find('div.collection__item-content').first(); - - const link = a.attr('href'); - const title = div.find('h4').find('span').text(); - const pubDate = parseDate(div.find('time').attr('datetime'), 'DD/MM/YY'); - const description = div.find('p.paragraph').text(); - - return { - title, - link, - pubDate, - description, - }; - }); + const response = await ofetch( + 'https://www.strategyand.pwc.com/content/pwc/03/en/functions/sustainability-strategy/publications/jcr:content/root/container/content-free-container/section_545483788/collection_v2.filter-dynamic.html', + { + query: { + currentPagePath: '/content/pwc/03/en/functions/sustainability-strategy/publications', + list: { menu_0: [] }, + defaultImagePath: '/content/dam/pwc/network/strategyand-collection-fallback-images', + }, + } + ); + const elements = JSON.parse(response.elements) as Element[]; - const items = list; + const items = elements.map((item) => ({ + title: item.title, + link: item.href, + pubDate: parseDate(item.publishDate, 'DD/MM/YY'), + description: item.text, + category: item.tags, + })); // TODO: Add full text support - browser.close(); - return { title: 'PwC Strategy& - Sustainability Publications', link: baseUrl, diff --git a/lib/routes/qbitai/category.ts b/lib/routes/qbitai/category.ts index 5b710bcb50e251..12ba955a7f552a 100644 --- a/lib/routes/qbitai/category.ts +++ b/lib/routes/qbitai/category.ts @@ -1,11 +1,13 @@ import { Route } from '@/types'; import parser from '@/utils/rss-parser'; - +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/category/:category', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/qbitai/category/资讯', parameters: { category: '分类名,见下表' }, features: { @@ -22,7 +24,7 @@ export const route: Route = { }, ], name: '分类', - maintainers: ['FuryMartin'], + maintainers: ['FuryMartin, Geraldxm'], handler, description: `| 资讯 | 数码 | 智能车 | 智库 | 活动 | | ---- | -------- | ------ | ----- | ------- | @@ -31,24 +33,36 @@ export const route: Route = { async function handler(ctx) { const category = ctx.req.param('category'); - const link = encodeURI(`https://www.qbitai.com/category/${category}/feed`); - const feed = await parser.parseURL(link); + const url = encodeURI(`https://www.qbitai.com/category/${category}/feed`); - const items = feed.items.map((item) => ({ + const feed = await parser.parseURL(url); + const entries = feed.items.map((item) => ({ title: item.title, pubDate: parseDate(item.pubDate), link: item.link, author: '量子位', category: item.categories, - description: item['content:encoded'], + description: '', // Initialize description field })); + const resolvedEntries = await Promise.all( + entries.map((entry) => + cache.tryGet(entry.link, async () => { + try { + const response = await ofetch(entry.link); + const $ = load(response); + entry.description = $('.article').html() || 'No content found'; + } catch { + entry.description = 'Failed to fetch content'; + } + return entry; + }) + ) + ); + return { - // 源标题 - title: `量子位-${category}`, - // 源链接 + title: `量子位 - ${category}`, link: `https://www.qbitai.com/category/${category}`, - // 源文章 - item: items, + item: resolvedEntries, }; } diff --git a/lib/routes/qbitai/namespace.ts b/lib/routes/qbitai/namespace.ts index b5d49ad0ff5e8a..fa86a4adba7cdb 100644 --- a/lib/routes/qbitai/namespace.ts +++ b/lib/routes/qbitai/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '量子位', url: 'qbitai.com', + lang: 'zh-CN', }; diff --git a/lib/routes/qbitai/tag.ts b/lib/routes/qbitai/tag.ts index edea512d1d5e74..34177b101f1997 100644 --- a/lib/routes/qbitai/tag.ts +++ b/lib/routes/qbitai/tag.ts @@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/tag/:tag', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/qbitai/tag/大语言模型', parameters: { tag: '标签名' }, features: { diff --git a/lib/routes/qbittorrent/namespace.ts b/lib/routes/qbittorrent/namespace.ts index 6d3679e47fbc31..c80161c763e635 100644 --- a/lib/routes/qbittorrent/namespace.ts +++ b/lib/routes/qbittorrent/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'qBittorrent', url: 'qbittorrent.org', + lang: 'en', }; diff --git a/lib/routes/qdu/namespace.ts b/lib/routes/qdu/namespace.ts index 23a52f5c24c22f..b9a2dcf0afb13b 100644 --- a/lib/routes/qdu/namespace.ts +++ b/lib/routes/qdu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '青岛大学', url: 'jwc.qdu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/qianp/namespace.ts b/lib/routes/qianp/namespace.ts index 74e99f15138738..3c989ed47c0969 100644 --- a/lib/routes/qianp/namespace.ts +++ b/lib/routes/qianp/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '千篇网', url: 'qianp.com', + lang: 'zh-CN', }; diff --git a/lib/routes/qianzhan/column.ts b/lib/routes/qianzhan/column.ts index c72318da966bd4..e6acc15c084419 100644 --- a/lib/routes/qianzhan/column.ts +++ b/lib/routes/qianzhan/column.ts @@ -22,8 +22,8 @@ export const route: Route = { maintainers: ['moke8'], handler, description: `| 全部 | 研究员专栏 | 规划师专栏 | 观察家专栏 | - | ---- | ---------- | ---------- | ---------- | - | all | 220 | 627 | 329 |`, +| ---- | ---------- | ---------- | ---------- | +| all | 220 | 627 | 329 |`, }; async function handler(ctx) { diff --git a/lib/routes/qianzhan/namespace.ts b/lib/routes/qianzhan/namespace.ts index 0002eecbbb5c96..f45a81e778808d 100644 --- a/lib/routes/qianzhan/namespace.ts +++ b/lib/routes/qianzhan/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '前瞻网', url: 'qianzhan.com', + lang: 'zh-CN', }; diff --git a/lib/routes/qianzhan/rank.ts b/lib/routes/qianzhan/rank.ts index f8500a53c96ed6..d3099afc045b21 100644 --- a/lib/routes/qianzhan/rank.ts +++ b/lib/routes/qianzhan/rank.ts @@ -29,8 +29,8 @@ export const route: Route = { handler, url: 'qianzhan.com/analyst', description: `| 周排行 | 月排行 | - | ------ | ------ | - | week | month |`, +| ------ | ------ | +| week | month |`, }; async function handler(ctx) { diff --git a/lib/routes/qiche365/namespace.ts b/lib/routes/qiche365/namespace.ts new file mode 100644 index 00000000000000..33eee78e3ca11a --- /dev/null +++ b/lib/routes/qiche365/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; +export const namespace: Namespace = { + name: '汽车召回网', + url: 'qiche365.org.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/qiche365/recall.ts b/lib/routes/qiche365/recall.ts new file mode 100644 index 00000000000000..882a93019f6d20 --- /dev/null +++ b/lib/routes/qiche365/recall.ts @@ -0,0 +1,53 @@ +import { Route, Data, DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const baseUrl = 'https://www.qiche365.org.cn'; + +export const route: Route = { + path: '/recall/:channel', + name: '汽车召回', + example: '/qiche365/recall/1', + parameters: { channel: '频道,见下表' }, + description: `| 国内召回新闻 | 国内召回公告 | 国外召回新闻 | 国外召回公告 | +| ------------ | ------------ | ------------ | ------------ | +| 1 | 2 | 3 | 4 |`, + categories: ['government'], + maintainers: ['huanfe1'], + handler, + url: 'qiche365.org.cn/index/recall/index.html', +}; + +async function handler(ctx): Promise<Data> { + const { channel } = ctx.req.param(); + + const { html } = await ofetch(`${baseUrl}/index/recall/index/item/${channel}.html?loadmore=1`, { + method: 'get', + headers: { + 'Accept-Language': 'zh-CN,zh;q=0.9', + }, + }); + + const $ = load(<string>html); + const items: DataItem[] = $('li') + .toArray() + .map((item) => { + const cheerioItem = $(item); + return { + title: cheerioItem.find('h1').text(), + link: `${baseUrl}${cheerioItem.find('a').attr('href')}`, + pubDate: timezone(parseDate(cheerioItem.find('h2').html()!.match('</i>(.*?)<b>')![1]), +8), + description: cheerioItem.find('p').text().trim(), + author: cheerioItem.find('h3 span').text(), + image: cheerioItem.find('img').attr('src') && `${baseUrl}${cheerioItem.find('img').attr('src')}`, + }; + }); + return { + title: ['国内召回公告', '国内召回新闻', '国外召回公告', '国外召回新闻'][channel - 1], + link: `${baseUrl}/index/recall/index.html`, + item: items, + language: 'zh-CN', + }; +} diff --git a/lib/routes/qidian/chapter.ts b/lib/routes/qidian/chapter.ts index 67e018b6a34d1f..13f7f641b80000 100644 --- a/lib/routes/qidian/chapter.ts +++ b/lib/routes/qidian/chapter.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: '/chapter/:id', - categories: ['reading'], + categories: ['reading', 'popular'], + view: ViewType.Notifications, example: '/qidian/chapter/1010400217', parameters: { id: '小说 id, 可在对应小说页 URL 中找到' }, features: { @@ -21,7 +22,7 @@ export const route: Route = { source: ['book.qidian.com/info/:id'], }, ], - name: '章节', + name: '作品章节', maintainers: ['fuzy112'], handler, }; diff --git a/lib/routes/qidian/namespace.ts b/lib/routes/qidian/namespace.ts index 212b22f4e2beb3..05bf1b1b56c26e 100644 --- a/lib/routes/qidian/namespace.ts +++ b/lib/routes/qidian/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '起点', - url: 'book.qidian.com', + url: 'qidian.com', + lang: 'zh-CN', }; diff --git a/lib/routes/qingting/channel.ts b/lib/routes/qingting/channel.ts index 27c08b96c768af..8833715da67f22 100644 --- a/lib/routes/qingting/channel.ts +++ b/lib/routes/qingting/channel.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 timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; @@ -18,18 +18,18 @@ export const route: Route = { supportScihub: false, }, name: '专辑', - maintainers: ['nczitzk'], + maintainers: ['nczitzk', 'pseudoyu'], handler, }; async function handler(ctx) { const channelUrl = `https://i.qingting.fm/capi/v3/channel/${ctx.req.param('id')}`; - let response = await got(channelUrl); - const title = response.data.data.title; - const programUrl = `https://i.qingting.fm/capi/channel/${ctx.req.param('id')}/programs/${response.data.data.v}?curpage=1&pagesize=10&order=asc`; - response = await got(programUrl); + let response = await ofetch(channelUrl); + const title = response.data.title; + const programUrl = `https://i.qingting.fm/capi/channel/${ctx.req.param('id')}/programs/${response.data.v}?curpage=1&order=asc`; + response = await ofetch(programUrl); - const items = response.data.data.programs.map((item) => ({ + const items = response.data.programs.map((item) => ({ title: item.title, link: `https://www.qingting.fm/channels/${ctx.req.param('id')}/programs/${item.id}/`, pubDate: timezone(parseDate(item.update_time), +8), @@ -41,8 +41,8 @@ async function handler(ctx) { item: await Promise.all( items.map((item) => cache.tryGet(item.link, async () => { - response = await got(item.link); - const data = JSON.parse(response.data.match(/},"program":(.*?),"plist":/)[1]); + response = await ofetch(item.link); + const data = JSON.parse(response.match(/},"program":(.*?),"plist":/)[1]); item.description = data.richtext; return item; }) diff --git a/lib/routes/qingting/namespace.ts b/lib/routes/qingting/namespace.ts index c675b79340fdfc..c5a7da59265cc1 100644 --- a/lib/routes/qingting/namespace.ts +++ b/lib/routes/qingting/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '蜻蜓 FM', url: 'qingting.fm', + lang: 'zh-CN', }; diff --git a/lib/routes/qingting/podcast.ts b/lib/routes/qingting/podcast.ts index 17d1c6b4bab23a..5df881379fcd6d 100644 --- a/lib/routes/qingting/podcast.ts +++ b/lib/routes/qingting/podcast.ts @@ -1,7 +1,7 @@ import type { DataItem, Route } from '@/types'; import cache from '@/utils/cache'; import crypto from 'crypto'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; import { config } from '@/config'; @@ -29,7 +29,7 @@ export const route: Route = { }, ], name: '播客', - maintainers: ['RookieZoe', 'huyyi'], + maintainers: ['RookieZoe', 'huyyi', 'pseudoyu'], handler, description: `获取的播放 URL 有效期只有 1 天,需要开启播客 APP 的自动下载功能。`, }; @@ -44,35 +44,27 @@ async function handler(ctx) { const channelId = ctx.req.param('id'); const channelUrl = `https://i.qingting.fm/capi/v3/channel/${channelId}`; - const response = await got({ - method: 'get', - url: channelUrl, + const response = await ofetch(channelUrl, { headers: { Referer: 'https://www.qingting.fm/', }, }); - const title = response.data.data.title; - const channel_img = response.data.data.thumbs['400_thumb']; - const authors = response.data.data.podcasters.map((author) => author.nick_name).join(','); - const desc = response.data.data.description; - const programUrl = `https://i.qingting.fm/capi/channel/${channelId}/programs/${response.data.data.v}?curpage=1&pagesize=10&order=asc`; + const title = response.data.title; + const channel_img = response.data.thumbs['400_thumb']; + const authors = response.data.podcasters.map((author) => author.nick_name).join(','); + const desc = response.data.description; + const programUrl = `https://i.qingting.fm/capi/channel/${channelId}/programs/${response.data.v}?curpage=1&pagesize=10&order=asc`; const { - data: { - data: { programs }, - }, - } = await got({ - method: 'get', - url: programUrl, + data: { programs }, + } = await ofetch(programUrl, { headers: { Referer: 'https://www.qingting.fm/', }, }); - const { - data: { data: channelInfo }, - } = await got(`https://i.qingting.fm/capi/v3/channel/${channelId}?user_id=${qingtingId}`); + const { data: channelInfo } = await ofetch(`https://i.qingting.fm/capi/v3/channel/${channelId}?user_id=${qingtingId}`); const isCharged = channelInfo.purchase?.item_type !== 0; @@ -83,15 +75,13 @@ async function handler(ctx) { const data = (await cache.tryGet(`qingting:podcast:${channelId}:${item.id}`, async () => { const link = `https://www.qingting.fm/channels/${channelId}/programs/${item.id}/`; - const detailRes = await got({ - method: 'get', - url: link, + const detailRes = await ofetch(link, { headers: { Referer: 'https://www.qingting.fm/', }, }); - const detail = JSON.parse(detailRes.data.match(/},"program":(.*?),"plist":/)[1]); + const detail = JSON.parse(detailRes.match(/},"program":(.*?),"plist":/)[1]); const rssItem = { title: item.title, diff --git a/lib/routes/qipamaijia/namespace.ts b/lib/routes/qipamaijia/namespace.ts index 1eee452ef21e7c..7fd1c4ba7ba0db 100644 --- a/lib/routes/qipamaijia/namespace.ts +++ b/lib/routes/qipamaijia/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '奇葩买家秀', url: 'qipamaijia.com', + lang: 'zh-CN', }; diff --git a/lib/routes/qiyoujiage/namespace.ts b/lib/routes/qiyoujiage/namespace.ts index bd4ecec1cecdad..44ea14de6fb9f5 100644 --- a/lib/routes/qiyoujiage/namespace.ts +++ b/lib/routes/qiyoujiage/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '汽油价格网', url: 'qiyoujiage.com', + lang: 'zh-CN', }; diff --git a/lib/routes/qlu/namespace.ts b/lib/routes/qlu/namespace.ts index cbfc02bba501da..56e5029bff4a5a 100644 --- a/lib/routes/qlu/namespace.ts +++ b/lib/routes/qlu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '齐鲁工业大学', url: 'qlu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/qm120/namespace.ts b/lib/routes/qm120/namespace.ts index 0687ba180ca1ba..4edb1a1ecacaad 100644 --- a/lib/routes/qm120/namespace.ts +++ b/lib/routes/qm120/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '全民健康网', url: 'qm120.com', + lang: 'zh-CN', }; diff --git a/lib/routes/qm120/news.ts b/lib/routes/qm120/news.ts index 9660a8a74f8856..5be2a794d5debc 100644 --- a/lib/routes/qm120/news.ts +++ b/lib/routes/qm120/news.ts @@ -28,16 +28,16 @@ export const route: Route = { handler, url: 'qm120.com/', description: `| 健康焦点 | 行业动态 | 医学前沿 | 法规动态 | - | -------- | -------- | -------- | -------- | - | jdxw | hydt | yxqy | fgdt | +| -------- | -------- | -------- | -------- | +| jdxw | hydt | yxqy | fgdt | - | 食品安全 | 医疗事故 | 医药会展 | 医药信息 | - | -------- | -------- | -------- | -------- | - | spaq | ylsg | yyhz | yyxx | +| 食品安全 | 医疗事故 | 医药会展 | 医药信息 | +| -------- | -------- | -------- | -------- | +| spaq | ylsg | yyhz | yyxx | - | 新闻专题 | 行业新闻 | - | -------- | -------- | - | zhuanti | xyxw |`, +| 新闻专题 | 行业新闻 | +| -------- | -------- | +| zhuanti | xyxw |`, }; async function handler(ctx) { diff --git a/lib/routes/qoo-app/apps/comment.ts b/lib/routes/qoo-app/apps/comment.ts index 9ca645da860fb6..a90d74956b5964 100644 --- a/lib/routes/qoo-app/apps/comment.ts +++ b/lib/routes/qoo-app/apps/comment.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| 中文 | English | 한국어 | Español | 日本語 | ไทย | Tiếng Việt | - | ---- | ------- | ------ | ------- | ------ | --- | ---------- | - | | en | ko | es | ja | th | vi |`, +| ---- | ------- | ------ | ------- | ------ | --- | ---------- | +| | en | ko | es | ja | th | vi |`, }; async function handler(ctx) { diff --git a/lib/routes/qoo-app/namespace.ts b/lib/routes/qoo-app/namespace.ts index 8cdc14f96716d8..46307c0ab467c3 100644 --- a/lib/routes/qoo-app/namespace.ts +++ b/lib/routes/qoo-app/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'QooApp', url: 'apps.qoo-app.com', + lang: 'zh-CN', }; diff --git a/lib/routes/qoo-app/news.ts b/lib/routes/qoo-app/news.ts index 068a8e102f8cc4..6c39b1626b1d96 100644 --- a/lib/routes/qoo-app/news.ts +++ b/lib/routes/qoo-app/news.ts @@ -22,8 +22,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| 中文 | English | - | ---- | ------- | - | | en |`, +| ---- | ------- | +| | en |`, }; async function handler(ctx) { diff --git a/lib/routes/qq/ac/rank.ts b/lib/routes/qq/ac/rank.ts index 1c40c78c29f6fe..4e21a3fdd4585d 100644 --- a/lib/routes/qq/ac/rank.ts +++ b/lib/routes/qq/ac/rank.ts @@ -23,12 +23,12 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 月票榜 | 飙升榜 | 新作榜 | 畅销榜 | TOP100 | 男生榜 | 女生榜 | - | ------ | ------ | ------ | ------ | ------ | ------ | ------ | - | mt | rise | new | pay | top | male | female | +| ------ | ------ | ------ | ------ | ------ | ------ | ------ | +| mt | rise | new | pay | top | male | female | - :::tip +::: tip \`time\` 参数仅在 \`type\` 参数选为 **月票榜** 的时候生效。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/qq/cfhd/index.ts b/lib/routes/qq/cfhd/index.ts new file mode 100644 index 00000000000000..826ec9a951d414 --- /dev/null +++ b/lib/routes/qq/cfhd/index.ts @@ -0,0 +1,140 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import iconv from 'iconv-lite'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; + +export const handler = async (ctx) => { + const { category = '60847' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 12; + + const rootUrl = 'https://cfhd.cf.qq.com'; + const rootImageUrl = 'https://game.gtimg.cn'; + const currentUrl = new URL(`webplat/info/news_version3/37427/59139/59140/${category}/m22510/list_1.shtml`, rootUrl).href; + + const { data: response } = await got(currentUrl, { + responseType: 'buffer', + }); + + const $ = load(iconv.decode(response, 'gbk')); + + const language = $('html').prop('lang'); + + let items = $('div.news-list-item ul li.list-item') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + return { + title: item.find('p').text(), + pubDate: parseDate(item.find('span.date').text()), + link: new URL(item.find('a.clearfix').prop('href'), rootUrl).href, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const { data: detailResponse } = await got(item.link, { + responseType: 'buffer', + }); + + const $$ = load(iconv.decode(detailResponse, 'gbk')); + + const title = $$('div.news-details-title h4').text(); + const description = $$('div.news-details-cont').html(); + const image = $$('div.news-details-cont img').first().prop('src'); + + item.title = title; + item.description = description; + item.pubDate = timezone(parseDate($$('p.news-details-p1').text().trim()), +8); + item.content = { + html: description, + text: $$('div.news-details-cont').text(), + }; + item.image = image; + item.banner = image; + item.language = language; + + return item; + }) + ) + ); + + const image = new URL('images/cfhd/web202305/logo.png', rootImageUrl).href; + + return { + title: `${$('title').text().split(/-/)[0]} - ${$('li.cur').text()}`, + description: $('meta[name="Description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $('meta[name="author"]').prop('content'), + language, + }; +}; + +export const route: Route = { + path: '/cfhd/news/:category?', + name: '穿越火线 CFHD 专区资讯中心', + url: 'cfhd.cf.qq.com', + maintainers: ['nczitzk'], + handler, + example: '/qq/cfhd/news', + parameters: { category: '分类,默认为 60847,即最新,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [穿越火线 CFHD 专区资讯中心 - 最新](https://cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/60847/m22510/list_1.shtml),网址为 \`https://cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/60847/m22510/list_1.shtml\`。截取 \`https://cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/\` 到末尾 \`/m22510/list_1.shtml\` 的部分 \`60847\` 作为参数填入,此时路由为 [\`/qq/cfhd/news/60847\`](https://rsshub.app/qq/cfhd/news/60847)。 +::: + +| 分类 | ID | +| ----------------------------------------------------------------------------------------------------- | --------------------------------------------- | +| [最新](https://cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/60847/m22510/list_1.shtml) | [60847](https://rsshub.app/qq/cfhd/news/60847) | +| [公告](https://cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/59625/m22510/list_1.shtml) | [59625](https://rsshub.app/qq/cfhd/news/59625) | +| [版本](https://cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/60850/m22510/list_1.shtml) | [60850](https://rsshub.app/qq/cfhd/news/60850) | +| [赛事](https://cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/59626/m22510/list_1.shtml) | [59626](https://rsshub.app/qq/cfhd/news/59626) | +| [杂谈](https://cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/59624/m22510/list_1.shtml) | [59624](https://rsshub.app/qq/cfhd/news/59624) | + `, + categories: ['game'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + title: '穿越火线 CFHD 专区资讯中心 - 最新', + source: ['cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/60847/m22510/list_1.shtml'], + target: '/cfhd/news/60847', + }, + { + title: '穿越火线 CFHD 专区资讯中心 - 公告', + source: ['cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/59625/m22510/list_1.shtml'], + target: '/cfhd/news/59625', + }, + { + title: '穿越火线 CFHD 专区资讯中心 - 版本', + source: ['cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/60850/m22510/list_1.shtml'], + target: '/cfhd/news/60850', + }, + { + title: '穿越火线 CFHD 专区资讯中心 - 赛事', + source: ['cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/59626/m22510/list_1.shtml'], + target: '/cfhd/news/59626', + }, + { + title: '穿越火线 CFHD 专区资讯中心 - 杂谈', + source: ['cfhd.cf.qq.com/webplat/info/news_version3/37427/59139/59140/59624/m22510/list_1.shtml'], + target: '/cfhd/news/59624', + }, + ], +}; diff --git a/lib/routes/qq/lol/news.ts b/lib/routes/qq/lol/news.ts new file mode 100644 index 00000000000000..9a08e95fcdaea6 --- /dev/null +++ b/lib/routes/qq/lol/news.ts @@ -0,0 +1,206 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +import cache from '@/utils/cache'; +import iconv from 'iconv-lite'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +import { type CheerioAPI, load } from 'cheerio'; +import { type Context } from 'hono'; + +export const handler = async (ctx: Context): Promise<Data> => { + const { category = 23 } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'https://lol.qq.com'; + const apiBaseUrl: string = 'https://apps.game.qq.com'; + const targetUrl: string = new URL('news/index.shtml', baseUrl).href; + const apiListUrl: string = new URL('cmc/zmMcnTargetContentList', apiBaseUrl).href; + const apiInfoUrl: string = new URL('cmc/zmMcnContentInfo', apiBaseUrl).href; + + const response = await ofetch(apiListUrl, { + query: { + page: 1, + num: limit, + target: category, + }, + }); + const targetResponse = await ofetch(targetUrl, { + responseType: 'arrayBuffer', + }); + + const $: CheerioAPI = load(iconv.decode(Buffer.from(targetResponse), 'gbk')); + const language = $('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + items = response.data.result.slice(0, limit).map((item): DataItem => { + const title: string = item.sTitle; + const pubDate: number | string = item.sCreated; + const linkUrl: string | undefined = item.iDocID ? `${item.iVideoId ? 'v/v2' : 'news'}/detail.shtml?docid=${item.iDocID}` : undefined; + const authors: DataItem['author'] = item.sAuthor + ? [ + { + name: item.sAuthor, + avatar: item.sCreaterHeader, + }, + ] + : undefined; + const guid: string = item.iDocID; + const image: string | undefined = item.sIMG ? (item.sIMG.startsWith('http') ? item.sIMG : `https:${item.sIMG}`) : undefined; + const updated: number | string = item.updated ?? pubDate; + + const processedItem: DataItem = { + title, + pubDate: pubDate ? timezone(parseDate(pubDate), +8) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + author: authors, + guid, + id: guid, + image, + banner: image, + updated: updated ? timezone(parseDate(updated), +8) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(apiInfoUrl, { + query: { + type: 0, + docid: item.guid, + }, + }); + + const result = detailResponse?.data?.result ?? undefined; + + if (!result) { + return item; + } + + const title: string = result.sTitle; + const description: string = result.sContent; + const pubDate: number | string = result.sCreated; + const linkUrl: string | undefined = result.iDocID ? `${result.iVideoId ? 'v/v2' : 'news'}/detail.shtml?docid=${result.iDocID}` : undefined; + const authors: DataItem['author'] = result.sAuthor + ? [ + { + name: result.sAuthor, + avatar: result.sCreaterHeader, + }, + ] + : undefined; + const guid: string = `qq-lol-${result.iDocID}`; + const image: string | undefined = result.sIMG ? (result.sIMG.startsWith('http') ? result.sIMG : `https:${result.sIMG}`) : undefined; + const updated: number | string = result.sIdxTime ?? pubDate; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDate ? timezone(parseDate(pubDate), +8) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + author: authors, + guid, + id: guid, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: updated ? timezone(parseDate(updated), +8) : undefined, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: `${$('div.website-path a') + .toArray() + .map((a) => $(a).text()) + .join('')} - ${$(`li[data-newsId="${category}"]`).text()}`, + description: $('meta[name="Description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: `https:${$('a.logo img').attr('src')}`, + author: $('meta[name="author"]').attr('content'), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/lol/news/:category?', + name: '英雄联盟新闻', + url: 'lol.qq.com', + maintainers: ['nczitzk'], + handler, + example: '/qq/lol/news', + parameters: { + category: '分类,默认为 `23`,即综合,见下表', + }, + description: `:::tip +若订阅 [英雄联盟首页新闻列表 - 公告](https://lol.qq.com/news/index.shtml),网址为 \`https://lol.qq.com/news/index.shtml\`,请选择 \`24\` 作为 \`category\` 参数填入,此时目标路由为 [\`/qq/lol/news/24\`](https://rsshub.app/qq/lol/news/24)。 +::: + +| 综合 | 公告 | 赛事 | 攻略 | 社区 | +| ---- | ---- | ---- | ---- | ---- | +| 23 | 24 | 25 | 27 | 28 | +`, + categories: ['game'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + title: '综合', + source: ['lol.qq.com/news/index.shtml'], + target: '/lol/news/23', + }, + { + title: '公告', + source: ['lol.qq.com/news/index.shtml'], + target: '/lol/news/24', + }, + { + title: '赛事', + source: ['lol.qq.com/news/index.shtml'], + target: '/lol/news/25', + }, + { + title: '攻略', + source: ['lol.qq.com/news/index.shtml'], + target: '/lol/news/27', + }, + { + title: '社区', + source: ['lol.qq.com/news/index.shtml'], + target: '/lol/news/28', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/qq/namespace.ts b/lib/routes/qq/namespace.ts index 72d8f17b3be95d..9f4be830407cc7 100644 --- a/lib/routes/qq/namespace.ts +++ b/lib/routes/qq/namespace.ts @@ -1,6 +1,9 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: '腾讯新闻较真查证平台', - url: 'ac.qq.com', + name: '腾讯网', + url: 'qq.com', + categories: ['new-media'], + description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/qq/pd/guild.ts b/lib/routes/qq/pd/guild.ts new file mode 100644 index 00000000000000..91c21736aeab69 --- /dev/null +++ b/lib/routes/qq/pd/guild.ts @@ -0,0 +1,146 @@ +import { Data, DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import type { Context } from 'hono'; + +import { Feed } from './types'; +import { parseFeed } from './utils'; +import cache from '@/utils/cache'; + +const baseUrl = 'https://pd.qq.com/g/'; +const baseApiUrl = 'https://pd.qq.com/qunng/guild/gotrpc/noauth/trpc.qchannel.commreader.ComReader/'; +const getGuildFeedsUrl = baseApiUrl + 'GetGuildFeeds'; +const getChannelTimelineFeedsUrl = baseApiUrl + 'GetChannelTimelineFeeds'; +const getFeedDetailUrl = baseApiUrl + 'GetFeedDetail'; + +const sortMap = { + hot: 0, + created: 1, + replied: 2, +}; + +export const route: Route = { + path: ['/pd/guild/:id/:sub?/:sort?'], + categories: ['bbs'], + example: '/qq/pd/guild/qrp4pkq01d/650967831/created', + parameters: { + id: '频道号', + sub: '子频道 ID,网页端 URL `subc` 参数的值,默认为 `hot`(全部)', + sort: '排序方式,`hot`(热门),`created`(最新发布),`replied`(最新回复),默认为 `created`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['pd.qq.com/'], + }, + ], + name: '腾讯频道', + maintainers: ['mobyw'], + handler, + url: 'pd.qq.com/', +}; + +async function handler(ctx: Context): Promise<Data> { + const { id, sub = 'hot', sort = 'created' } = ctx.req.param(); + + if (sort in sortMap === false) { + throw new InvalidParameterError('invalid sort parameter, should be `hot`, `created`, or `replied`'); + } + const sortType = sortMap[sort]; + + let url = ''; + let body = {}; + let headers = {}; + + if (sub === 'hot') { + url = getGuildFeedsUrl; + // notice: do not change the order of the keys in the body + body = { count: 20, from: 7, guild_number: id, get_type: 1, feedAttchInfo: '', sortOption: sortType, need_channel_list: false, need_top_info: false }; + headers = { + cookie: 'p_uin=o09000002', + 'x-oidb': '{"uint32_service_type":12}', + 'x-qq-client-appid': '537246381', + }; + } else { + url = getChannelTimelineFeedsUrl; + // notice: do not change the order of the keys in the body + body = { count: 20, from: 7, guild_number: id, channelSign: { channel_id: sub }, feedAttchInfo: '', sortOption: sortType, need_top_info: false }; + headers = { + cookie: 'p_uin=o09000002', + 'x-oidb': '{"uint32_service_type":11}', + 'x-qq-client-appid': '537246381', + }; + } + + const data = await ofetch(url, { method: 'POST', body, headers }); + const feeds = data.data?.vecFeed || []; + + const items = feeds.map(async (feed: Feed) => { + let subId = sub; + if (sub === 'hot') { + // get real subId for hot feeds + subId = feed.channelInfo?.sign?.channel_id || ''; + } + const feedLink = baseUrl + id + '/post/' + feed.id; + const feedDetail = await cache.tryGet(feedLink, async () => { + // notice: do not change the order of the keys in the body + body = { + feedId: feed.id, + userId: feed.poster?.id, + createTime: feed.createTime, + from: 2, + detail_type: 1, + channelSign: { guild_number: id, channel_id: subId }, + extInfo: { + mapInfo: [ + { key: 'qc-tabid', value: 'ark' }, + { key: 'qc-pageid', value: 'pc' }, + ], + }, + }; + headers = { + cookie: 'p_uin=o09000002', + referer: feedLink, + 'x-oidb': '{"uint32_service_type":5}', + 'x-qq-client-appid': '537246381', + }; + const feedResponse = await ofetch(getFeedDetailUrl, { method: 'POST', body, headers }); + const feedContent: Feed = feedResponse.data?.feed || {}; + return { + title: feed.title?.contents[0]?.text_content?.text || feed.channelInfo?.guild_name || '', + link: feedLink, + description: parseFeed(feedContent), + pubDate: new Date(Number(feed.createTime) * 1000), + author: feed.poster?.nick, + }; + }); + + return feedDetail; + }); + + const feedItems = await Promise.all(items); + + let guildName = ''; + + if (feeds.length > 0 && feeds[0].channelInfo?.guild_name) { + guildName = feeds[0].channelInfo?.guild_name; + if (sub !== 'hot' && feeds[0].channelInfo?.name) { + guildName += ' (' + feeds[0].channelInfo?.name + ')'; + } + guildName += ' - 腾讯频道'; + } + + return { + title: guildName, + link: baseUrl + id, + description: guildName, + item: feedItems as DataItem[], + }; +} diff --git a/lib/routes/qq/pd/types.ts b/lib/routes/qq/pd/types.ts new file mode 100644 index 00000000000000..7164c822d6cba0 --- /dev/null +++ b/lib/routes/qq/pd/types.ts @@ -0,0 +1,82 @@ +// Description: Types for QQ PD API. + +export type Feed = { + id: string; + feed_type: number; // 1-post, 2-article + patternInfo: string; // JSON string + channelInfo: ChannelInfo; + title: { + contents: FeedContent[]; + }; + contents: { + contents: FeedContent[]; + }; + images: FeedImage[]; + poster: { + id: string; + nick: string; + }; + createTime: string; +}; + +export type ChannelInfo = { + name: string; + guild_number: string; + guild_name: string; + sign: { + guild_id: string; + channel_id: string; + }; +}; + +export type FeedContent = { + type: number; + pattern_id: string; + text_content?: { + text: string; + }; + emoji_content?: { + id: string; + type: string; + }; + url_content?: { + url: string; + displayText: string; + type: number; + }; +}; + +export type FeedImage = { + picId: string; + picUrl: string; + width: number; + height: number; + pattern_id?: string; +}; + +export type FeedPattern = { + id: string; + props?: { + textAlignment: number; // 0-left, 1-center, 2-right + }; + data: FeedPatternData[]; +}; + +export type FeedPatternData = { + type: number; // 1-text, 2-emoji, 5-link, 6-image, 9-newline + text?: string; + props?: FeedFontProps; + fileId?: string; + taskId?: string; + url?: string; + width?: number; + height?: number; + desc?: string; + href?: string; +}; + +export type FeedFontProps = { + fontWeight: number; // 400-normal, 700-bold + italic: boolean; + underline: boolean; +}; diff --git a/lib/routes/qq/pd/utils.ts b/lib/routes/qq/pd/utils.ts new file mode 100644 index 00000000000000..7e469d552539d7 --- /dev/null +++ b/lib/routes/qq/pd/utils.ts @@ -0,0 +1,119 @@ +// Description: QQ PD utils + +import { Feed, FeedImage, FeedPattern, FeedFontProps, FeedPatternData } from './types'; + +const patternTypeMap = { + 1: 'text', + 2: 'emoji', + 5: 'link', + 6: 'image', + 9: 'newline', +}; + +const textAlignmentMap = { + 0: 'left', + 1: 'center', + 2: 'right', +}; + +function parseText(text: string, props: FeedFontProps | undefined): string { + if (props === undefined) { + return text; + } + let style = ''; + if (props.fontWeight === 700) { + style += 'font-weight: bold;'; + } + if (props.italic) { + style += 'font-style: italic;'; + } + if (props.underline) { + style += 'text-decoration: underline;'; + } + if (style === '') { + return text; + } + return `<span style="${style}">${text}</span>`; +} + +function parseDataItem(item: FeedPatternData, texts: string[], images: { [id: string]: FeedImage }): string { + let imageId = ''; + switch (patternTypeMap[item.type] || undefined) { + case 'text': + return parseText(texts.shift() ?? '', item.props); + case 'newline': + texts.shift(); + return '<br />'; + case 'link': + return `<a href="${item.href ?? '#'}" target="_blank">${item.desc ?? ''}</a>`; + case 'image': + imageId = item.fileId || item.taskId || ''; + return `<img src="${images[imageId].picUrl}" style="max-width: 100%; width: ${images[imageId].width}px;"><br />`; + default: + return ''; + } +} + +function parseArticle(feed: Feed, texts: string[], images: { [id: string]: FeedImage }): string { + let result = ''; + if (feed.patternInfo === undefined || feed.patternInfo === null || feed.patternInfo === '') { + feed.patternInfo = '[]'; + } + const patterns: FeedPattern[] = JSON.parse(feed.patternInfo); + for (const pattern of patterns) { + if (pattern.props === undefined) { + continue; + } + const textAlign = pattern.props.textAlignment || 0; + result += '<p style="text-align: ' + textAlignmentMap[textAlign] + ';">'; + for (const item of pattern.data) { + result += parseDataItem(item, texts, images); + } + result += '</p>'; + } + return result; +} + +function parsePost(feed: Feed, texts: string[], images: { [id: string]: FeedImage }): string { + for (const content of feed.contents.contents) { + if (content.text_content) { + texts.push(content.text_content.text); + } + } + let result = ''; + for (const text of texts) { + result += text; + } + for (const image of Object.values(images)) { + result += '<p style="text-align: center">'; + result += `<img src="${image.picUrl}" style="max-width: 100%; width: ${image.width}px;">`; + result += '</p>'; + } + return result; +} + +export function parseFeed(feed: Feed): string { + const texts: string[] = []; + const images: { [id: string]: FeedImage } = {}; + for (const content of feed.contents.contents) { + if (content.text_content) { + texts.push(content.text_content.text); + } + } + for (const image of feed.images) { + images[image.picId] = { + picId: image.picId, + picUrl: image.picUrl, + width: image.width, + height: image.height, + }; + } + if (feed.feed_type === 1) { + // post: text and attachments + return parsePost(feed, texts, images); + } else if (feed.feed_type === 2) { + // article: pattern info + return parseArticle(feed, texts, images); + } + return ''; +} diff --git a/lib/routes/qq/weread/category.ts b/lib/routes/qq/weread/category.ts new file mode 100644 index 00000000000000..410f728ca02bea --- /dev/null +++ b/lib/routes/qq/weread/category.ts @@ -0,0 +1,120 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/weread/:category', + categories: ['new-media'], + example: '/qq/weread/newbook', + parameters: { + category: '榜单名,见下表', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '微信读书榜单', + maintainers: ['gogo-100'], + handler, + description: `| 榜单 | 榜单名 | +| ---------------------- | ---------- | +| Top50飙升榜 | rising | +| Top50热搜榜 | hot_search | +| Top50新书榜 | newbook | +| Top50小说榜 | general_novel_rising | +| Top200总榜 | all | +| 神作榜 | newrating_publish | +| 神作潜力榜 | newrating_potential_publish | +| 精品小说 | 100000 | +| 历史 | 200000 | +| 文学 | 300000 | +| 艺术 | 400000 | +| 人物传记 | 500000 | +| 哲学宗教 | 600000 | +| 计算机 | 700000 | +| 心理 | 800000 | +| 社会文化 | 900000 | +| 个人成长 | 1000000 | +| 经济理财 | 1100000 | +| 政治军事 | 1200000 | +| 童书 | 1300000 | +| 教育学习 | 1400000 | +| 科学技术 | 1500000 | +| 生活百科 | 1600000 | +| 期刊杂志 | 1700000 | +| 原版书 | 1800000 | +| 男生小说 | 1900000 | +| 女生小说 | 2000000 | +| 医学健康 | 2100000 | + +还可以分得更细 见 https://weread.qq.com/web/category/100000 的小标题栏 +`, +}; + +async function handler(ctx) { + const category = ctx.req.param('category'); + + // 检查 category 是否是榜单,若否则全部为阿拉伯数字 + const isRank = /^\d+$/.test(category) ? 0 : 1; + + const LIMIT = category === 'all' ? 180 : 40; + const SIZE = 20; + + const urls = Array.from({ length: Math.ceil((LIMIT + 1) / SIZE) }, (_, index) => `https://weread.qq.com/web/bookListInCategory/${category}?maxIndex=${index * SIZE}&rank=${isRank}`); + + const responses = await Promise.all(urls.map((url) => ofetch(url))); + + const results = responses.flatMap((response) => + response.books.map((book) => { + const bookInfo = book.bookInfo; + return { + title: bookInfo.title, + description: `推荐值 ${bookInfo.newRating / 10}% ${bookInfo.newRatingDetail.title}|| ` + bookInfo.intro, + author: bookInfo.author, + guid: bookInfo.bookId, + itunes_item_image: bookInfo.cover, + }; + }) + ); + + const title = categoryTitles[category] || '书籍列表'; + return { + title: '微信读书 - ' + title, + link: `https://weread.qq.com/web/category/${category}`, + item: results, + }; +} + +const categoryTitles = { + rising: 'Top50飙升榜', + hot_search: 'Top50热搜榜', + newbook: 'Top50新书榜', + general_novel_rising: 'Top50小说榜', + all: 'Top200总榜', + newrating_publish: '神作榜', + newrating_potential_publish: '神作潜力榜', + '100000': '精品小说', + '200000': '历史', + '300000': '文学', + '400000': '艺术', + '500000': '人物传记', + '600000': '哲学宗教', + '700000': '计算机', + '800000': '心理', + '900000': '社会文化', + '1000000': '个人成长', + '1100000': '经济理财', + '1200000': '政治军事', + '1300000': '童书', + '1400000': '教育学习', + '1500000': '科学技术', + '1600000': '生活百科', + '1700000': '期刊杂志', + '1800000': '原版书', + '1900000': '男生小说', + '2000000': '女生小说', + '2100000': '医学健康', +}; diff --git a/lib/routes/qq88/index.ts b/lib/routes/qq88/index.ts index 72d352f3ac927d..e02e1e48ff3f86 100644 --- a/lib/routes/qq88/index.ts +++ b/lib/routes/qq88/index.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 首页 | オトナの土ドラ | 日剧 | 日剧 SP | - | ---- | -------------- | ---- | ------- | - | | 10 | 5 | 11 |`, +| ---- | -------------- | ---- | ------- | +| | 10 | 5 | 11 |`, }; async function handler(ctx) { diff --git a/lib/routes/qq88/namespace.ts b/lib/routes/qq88/namespace.ts index 1f9fc0f1c5920f..b1eb47302d20f2 100644 --- a/lib/routes/qq88/namespace.ts +++ b/lib/routes/qq88/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '秋爸日字', url: 'qq88.info', + lang: 'zh-CN', }; diff --git a/lib/routes/qqorw/index.ts b/lib/routes/qqorw/index.ts index 9438026d0c6cc1..b485e7ff0b1587 100644 --- a/lib/routes/qqorw/index.ts +++ b/lib/routes/qqorw/index.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 首页 | 每日早报 | 国际早报 | 生活冷知识 | - | ---- | -------- | -------- | ---------- | - | | mrzb | zbapp | zbzzd |`, +| ---- | -------- | -------- | ---------- | +| | mrzb | zbapp | zbzzd |`, }; async function handler(ctx) { diff --git a/lib/routes/qqorw/namespace.ts b/lib/routes/qqorw/namespace.ts index c1c35d266713d7..df6e11bbcced1c 100644 --- a/lib/routes/qqorw/namespace.ts +++ b/lib/routes/qqorw/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '早报网', url: 'qqorw.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/qstheory/index.ts b/lib/routes/qstheory/index.ts new file mode 100644 index 00000000000000..9af215f31f25af --- /dev/null +++ b/lib/routes/qstheory/index.ts @@ -0,0 +1,121 @@ +import { Route } from '@/types'; + +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import cache from '@/utils/cache'; +import { baseUrl as rootUrl, getItem } from './utils'; + +const config = { + toutiao: { + title: '头条', + url: `${rootUrl}/v9zhuanqu/toutiao/index.htm`, + }, + qswp: { + title: '网评', + url: `${rootUrl}/qswp.htm`, + }, + qssp: { + title: '视频', + url: `${rootUrl}/qssp/index.htm`, + }, + qslgxd: { + title: '原创', + url: `${rootUrl}/qslgxd/index.htm`, + }, + economy: { + title: '经济', + url: `${rootUrl}/economy/index.htm`, + }, + politics: { + title: '政治', + url: `${rootUrl}/politics/index.htm`, + }, + culture: { + title: '文化', + url: `${rootUrl}/culture/index.htm`, + }, + society: { + title: '社会', + url: `${rootUrl}/society/index.htm`, + }, + cpc: { + title: '党建', + url: `${rootUrl}/cpc/index.htm`, + }, + science: { + title: '科教', + url: `${rootUrl}/science/index.htm`, + }, + zoology: { + title: '生态', + url: `${rootUrl}/zoology/index.htm`, + }, + defense: { + title: '国防', + url: `${rootUrl}/defense/index.htm`, + }, + international: { + title: '国际', + url: `${rootUrl}/international/index.htm`, + }, + books: { + title: '图书', + url: `${rootUrl}/books/index.htm`, + }, + xxbj: { + title: '学习笔记', + url: `${rootUrl}/qszq/xxbj/index.htm`, + }, + llwx: { + title: '理论文选', + url: `${rootUrl}/qszq/llwx/index.htm`, + }, +}; + +export const route: Route = { + path: '/:category?', + categories: ['traditional-media'], + example: '/qstheory', + parameters: { industry: '分类,见下表' }, + radar: [ + { + source: ['www.qstheory.cn/v9zhuanqu/:category/index.htm', 'www.qstheory.cn/qszq/:category/index.htm', 'www.qstheory.cn/:category/index.htm'], + }, + ], + name: '分类', + maintainers: ['nczitzk'], + handler, + description: ` +| 头条 | 网评 | 视频 | 原创 | 经济 | 政治 | 文化 | 社会 | 党建 | 科教 | 生态 | 国防 | 国际 | 图书 | 学习笔记 | 理论文选 | +| ------- | ---- | ---- | ------ | ------- | -------- | ------- | ------- | ---- | ------- | ------- | ------- | ------------- | ----- | -------- | -------- | +| toutiao | qswp | qssp | qslgxd | economy | politics | culture | society | cpc | science | zoology | defense | international | books | xxbj | llwx |`, +}; + +async function handler(ctx) { + const { category = 'toutiao' } = ctx.req.param(); + const limit = Number.parseInt(ctx.req.query('limit')) || 50; + + const currentUrl = config[category].url; + const response = await ofetch(currentUrl); + + const $ = cheerio.load(response); + + const list = $('.list-style1 ul li a, .text h2 a, .no-pic ul li a') + .slice(0, limit) + .toArray() + .map((item) => { + const $item = $(item); + return { + title: $item.text(), + link: $item.attr('href')!, + }; + }); + + const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => getItem(item)))); + + return { + title: $('title').text(), + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/qstheory/magazine.ts b/lib/routes/qstheory/magazine.ts new file mode 100644 index 00000000000000..4096ddd493d07c --- /dev/null +++ b/lib/routes/qstheory/magazine.ts @@ -0,0 +1,64 @@ +import { Route } from '@/types'; + +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import cache from '@/utils/cache'; +import { baseUrl, getItem } from './utils'; + +export const route: Route = { + path: '/magazine/:magazine', + categories: ['traditional-media'], + example: '/qstheory/magazine/qs', + parameters: { magazine: '刊物,`qs` 为求是,`hqwglist` 为红旗文稿' }, + radar: [ + { + source: ['www.qstheory.cn/:magazine/mulu.htm'], + }, + ], + name: '在线读刊', + maintainers: ['TonyRL', 'cscnk52'], + handler, +}; + +async function handler(ctx) { + const { magazine } = ctx.req.param(); + + const link = `${baseUrl}/${magazine}/mulu.htm`; + const yearResponse = await ofetch(link); + + const $ = cheerio.load(yearResponse); + + const yearList = $('.booktitle a') + .toArray() + .map((item) => { + const $item = $(item); + return { + title: $item.text(), + link: new URL($item.attr('href')!, baseUrl).href, + }; + }); + + const issueResponse = await ofetch(yearList[0].link); + const $$ = cheerio.load(issueResponse); + + const list = $$('.highlight a') + .toArray() + .map((item) => { + const $item = $$(item); + return { + title: $item.text(), + link: $item.attr('href')!, + }; + }) + .toReversed() + .filter((item) => item.title); + + const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => getItem(item)))); + + return { + title: $('head title').text(), + link, + image: new URL($('.book img').attr('src')!, link).href, + item: items, + }; +} diff --git a/lib/routes/qstheory/namespace.ts b/lib/routes/qstheory/namespace.ts new file mode 100644 index 00000000000000..80ea898cb8b66f --- /dev/null +++ b/lib/routes/qstheory/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '求是网', + url: 'www.qstheory.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/qstheory/utils.ts b/lib/routes/qstheory/utils.ts new file mode 100644 index 00000000000000..08878d9c3aa2cb --- /dev/null +++ b/lib/routes/qstheory/utils.ts @@ -0,0 +1,25 @@ +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +export const baseUrl = 'http://www.qstheory.cn'; + +export const getItem = async (item) => { + const response = await ofetch(item.link); + const $ = cheerio.load(response); + + $('.fs-text, .fs-pinglun, .hidden-xs').remove(); + + item.author = $('.appellation').text(); + item.description = $('.highlight, .text').html() || $('.content').html(); + item.pubDate = parseDate( + $('.puttime_mobi, .pubtime, .headtitle span') + .text() + .trim() + .replace('发表于', '') + .replaceAll(/(年|月)/g, '-') + .replace('日', '') + ); + + return item; +}; diff --git a/lib/routes/questmobile/namespace.ts b/lib/routes/questmobile/namespace.ts index 5a9cac1bba8bb9..54b3536a138b0f 100644 --- a/lib/routes/questmobile/namespace.ts +++ b/lib/routes/questmobile/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'QuestMobile', url: 'questmobile.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/questmobile/report.ts b/lib/routes/questmobile/report.ts index 75f07bb97f8491..7b5ff1e0eb0ac2 100644 --- a/lib/routes/questmobile/report.ts +++ b/lib/routes/questmobile/report.ts @@ -31,7 +31,7 @@ const parseTree = (tree, result = []) => { export const route: Route = { path: '/report/:industry?/:label?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/questmobile/report', parameters: { industry: '行业,见下表,默认为 `-1`,即全部行业', label: '标签,见下表,默认为 `-1`,即全部标签' }, features: { @@ -45,7 +45,7 @@ export const route: Route = { name: '行业研究报告', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 若订阅行业 [互联网行业](https://www.questmobile.com.cn/research/reports/1/-1),网址为 \`https://www.questmobile.com.cn/research/reports/1/-1\` 参数 industry 为 \`互联网行业\` 或 \`1\`,此时路由为 [\`/questmobile/report/互联网行业\`](https://rsshub.app/questmobile/report/互联网行业) 或 [\`/questmobile/report/1/-1\`](https://rsshub.app/questmobile/report/1/-1)。 @@ -54,107 +54,107 @@ export const route: Route = { 若订阅行业和标签 [品牌领域 - 互联网经济](https://www.questmobile.com.cn/research/reports/2/1),网址为 \`https://www.questmobile.com.cn/research/reports/2/1\` 参数 industry 为 \`品牌领域\` 或 \`2\`,参数 label 为 \`互联网经济\` 或 \`1\`,此时路由为 [\`/questmobile/report/品牌领域/互联网经济\`](https://rsshub.app/questmobile/report/品牌领域/互联网经济) 或 [\`/questmobile/report/2/1\`](https://rsshub.app/questmobile/report/2/1),甚至 [\`/questmobile/report/品牌领域/1\`](https://rsshub.app/questmobile/report/品牌领域/1)。 - ::: +::: - <details> - <summary>全部行业和标签</summary> +<details> +<summary>全部行业和标签</summary> - #### 行业 +#### 行业 - | 互联网行业 | 移动社交 | 移动视频 | 移动购物 | 系统工具 | - | ---------- | -------- | -------- | -------- | -------- | - | 1 | 1001 | 1002 | 1003 | 1004 | +| 互联网行业 | 移动社交 | 移动视频 | 移动购物 | 系统工具 | +| ---------- | -------- | -------- | -------- | -------- | +| 1 | 1001 | 1002 | 1003 | 1004 | - | 出行服务 | 金融理财 | 生活服务 | 移动音乐 | 新闻资讯 | - | -------- | -------- | -------- | -------- | -------- | - | 1005 | 1006 | 1007 | 1008 | 1009 | +| 出行服务 | 金融理财 | 生活服务 | 移动音乐 | 新闻资讯 | +| -------- | -------- | -------- | -------- | -------- | +| 1005 | 1006 | 1007 | 1008 | 1009 | - | 办公商务 | 手机游戏 | 实用工具 | 数字阅读 | 教育学习 | - | -------- | -------- | -------- | -------- | -------- | - | 1010 | 1011 | 1012 | 1013 | 1014 | +| 办公商务 | 手机游戏 | 实用工具 | 数字阅读 | 教育学习 | +| -------- | -------- | -------- | -------- | -------- | +| 1010 | 1011 | 1012 | 1013 | 1014 | - | 汽车服务 | 拍摄美化 | 智能设备 | 旅游服务 | 健康美容 | - | -------- | -------- | -------- | -------- | -------- | - | 1015 | 1016 | 1017 | 1018 | 1020 | +| 汽车服务 | 拍摄美化 | 智能设备 | 旅游服务 | 健康美容 | +| -------- | -------- | -------- | -------- | -------- | +| 1015 | 1016 | 1017 | 1018 | 1020 | - | 育儿母婴 | 主题美化 | 医疗服务 | 品牌领域 | 美妆品牌 | - | -------- | -------- | -------- | -------- | -------- | - | 1022 | 1023 | 1024 | 2 | 2001 | +| 育儿母婴 | 主题美化 | 医疗服务 | 品牌领域 | 美妆品牌 | +| -------- | -------- | -------- | -------- | -------- | +| 1022 | 1023 | 1024 | 2 | 2001 | - | 母婴品牌 | 家电品牌 | 食品饮料品牌 | 汽车品牌 | 服饰箱包品牌 | - | -------- | -------- | ------------ | -------- | ------------ | - | 2002 | 2003 | 2004 | 2005 | 2006 | +| 母婴品牌 | 家电品牌 | 食品饮料品牌 | 汽车品牌 | 服饰箱包品牌 | +| -------- | -------- | ------------ | -------- | ------------ | +| 2002 | 2003 | 2004 | 2005 | 2006 | - #### 标签 +#### 标签 - | 互联网经济 | 圈层经济 | 粉丝经济 | 银发经济 | 儿童经济 | - | ---------- | -------- | -------- | -------- | -------- | - | 1 | 1001 | 1002 | 1004 | 1005 | +| 互联网经济 | 圈层经济 | 粉丝经济 | 银发经济 | 儿童经济 | +| ---------- | -------- | -------- | -------- | -------- | +| 1 | 1001 | 1002 | 1004 | 1005 | - | 萌宠经济 | 她经济 | 他经济 | 泛娱乐经济 | 下沉市场经济 | - | -------- | ------ | ------ | ---------- | ------------ | - | 1007 | 1009 | 1010 | 1011 | 1012 | +| 萌宠经济 | 她经济 | 他经济 | 泛娱乐经济 | 下沉市场经济 | +| -------- | ------ | ------ | ---------- | ------------ | +| 1007 | 1009 | 1010 | 1011 | 1012 | - | 内容经济 | 订阅经济 | 会员经济 | 居家经济 | 到家经济 | - | -------- | -------- | -------- | -------- | -------- | - | 1013 | 1014 | 1015 | 1016 | 1017 | +| 内容经济 | 订阅经济 | 会员经济 | 居家经济 | 到家经济 | +| -------- | -------- | -------- | -------- | -------- | +| 1013 | 1014 | 1015 | 1016 | 1017 | - | 颜值经济 | 闲置经济 | 旅游经济 | 人群洞察 | 00 后 | - | -------- | -------- | ------------------- | -------- | ----- | - | 1018 | 1020 | 1622842051677753346 | 2 | 2002 | +| 颜值经济 | 闲置经济 | 旅游经济 | 人群洞察 | 00 后 | +| -------- | -------- | ------------------- | -------- | ----- | +| 1018 | 1020 | 1622842051677753346 | 2 | 2002 | - | Z 世代 | 银发族 | 宝妈宝爸 | 萌宠人群 | 运动达人 | - | ------ | ------ | -------- | -------- | -------- | - | 2003 | 2004 | 2005 | 2007 | 2008 | +| Z 世代 | 银发族 | 宝妈宝爸 | 萌宠人群 | 运动达人 | +| ------ | ------ | -------- | -------- | -------- | +| 2003 | 2004 | 2005 | 2007 | 2008 | - | 女性消费 | 男性消费 | 游戏人群 | 二次元 | 新中产 | - | -------- | -------- | -------- | ------ | ------ | - | 2009 | 2010 | 2012 | 2013 | 2014 | +| 女性消费 | 男性消费 | 游戏人群 | 二次元 | 新中产 | +| -------- | -------- | -------- | ------ | ------ | +| 2009 | 2010 | 2012 | 2013 | 2014 | - | 下沉市场用户 | 大学生 | 数字化营销 | 广告效果 | 品牌营销 | - | ------------ | ------ | ---------- | -------- | -------- | - | 2018 | 2022 | 3 | 3001 | 3002 | +| 下沉市场用户 | 大学生 | 数字化营销 | 广告效果 | 品牌营销 | +| ------------ | ------ | ---------- | -------- | -------- | +| 2018 | 2022 | 3 | 3001 | 3002 | - | 全域营销 | 私域流量 | 新媒体营销 | KOL 生态 | 内容营销 | - | -------- | -------- | ---------- | -------- | -------- | - | 3003 | 3004 | 3005 | 3006 | 3008 | +| 全域营销 | 私域流量 | 新媒体营销 | KOL 生态 | 内容营销 | +| -------- | -------- | ---------- | -------- | -------- | +| 3003 | 3004 | 3005 | 3006 | 3008 | - | 直播电商 | 短视频带货 | 娱乐营销 | 营销热点 | 双 11 电商大促 | - | -------- | ---------- | ------------------- | -------- | -------------- | - | 3009 | 3010 | 1630464311158738945 | 4 | 4001 | +| 直播电商 | 短视频带货 | 娱乐营销 | 营销热点 | 双 11 电商大促 | +| -------- | ---------- | ------------------- | -------- | -------------- | +| 3009 | 3010 | 1630464311158738945 | 4 | 4001 | - | 618 电商大促 | 春节营销 | 五一假期营销 | 热点事件盘点 | 消费热点 | - | ------------ | -------- | ------------ | ------------ | -------- | - | 4002 | 4003 | 4004 | 4007 | 5 | +| 618 电商大促 | 春节营销 | 五一假期营销 | 热点事件盘点 | 消费热点 | +| ------------ | -------- | ------------ | ------------ | -------- | +| 4002 | 4003 | 4004 | 4007 | 5 | - | 时尚品牌 | 连锁餐饮 | 新式茶饮 | 智能家电 | 国潮品牌 | - | -------- | -------- | -------- | -------- | -------- | - | 5001 | 5002 | 5003 | 5004 | 5007 | +| 时尚品牌 | 连锁餐饮 | 新式茶饮 | 智能家电 | 国潮品牌 | +| -------- | -------- | -------- | -------- | -------- | +| 5001 | 5002 | 5003 | 5004 | 5007 | - | 白酒品牌 | 精益运营 | 媒介策略 | 用户争夺 | 精细化运营 | - | ------------------- | -------- | -------- | -------- | ---------- | - | 1622841828310093825 | 6 | 6001 | 6002 | 6003 | +| 白酒品牌 | 精益运营 | 媒介策略 | 用户争夺 | 精细化运营 | +| ------------------- | -------- | -------- | -------- | ---------- | +| 1622841828310093825 | 6 | 6001 | 6002 | 6003 | - | 用户分层 | 增长黑马 | 社交裂变 | 新兴领域 | 新能源汽车 | - | -------- | -------- | -------- | -------- | ---------- | - | 6004 | 6005 | 6007 | 7 | 7001 | +| 用户分层 | 增长黑马 | 社交裂变 | 新兴领域 | 新能源汽车 | +| -------- | -------- | -------- | -------- | ---------- | +| 6004 | 6005 | 6007 | 7 | 7001 | - | 智能汽车 | 新消费 | AIoT | 产业互联网 | AIGC | - | -------- | ------ | ---- | ---------- | ------------------- | - | 7002 | 7003 | 7004 | 7005 | 1645677998450511873 | +| 智能汽车 | 新消费 | AIoT | 产业互联网 | AIGC | +| -------- | ------ | ---- | ---------- | ------------------- | +| 7002 | 7003 | 7004 | 7005 | 1645677998450511873 | - | OTT 应用 | 智能电视 | 全景数据 | 全景生态 | 微信小程序 | - | ------------------- | ------------------- | -------- | -------- | ---------- | - | 1676063510499528705 | 1676063630293045249 | 8 | 8001 | 8002 | +| OTT 应用 | 智能电视 | 全景数据 | 全景生态 | 微信小程序 | +| ------------------- | ------------------- | -------- | -------- | ---------- | +| 1676063510499528705 | 1676063630293045249 | 8 | 8001 | 8002 | - | 支付宝小程序 | 百度智能小程序 | 企业流量 | 抖音小程序 | 手机终端 | - | ------------ | -------------- | ------------------- | ------------------- | -------- | - | 8003 | 8004 | 1671052842096496642 | 1676063017220018177 | 9 | +| 支付宝小程序 | 百度智能小程序 | 企业流量 | 抖音小程序 | 手机终端 | +| ------------ | -------------- | ------------------- | ------------------- | -------- | +| 8003 | 8004 | 1671052842096496642 | 1676063017220018177 | 9 | - | 智能终端 | 国产终端 | 5G 手机 | 盘点 | 季度报告 | - | -------- | -------- | ------- | ---- | -------- | - | 9001 | 9002 | 9003 | 10 | 10001 | - </details>`, +| 智能终端 | 国产终端 | 5G 手机 | 盘点 | 季度报告 | +| -------- | -------- | ------- | ---- | -------- | +| 9001 | 9002 | 9003 | 10 | 10001 | +</details>`, }; async function handler(ctx) { @@ -175,7 +175,7 @@ async function handler(ctx) { const labels = parseTree(labelTree); const industryObj = industry ? industries.find((i) => i.key === industry || i.value === industry) : undefined; - const labelObj = label ? labels.find((i) => i.key === label || i.value === label) : industryObj ? undefined : labels.find((i) => i.key === industry || i.value === industry); + const labelObj = label ? labels.find((i) => i.key === label || i.value === label) : (industryObj ? undefined : labels.find((i) => i.key === industry || i.value === industry)); const industryId = industryObj?.key ?? -1; const labelId = labelObj?.key ?? -1; diff --git a/lib/routes/questn/community.ts b/lib/routes/questn/community.ts new file mode 100644 index 00000000000000..f6094e8ebf091e --- /dev/null +++ b/lib/routes/questn/community.ts @@ -0,0 +1,76 @@ +import { Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/community/:communityUrl', + name: 'Community Events', + url: 'app.questn.com', + maintainers: ['cxheng315'], + example: '/questn/community/gmnetwork', + parameters: { + community_url: 'Community URL', + }, + categories: ['other'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['app.questn.com/:communityUrl'], + target: '/community/:communityUrl', + }, + ], + handler, +}; + +async function handler(ctx) { + const url = 'https://api.questn.com/consumer/explore/entity_list/'; + + const params = { + count: ctx.req.query('limit') || '20', + page: '1', + community_url: ctx.req.param('communityUrl') || 'questn', // default to questn + }; + + const response = await ofetch(`${url}?${new URLSearchParams(params)}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.result.data; + + const items = data.map((item) => ({ + title: item.title, + link: `https://app.questn.com/quest/${item.id}`, + author: item.community_info ? item.community_info.name : '', + guid: item.id, + pubDate: parseDate(item.start_time * 1000), + itunes_duration: item.end_time > 0 ? item.end_time - item.start_time : 0, + })); + + return { + title: `QuestN Community - ${data[0].community_info ? data[0].community_info.name : ''} Events`, + link: `https://app.questn.com/${ctx.req.param('community_url')}`, + description: data[0].community_info ? data[0].community_info.introduction : '', + image: data[0].community_info ? data[0].community_info.logo : '', + logo: data[0].community_info ? data[0].community_info.logo : '', + item: + items && items.length > 0 + ? items + : [ + { + title: 'No events found', + link: `https://app.questn.com/${ctx.req.param('community_url')}`, + description: 'No events found', + }, + ], + }; +} diff --git a/lib/routes/questn/events.ts b/lib/routes/questn/events.ts new file mode 100644 index 00000000000000..74ad6298b170e7 --- /dev/null +++ b/lib/routes/questn/events.ts @@ -0,0 +1,101 @@ +import { Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; + +import { parseFilterStr } from './util'; + +export const route: Route = { + path: '/events/:filter?', + name: 'Events', + url: 'app.questn.com', + maintainers: ['cxheng315'], + example: '/questn/events', + parameters: { + filter: 'Filter string', + }, + description: ` +::: tip + +Filter parameters: +- category: 100: trending, 200: newest, 300: top +- status_filter: 0: all, 100: available, 400: missed +- community_filter: 0: all community, 100: verified, 200: followed +- rewards_filter: 0: all rewards, 100: nft, 200: token, 400: whitelist +- chain_filter: 0: all chains, 1: ethereum, 56: bsc, 137: polygon, 42161: arb, 10: op, 324: zksync, 43114: avax +- search: 'Search keyword', +- count: 'Number of events to fetch', +- page: 'Page number', +:::`, + categories: ['other'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['app.questn.com/explore'], + target: '/events/:category?/:status_filter?/:community_filter?/:reward_filter?/:chain_filter?/:search?/:count?/:page?', + }, + ], + handler, +}; + +async function handler(ctx) { + const url = 'https://api.questn.com/consumer/explore/list/'; + + const parsedFilter: { category?: string; status_filter?: string; community_filter?: string; reward_filter?: string; chain_filter?: string; search?: string; count?: string; page?: string } = parseFilterStr( + ctx.req.param('filter') + ); + + const params = { + category: parsedFilter.category || '200', + status_filter: parsedFilter.status_filter || '100', + community_filter: parsedFilter.community_filter || '0', + rewards_filter: parsedFilter.reward_filter || '0', + chain_filter: parsedFilter.chain_filter || '0', + search: parsedFilter.search || '', + count: parsedFilter.count || ctx.req.query('limit') || '20', + page: parsedFilter.page || '1', + }; + + const response = await ofetch(`${url}?${new URLSearchParams(params)}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.result.data; + + const items = data.map((item) => ({ + title: item.title, + link: `https://app.questn.com/quest/${item.id}`, + author: item.community_info ? item.community_info.name : '', + guid: item.id, + pubDate: parseDate(item.start_time * 1000), + itunes_duration: item.end_time > 0 ? item.end_time - item.start_time : 0, + })); + + return { + title: 'QuestN Events', + link: 'https://app.questn.com/explore', + description: 'A Quest Protocol Dedicated to DePIN and AI Training', + image: 'https://app.questn.com/static/svgs/logo-white.svg', + logo: 'https://app.questn.com/static/svgs/logo-white.svg', + author: 'QuestN', + item: + items && items.length > 0 + ? items + : [ + { + title: 'No events found', + description: 'No events found', + link: 'https://app.questn.com/explore', + }, + ], + }; +} diff --git a/lib/routes/questn/namespace.ts b/lib/routes/questn/namespace.ts new file mode 100644 index 00000000000000..b25383efcf4ce2 --- /dev/null +++ b/lib/routes/questn/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'QuestN', + url: 'app.questn.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/questn/util.ts b/lib/routes/questn/util.ts new file mode 100644 index 00000000000000..1a09f1de7938df --- /dev/null +++ b/lib/routes/questn/util.ts @@ -0,0 +1,16 @@ +const parseFilterStr = (filterStr) => { + const filters = {}; + if (!filterStr) { + return filters; + } + const filterPairs = filterStr.split('&'); // Split by '&' + + for (const pair of filterPairs) { + const [key, value] = pair.split('='); // Split by '=' + filters[key] = value; + } + + return filters; +}; + +export { parseFilterStr }; diff --git a/lib/routes/quicker/namespace.ts b/lib/routes/quicker/namespace.ts index f507523cf9e14c..8bada9687e0693 100644 --- a/lib/routes/quicker/namespace.ts +++ b/lib/routes/quicker/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Quicker', url: 'getquicker.net', + lang: 'zh-CN', }; diff --git a/lib/routes/quicker/qa.ts b/lib/routes/quicker/qa.ts index c99584e653704c..4f1b418d181095 100644 --- a/lib/routes/quicker/qa.ts +++ b/lib/routes/quicker/qa.ts @@ -23,23 +23,23 @@ export const route: Route = { handler, description: `分类 - | 使用问题 | 动作开发 | BUG 反馈 | 功能建议 | - | -------- | -------- | -------- | -------- | - | 1 | 9 | 3 | 4 | +| 使用问题 | 动作开发 | BUG 反馈 | 功能建议 | +| -------- | -------- | -------- | -------- | +| 1 | 9 | 3 | 4 | - | 动作需求 | 经验创意 | 动作推荐 | 信息发布 | - | -------- | -------- | -------- | -------- | - | 6 | 2 | 7 | 5 | +| 动作需求 | 经验创意 | 动作推荐 | 信息发布 | +| -------- | -------- | -------- | -------- | +| 6 | 2 | 7 | 5 | - | 随便聊聊 | 异常报告 | 全部 | - | -------- | -------- | ---- | - | 8 | 10 | all | +| 随便聊聊 | 异常报告 | 全部 | +| -------- | -------- | ---- | +| 8 | 10 | all | 状态 - | 全部 | 精华 | 已归档 | - | ---- | ------ | ------- | - | | digest | achived |`, +| 全部 | 精华 | 已归档 | +| ---- | ------ | ------- | +| | digest | achived |`, }; async function handler(ctx) { diff --git a/lib/routes/quicker/share.ts b/lib/routes/quicker/share.ts index 15fa089921b189..f8a73ddef6481e 100644 --- a/lib/routes/quicker/share.ts +++ b/lib/routes/quicker/share.ts @@ -27,12 +27,12 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 动作库最新更新 | 动作库最多赞 | 动作库新动作 | 动作库最近赞 | - | -------------- | ------------ | ------------ | ------------ | - | Recent | Recommended | NewActions | RecentLiked | +| -------------- | ------------ | ------------ | ------------ | +| Recent | Recommended | NewActions | RecentLiked | - | 子程序 | 扩展热键 | 文本指令 | - | ----------- | --------- | ------------ | - | SubPrograms | PowerKeys | TextCommands |`, +| 子程序 | 扩展热键 | 文本指令 | +| ----------- | --------- | ------------ | +| SubPrograms | PowerKeys | TextCommands |`, }; async function handler(ctx) { diff --git a/lib/routes/quicker/user.ts b/lib/routes/quicker/user.ts index ee987652bfa0fc..25a6a526f4fe84 100644 --- a/lib/routes/quicker/user.ts +++ b/lib/routes/quicker/user.ts @@ -22,8 +22,8 @@ export const route: Route = { maintainers: ['Cesaryuan', 'nczitzk'], handler, description: `| 动作 | 子程序 | 动作单 | - | ------- | ----------- | ----------- | - | Actions | SubPrograms | ActionLists |`, +| ------- | ----------- | ----------- | +| Actions | SubPrograms | ActionLists |`, }; async function handler(ctx) { diff --git a/lib/routes/qust/namespace.ts b/lib/routes/qust/namespace.ts index 41d66ab155b20d..c4228b95b966a9 100644 --- a/lib/routes/qust/namespace.ts +++ b/lib/routes/qust/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '青岛科技大学', url: 'jw.qust.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/qweather/3days.ts b/lib/routes/qweather/3days.ts index d077dde84f7d76..7602d89d493994 100644 --- a/lib/routes/qweather/3days.ts +++ b/lib/routes/qweather/3days.ts @@ -35,19 +35,20 @@ export const route: Route = { name: '近三天天气', maintainers: ['Rein-Ou', 'la3rence'], handler, - description: `需自行注册获取 api 的 key,并在环境变量 HEFENG\_KEY 中进行配置,获取订阅近三天天气预报`, + description: '获取订阅近三天天气预报', }; async function handler(ctx) { if (!config.hefeng.key) { throw new ConfigNotFoundError('QWeather RSS is disabled due to the lack of <a href="https://docs.rsshub.app/zh/install/config#%E5%92%8C%E9%A3%8E%E5%A4%A9%E6%B0%94">relevant config</a>'); } - const id = await cache.tryGet(ctx.req.param('location') + '_id', async () => { + + const id = await cache.tryGet('qweather:' + ctx.req.param('location') + ':id', async () => { const response = await got(`${CIRY_LOOKUP_API}?location=${ctx.req.param('location')}&key=${config.hefeng.key}`); return response.data.location[0].id; }); const weatherData = await cache.tryGet( - ctx.req.param('location'), + 'qweather:' + ctx.req.param('location'), async () => { const response = await got(`${WEATHER_API}?key=${config.hefeng.key}&location=${id}`); return response.data; diff --git a/lib/routes/qweather/namespace.ts b/lib/routes/qweather/namespace.ts index f2504a208972b3..4f803c1cbee555 100644 --- a/lib/routes/qweather/namespace.ts +++ b/lib/routes/qweather/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '和风天气', url: 'qweather.com', + lang: 'zh-CN', }; diff --git a/lib/routes/qweather/now.ts b/lib/routes/qweather/now.ts index c73e227b53bd9c..4233562d7fcc7c 100644 --- a/lib/routes/qweather/now.ts +++ b/lib/routes/qweather/now.ts @@ -8,17 +8,20 @@ import { art } from '@/utils/render'; import path from 'node:path'; import { parseDate } from '@/utils/parse-date'; import { config } from '@/config'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; + const rootUrl = 'https://devapi.qweather.com/v7/weather/now?'; + export const route: Route = { path: '/now/:location', categories: ['forecast'], - example: '/qweather/广州', + example: '/qweather/now/广州', parameters: { location: 'N' }, features: { requireConfig: [ { name: 'HEFENG_KEY', - description: '', + description: '访问 `https://www.qweather.com/` 注册开发 API Key。', }, ], requirePuppeteer: false, @@ -30,21 +33,21 @@ export const route: Route = { name: '实时天气', maintainers: ['Rein-Ou'], handler, - description: `需自行注册获取 api 的 key,每小时更新一次数据`, }; async function handler(ctx) { - const id = await cache.tryGet(ctx.req.param('location') + '_id', async () => { + if (!config.hefeng.key) { + throw new ConfigNotFoundError('QWeather RSS is disabled due to the lack of <a href="https://docs.rsshub.app/zh/install/config#%E5%92%8C%E9%A3%8E%E5%A4%A9%E6%B0%94">relevant config</a>'); + } + + const id = await cache.tryGet('qweather:' + ctx.req.param('location') + ':id', async () => { const response = await got(`https://geoapi.qweather.com/v2/city/lookup?location=${ctx.req.param('location')}&key=${config.hefeng.key}`); - const data = []; - for (const i in response.data.location) { - data.push(response.data.location[i]); - } + const data = response.data.location.map((loc) => loc); return data[0].id; }); const requestUrl = rootUrl + 'key=' + config.hefeng.key + '&location=' + id; const responseData = await cache.tryGet( - ctx.req.param('location') + '_now', + 'qweather:' + ctx.req.param('location') + ':now', async () => { const response = await got(requestUrl); return response.data; diff --git a/lib/routes/qztc/home/index.ts b/lib/routes/qztc/home/index.ts new file mode 100644 index 00000000000000..872255795c8f3a --- /dev/null +++ b/lib/routes/qztc/home/index.ts @@ -0,0 +1,100 @@ +import { Data, Route } from '@/types'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import timezone from '@/utils/timezone'; + +const rootUrl = 'https://www.qztc.edu.cn/'; + +export const route: Route = { + path: '/home/:type', + categories: ['university'], + example: '/qztc/home/2093', + parameters: { type: '分类,见下表' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '首页', + maintainers: ['iQNRen'], + url: 'www.qztc.edu.cn', + handler, + radar: [ + { + source: ['www.qztc.edu.cn/:type/list.htm'], + target: '/home/:type', + }, + ], + description: `| 板块 | 参数 | +| ------- | ------- | +| 泉师新闻 | 2093 | +| 通知公告 | 2094 | +| 采购公告 | 2095 | +| 学术资讯 | xszx | +| 招聘信息 | 2226 | +`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type'); + // const type = Number.parseInt(ctx.req.param('type')); + const response = await ofetch(rootUrl + type + '/list.htm'); + const $ = load(response); + + const list = $('.news.clearfix') + .toArray() + .map((item) => { + const cheerioItem = $(item); + const a = cheerioItem.find('a'); + + try { + const title = a.attr('title') || ''; + let link = a.attr('href'); + if (!link) { + link = ''; + } else if (!link.startsWith('http')) { + link = rootUrl.slice(0, -1) + link; + } + const pubDate = timezone(parseDate(cheerioItem.find('.news_meta').text()), +8); + + return { + title, + link, + pubDate, + }; + } catch { + return { + title: '', + link: '', + pubDate: Date.now(), + }; + } + }) + .filter((item) => item.title && item.link); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const newItem = { + ...item, + description: '', + }; + const response = await ofetch(item.link); + const $ = load(response); + newItem.description = $('.wp_articlecontent').html() || ''; + return newItem; + }) + ) + ); + + return { + title: $('head > title').text() + ' - 泉州师范学院-首页', + link: rootUrl + type + '/list.htm', + item: items, + } as Data; +} diff --git a/lib/routes/qztc/jwc/index.ts b/lib/routes/qztc/jwc/index.ts new file mode 100644 index 00000000000000..e536f3053dd44b --- /dev/null +++ b/lib/routes/qztc/jwc/index.ts @@ -0,0 +1,120 @@ +import { Data, Route } from '@/types'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import timezone from '@/utils/timezone'; + +const rootUrl = 'https://www.qztc.edu.cn/jwc/'; +const host = 'www.qztc.edu.cn'; + +export const route: Route = { + path: '/jwc/:type', + categories: ['university'], + example: '/qztc/jwc/jwdt', + parameters: { type: '分类,见下表' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '教务处', + maintainers: ['iQNRen'], + url: 'www.qztc.edu.cn', + handler, + radar: [ + { + source: ['www.qztc.edu.cn/jwc/:type/list.htm'], + target: '/jwc/:type', + }, + ], + description: `| 板块 | 参数 | +| ------- | ------- | +| 教务动态 | jwdt | +| 首 页 | 1020 | +| 岗位介绍 | 1021 | +| 管理文件 | 1022 | +| 教学教改 | 1023 | +| 办事指南 | 1024 | +| 通知公告 | 1025 | +| 下载中心 | 1026 | +| 对外交流 | 1027 | +| 政策文件 | 1028 | +| 会议纪要 | 1029 | +`, + // | 学院简介 | 1949 | + // | 学院领导 | 1950 | + // | 组织机构 | 1951 | +}; + +async function handler(ctx) { + const type = ctx.req.param('type'); + // const type = Number.parseInt(ctx.req.param('type')); + const response = await ofetch(rootUrl + type + '/list.htm'); + const $ = load(response); + + const list = $('.news.clearfix') + .toArray() + .map((item) => { + const cheerioItem = $(item); + const a = cheerioItem.find('a'); + + try { + const title = a.attr('title') || ''; + let link = a.attr('href'); + if (!link) { + link = ''; + } else if (!link.startsWith('http')) { + link = rootUrl.slice(0, -1) + link; + } + const pubDate = timezone(parseDate(cheerioItem.find('.news_meta').text()), +8); + + return { + title, + link, + pubDate, + }; + } catch { + return { + title: '', + link: '', + pubDate: Date.now(), + }; + } + }) + .filter((item) => item.title && item.link); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const newItem = { + ...item, + description: '', + }; + if (host === new URL(item.link).hostname) { + if (new URL(item.link).pathname.startsWith('/_upload')) { + // 链接为一个文件,直接返回链接 + newItem.description = item.link; + } else { + const response = await ofetch(item.link); + const $ = load(response); + newItem.description = $('.wp_articlecontent').html() || ''; + } + } else { + // 涉及到其他站点,不方便做统一的 html 解析,直接返回链接 + newItem.description = item.link; + } + return newItem; + }) + ) + ); + + return { + title: $('head > title').text() + ' - 泉州师范学院-教务处', + link: rootUrl + type + '/list.htm', + item: items, + } as Data; +} diff --git a/lib/routes/qztc/namespace.ts b/lib/routes/qztc/namespace.ts new file mode 100644 index 00000000000000..199c5401dc1e17 --- /dev/null +++ b/lib/routes/qztc/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '泉州师范学院', + url: 'www.qztc.edu.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/qztc/sjxy/index.ts b/lib/routes/qztc/sjxy/index.ts new file mode 100644 index 00000000000000..0cea3be85b6024 --- /dev/null +++ b/lib/routes/qztc/sjxy/index.ts @@ -0,0 +1,120 @@ +import { Data, Route } from '@/types'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import timezone from '@/utils/timezone'; + +const rootUrl = 'https://www.qztc.edu.cn/sjxy/'; +const host = 'www.qztc.edu.cn'; + +export const route: Route = { + path: '/sjxy/:type', + categories: ['university'], + example: '/qztc/sjxy/1939', + parameters: { type: '分类,见下表' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '数学与计算机科学学院 软件学院', + maintainers: ['iQNRen'], + url: 'www.qztc.edu.cn', + handler, + radar: [ + { + source: ['www.qztc.edu.cn/sjxy/:type/list.htm'], + target: '/sjxy/:type', + }, + ], + description: `| 板块 | 参数 | +| ------- | ------- | +| 学院概况 | 1938 | +| 学院动态 | 1939 | +| 学科建设 | 1940 | +| 教学教务 | 1941 | +| 人才培养 | 1942 | +| 科研工作 | 1943 | +| 党群工作 | 1944 | +| 团学工作 | 1945 | +| 资料下载 | 1947 | +| 采购信息 | 1948 | +| 信息公开 | xxgk | +`, + // | 学院简介 | 1949 | + // | 学院领导 | 1950 | + // | 组织机构 | 1951 | +}; + +async function handler(ctx) { + const type = ctx.req.param('type'); + // const type = Number.parseInt(ctx.req.param('type')); + const response = await ofetch(rootUrl + type + '/list.htm'); + const $ = load(response); + + const list = $('.news.clearfix') + .toArray() + .map((item) => { + const cheerioItem = $(item); + const a = cheerioItem.find('a'); + + try { + const title = a.attr('title') || ''; + let link = a.attr('href'); + if (!link) { + link = ''; + } else if (!link.startsWith('http')) { + link = rootUrl.slice(0, -1) + link; + } + const pubDate = timezone(parseDate(cheerioItem.find('.news_meta').text()), +8); + + return { + title, + link, + pubDate, + }; + } catch { + return { + title: '', + link: '', + pubDate: Date.now(), + }; + } + }) + .filter((item) => item.title && item.link); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const newItem = { + ...item, + description: '', + }; + if (host === new URL(item.link).hostname) { + if (new URL(item.link).pathname.startsWith('/_upload')) { + // 链接为一个文件,直接返回链接 + newItem.description = item.link; + } else { + const response = await ofetch(item.link); + const $ = load(response); + newItem.description = $('.wp_articlecontent').html() || ''; + } + } else { + // 涉及到其他站点,不方便做统一的 html 解析,直接返回链接 + newItem.description = item.link; + } + return newItem; + }) + ) + ); + + return { + title: $('head > title').text() + ' - 泉州师范学院-数学与计算机科学学院 软件学院', + link: rootUrl + type + '/list.htm', + item: items, + } as Data; +} diff --git a/lib/routes/radio-canada/latest.ts b/lib/routes/radio-canada/latest.ts index 82e4fc69e27965..7c44933f56f7dc 100644 --- a/lib/routes/radio-canada/latest.ts +++ b/lib/routes/radio-canada/latest.ts @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| Français | English | Español | 简体中文 | 繁體中文 | العربية | ਪੰਜਾਬੀ | Tagalog | - | -------- | ------- | ------- | -------- | -------- | ------- | --- | ------- | - | fr | en | es | zh-hans | zh-hant | ar | pa | tl |`, +| -------- | ------- | ------- | -------- | -------- | ------- | --- | ------- | +| fr | en | es | zh-hans | zh-hant | ar | pa | tl |`, }; async function handler(ctx) { @@ -63,7 +63,7 @@ async function handler(ctx) { .match(/window\._rcState_ = (.*);/)[1]; const rcStateJson = JSON.parse(rcState); const news = Object.values(rcStateJson.pagesV2.pages)[0]; - item.description = news.data.newsStory.body.html.replaceAll('\\n', '<br>'); + item.description = news.data.newsStory.body.html.replaceAll(String.raw`\n`, '<br>'); return item; }) diff --git a/lib/routes/radio-canada/namespace.ts b/lib/routes/radio-canada/namespace.ts index ac78210e8fe291..54591d44fc6e77 100644 --- a/lib/routes/radio-canada/namespace.ts +++ b/lib/routes/radio-canada/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Radio-Canada.ca', url: 'ici.radio-canada.ca', + lang: 'en', }; diff --git a/lib/routes/radio/album.ts b/lib/routes/radio/album.ts index 3488b738de7641..babb63ded8b501 100644 --- a/lib/routes/radio/album.ts +++ b/lib/routes/radio/album.ts @@ -35,9 +35,9 @@ export const route: Route = { 所以对应路由为 [\`/radio/album/15682090498666\`](https://rsshub.app/radio/album/15682090498666) - :::tip +::: tip 部分专辑不适用该路由,此时可以尝试 [节目](#yun-ting-jie-mu) 路由 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/radio/index.ts b/lib/routes/radio/index.ts index 348acada8b624e..27135ceee7838f 100644 --- a/lib/routes/radio/index.ts +++ b/lib/routes/radio/index.ts @@ -27,11 +27,11 @@ export const route: Route = { 所以对应路由为 [\`/radio/1552135\`](https://rsshub.app/radio/1552135) - :::tip +::: tip 该路由仅适用于更新时间较早的电台节目,如 [共和国追梦人](http://www.radio.cn/pc-portal/sanji/detail.html?columnId=1552135) 与适用于 [专辑](#yun-ting-zhuan-ji) 路由的专辑其 \`columnId\` 长度相比,它们的 \`columnId\` 长度较短 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/radio/namespace.ts b/lib/routes/radio/namespace.ts index b8c597698d49ce..f354b8bdbdc64a 100644 --- a/lib/routes/radio/namespace.ts +++ b/lib/routes/radio/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '云听', url: 'radio.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/radio/zhibo.ts b/lib/routes/radio/zhibo.ts index 500f4d855b9156..f5d271cf81f81c 100644 --- a/lib/routes/radio/zhibo.ts +++ b/lib/routes/radio/zhibo.ts @@ -35,9 +35,9 @@ export const route: Route = { 所以对应路由为 [\`/radio/zhibo/1395528\`](https://rsshub.app/radio/zhibo/1395528) - :::tip +::: tip 查看更多电台直播节目,可前往 [电台直播](http://www.radio.cn/pc-portal/erji/radioStation.html) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/rarehistoricalphotos/namespace.ts b/lib/routes/rarehistoricalphotos/namespace.ts index df543e16ace575..72181bd0ff312e 100644 --- a/lib/routes/rarehistoricalphotos/namespace.ts +++ b/lib/routes/rarehistoricalphotos/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Rare Historical Photos', url: 'rarehistoricalphotos.com', + lang: 'en', }; diff --git a/lib/routes/raspberrypi/magazine.ts b/lib/routes/raspberrypi/magazine.ts new file mode 100644 index 00000000000000..77401ecbc71716 --- /dev/null +++ b/lib/routes/raspberrypi/magazine.ts @@ -0,0 +1,174 @@ +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<Data> => { + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '12', 10); + + const baseUrl: string = 'https://magazine.raspberrypi.com'; + const targetUrl: string = new URL('issues', baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'en'; + + let items: DataItem[] = []; + + const author: DataItem['author'] = $('meta[property="og:site_name"]').attr('content'); + + items = $('div.o-grid--equal div.o-grid__col') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio<Element> = $(el); + const $aEl: Cheerio<Element> = $el.find('h2.rspec-issue-card-heading a.c-link'); + + const title: string = $aEl.text()?.trim(); + const image: string | undefined = $el.find('div.o-media__fixed a.c-link img').attr('src'); + const description: string | undefined = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + intro: $el.find('p.rspec-issue-card-summary').text(), + }); + const pubDateStr: string | undefined = $el.find('time').attr('datetime'); + const linkUrl: string | undefined = $aEl.attr('href'); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + author, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('h1.rspec-issue__heading').text().split(/-/).pop()?.trim() ?? item.title; + const description: string | undefined = + item.description + + art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.rspec-issue__description').html(), + }); + const pubDateStr: string | undefined = $$('time.rspec-issue__publication-month').attr('datetime'); + const image: string | undefined = $$('img.c-figure__image').attr('src'); + const upDatedStr: string | undefined = pubDateStr; + + let processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate, + author, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : item.updated, + language, + }; + + const pdfUrl: string = new URL('pdf/download', `${item.link}/`).href; + const pdfResponse = await ofetch(pdfUrl); + const $$$: CheerioAPI = load(pdfResponse); + + const $$$enclosureEl: Cheerio<Element> = $$$('a.c-link').first(); + const enclosureUrl: string | undefined = $$$enclosureEl.attr('href') ? new URL($$$enclosureEl.attr('href') as string, baseUrl).href : undefined; + + if (enclosureUrl) { + const enclosureType: string = 'application/pdf'; + + processedItem = { + ...processedItem, + enclosure_url: enclosureUrl, + enclosure_type: enclosureType, + enclosure_title: title, + enclosure_length: undefined, + }; + } + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[property="og:description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('meta[property="og:image"]').attr('content'), + author: $('meta[property="og:site_name"]').attr('content'), + language, + id: $('meta[property="og:url"]').attr('content'), + }; +}; + +export const route: Route = { + path: '/magazine', + name: 'Official Magazine', + url: 'magazine.raspberrypi.com', + maintainers: ['nczitzk'], + handler, + example: '/raspberrypi/magazine', + parameters: undefined, + categories: ['programming'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['magazine.raspberrypi.com'], + target: '/raspberrypi/magazine', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/raspberrypi/namespace.ts b/lib/routes/raspberrypi/namespace.ts new file mode 100644 index 00000000000000..7117581a65e5e2 --- /dev/null +++ b/lib/routes/raspberrypi/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Raspberry Pi', + url: 'raspberrypi.com', + categories: ['programming'], + description: '', + lang: 'en', +}; diff --git a/lib/routes/raspberrypi/templates/description.art b/lib/routes/raspberrypi/templates/description.art new file mode 100644 index 00000000000000..249654e7e618a4 --- /dev/null +++ b/lib/routes/raspberrypi/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} + <figure> + <img + {{ if image.alt }} + alt="{{ image.alt }}" + {{ /if }} + src="{{ image.src }}"> + </figure> + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} + <blockquote>{{ intro }}</blockquote> +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/rattibha/namespace.ts b/lib/routes/rattibha/namespace.ts index b4537f1f7696d4..9f0df22a225a49 100644 --- a/lib/routes/rattibha/namespace.ts +++ b/lib/routes/rattibha/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Rattibha', url: 'rattibha.com', + lang: 'en', }; diff --git a/lib/routes/rawkuma/namespace.ts b/lib/routes/rawkuma/namespace.ts index 96518104969bf4..3ef3e8182ac7cf 100644 --- a/lib/routes/rawkuma/namespace.ts +++ b/lib/routes/rawkuma/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Rawkuma', url: 'rawkuma.com', + lang: 'en', }; diff --git a/lib/routes/raycast/changelog.ts b/lib/routes/raycast/changelog.ts new file mode 100644 index 00000000000000..35e72f539542ea --- /dev/null +++ b/lib/routes/raycast/changelog.ts @@ -0,0 +1,54 @@ +import type { Route, DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +const handler: Route['handler'] = async () => { + const item = (await cache.tryGet('raycast:changelog', async () => { + const data = await ofetch('https://www.raycast.com/changelog'); + + const $ = load(data); + + return $('article') + .toArray() + .map<DataItem>((item) => { + const $ = load(item); + + const version = $('span[id]').attr('id'); + const html = $('div.markdown').html() ?? ''; + const date = $('span[class^=ChangelogEntry_changelogDate]').text().trim(); + + return { + title: `Version ${version}`, + description: html, + link: `https://www.raycast.com/changelog/${version?.replaceAll('.', '-')}`, + pubDate: parseDate(date), + }; + }); + })) as DataItem[]; + + return { + title: 'Raycast Changelog', + link: 'https://www.raycast.com/changelog', + language: 'en-US', + item, + }; +}; + +export const route: Route = { + path: '/changelog', + name: 'Changelog', + categories: ['program-update'], + example: '/raycast/changelog', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + handler, + maintainers: ['equt'], +}; diff --git a/lib/routes/raycast/namespace.ts b/lib/routes/raycast/namespace.ts new file mode 100644 index 00000000000000..8919e5ff2aebda --- /dev/null +++ b/lib/routes/raycast/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Raycast', + url: 'raycast.com', + categories: ['program-update'], + lang: 'en', +}; diff --git a/lib/routes/react/blog.ts b/lib/routes/react/blog.ts new file mode 100644 index 00000000000000..a1d3f2f01ca54b --- /dev/null +++ b/lib/routes/react/blog.ts @@ -0,0 +1,57 @@ +import type { DataItem, Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; + +const handler: Route['handler'] = async () => { + const data = await ofetch('https://react.dev/blog'); + + const $ = load(data); + + const item = (await Promise.all( + $('a[href^="/blog/"]') + .toArray() + .slice(0, 20) + .map((item) => { + const link = `https://react.dev${item.attribs.href}`; + + return cache.tryGet(`react:blog:${link}`, async () => { + const data = await ofetch(link); + + const $ = load(data); + + return { + title: $('h1').first().text().trim(), + link, + description: $('article div:nth-child(2)').html() ?? '', + pubDate: parseDate($('p.whitespace-pre-wrap').first().text().split(/\s+by/)[0]), + }; + }); + }) + )) as DataItem[]; + + return { + title: 'React Blog', + link: 'https://react.dev/blog', + language: 'en-US', + item, + }; +}; + +export const route: Route = { + path: '/blog', + name: 'Blog', + categories: ['blog'], + maintainers: ['equt'], + example: '/react/blog', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + handler, +}; diff --git a/lib/routes/react/namespace.ts b/lib/routes/react/namespace.ts new file mode 100644 index 00000000000000..5164acbc3ef333 --- /dev/null +++ b/lib/routes/react/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'React', + url: 'react.dev', + lang: 'en', +}; diff --git a/lib/routes/reactiflux/namespace.ts b/lib/routes/reactiflux/namespace.ts index 408b8829ca215d..52eed789380ae2 100644 --- a/lib/routes/reactiflux/namespace.ts +++ b/lib/routes/reactiflux/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: 'Reactiflux', url: 'reactiflux.com', categories: ['programming'], + lang: 'en', }; diff --git a/lib/routes/reactnewsletter/namespace.ts b/lib/routes/reactnewsletter/namespace.ts index 99b9f6beb30dfb..7b040e171a07eb 100644 --- a/lib/routes/reactnewsletter/namespace.ts +++ b/lib/routes/reactnewsletter/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/readhub/daily.ts b/lib/routes/readhub/daily.ts index 5ddcbfa5ff815a..2e36c2ed995499 100644 --- a/lib/routes/readhub/daily.ts +++ b/lib/routes/readhub/daily.ts @@ -7,7 +7,7 @@ import { rootUrl, apiRootUrl, processItems } from './util'; export const route: Route = { path: '/daily', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/readhub/daily', parameters: {}, features: { diff --git a/lib/routes/readhub/index.ts b/lib/routes/readhub/index.ts index f78073e546b5a0..b6a0c257c3dfa4 100644 --- a/lib/routes/readhub/index.ts +++ b/lib/routes/readhub/index.ts @@ -12,7 +12,7 @@ import { rootUrl, apiTopicUrl, art, processItems } from './util'; export const route: Route = { path: '/:category?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/readhub', parameters: { category: '分类,见下表,默认为热门话题' }, features: { @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['WhiteWorld', 'nczitzk', 'Fatpandac'], handler, description: `| 热门话题 | 科技动态 | 医疗产业 | 财经快讯 | - | -------- | -------- | -------- | ------------------ | - | | news | medical | financial\_express |`, +| -------- | -------- | -------- | ------------------ | +| | news | medical | financial\_express |`, }; async function handler(ctx) { diff --git a/lib/routes/readhub/namespace.ts b/lib/routes/readhub/namespace.ts index cfbf4916e45050..3dc60a8959fcb9 100644 --- a/lib/routes/readhub/namespace.ts +++ b/lib/routes/readhub/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Readhub', url: 'readhub.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/readhub/util.ts b/lib/routes/readhub/util.ts index 9aabb38d4a1567..fabad327a88ea9 100644 --- a/lib/routes/readhub/util.ts +++ b/lib/routes/readhub/util.ts @@ -40,7 +40,7 @@ const processItems = async (items, tryGet) => const { data: detailResponse } = await got(item.link); - const data = JSON.parse(detailResponse.match(/{\\"topic\\":(.*?)}]\\n"]\)<\/script>/)[1].replaceAll('\\"', '"')); + const data = JSON.parse(detailResponse.match(/{\\"topic\\":(.*?)}]\\n"]\)<\/script>/)[1].replaceAll(String.raw`\"`, '"')); item.title = data.title; item.link = data.url ?? new URL(`topic/${data.uid}`, rootUrl).href; diff --git a/lib/routes/readwise/list.ts b/lib/routes/readwise/list.ts new file mode 100644 index 00000000000000..f996a7e08ab97e --- /dev/null +++ b/lib/routes/readwise/list.ts @@ -0,0 +1,152 @@ +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import ofetch from '@/utils/ofetch'; +import { Route } from '@/types'; +import { config } from '@/config'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/list/:routeParams?', + categories: ['reading'], + example: '/readwise/list/location=new&category=article', + parameters: { routeParams: 'Parameter combinations, see the description above.' }, + features: { + requireConfig: [ + { + name: 'READWISE_ACCESS_TOKEN', + optional: false, + description: 'Visit `https://readwise.io/access_token` to get your access token.', + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['read.readwise.io'], + target: '/list', + }, + ], + name: 'Reader Document List', + maintainers: ['xbot'], + handler, + description: `Specify options (in the format of query string) in parameter \`routeParams\` to filter documents. + +| Parameter | Description | Values | Default | +| -------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------- | +| \`location\` | The document's location. | \`new\`/\`later\`/\`shortlist\`/\`archive\`/\`feed\` | | +| \`category\` | The document's category. | \`article\`/\`email\`/\`rss\`/\`highlight\`/\`note\`/\`pdf\`/\`epub\`/\`tweet\`/\`video\` | | +| \`updatedAfter\` | Fetch only documents updated after this date. | string (formatted as ISO 8601 date) || +| \`tag\` | The document's tag, can be specified once or multiple times. ||| +| \`tagStrategy\` | If multiple tags are specified, should the documents match all of them or any of them. | \`any\`/\`all\` | \`any\` | + +Customise parameter values to fetch specific documents, for example: + +\`\`\` +https://rsshub.app/readwise/list/location=new&category=article +\`\`\` + +fetches articles in the Inbox. + +\`\`\` +https://rsshub.app/readwise/list/category=article&tag=shortlist&tag=AI&tagStrategy=all +\`\`\` + +fetches articles tagged both by \`shortlist\` and \`AI\`. `, +}; + +const TAG_STRATEGY_ALL = 'all'; // Items with tags matching all the given ones can then be returned. +const TAG_STRATEGY_ANY = 'any'; // Items with tags matching any of the given ones can be returned. + +async function handler(ctx) { + if (!config.readwise || !config.readwise.accessToken) { + throw new ConfigNotFoundError('Readwise access token is missing'); + } + + let apiUrl = 'https://readwise.io/api/v3/list/?'; + let tag, tagStrategy; + + if (ctx.req.param('routeParams')) { + const urlSearchParams = new URLSearchParams(ctx.req.param('routeParams')); + + const location = urlSearchParams.get('location'); + const category = urlSearchParams.get('category'); + const updatedAfter = urlSearchParams.get('updatedAfter'); + + tag = urlSearchParams.get('tag'); + tagStrategy = urlSearchParams.get('tagStrategy') === TAG_STRATEGY_ANY || urlSearchParams.get('tagStrategy') === TAG_STRATEGY_ALL ? urlSearchParams.get('tagStrategy') : TAG_STRATEGY_ANY; + + if (location) { + apiUrl += `location=${location}&`; + } + if (category) { + apiUrl += `category=${category}&`; + } + if (updatedAfter) { + apiUrl += `updatedAfter=${updatedAfter}&`; + } + } + + const fullData = []; + + async function fetchNextPage(url) { + const response = await ofetch(url, { + headers: { + Authorization: `Token ${config.readwise.accessToken}`, + }, + }); + + fullData.push(...response.results); + + if (response.nextPageCursor) { + await fetchNextPage(apiUrl + `pageCursor=${response.nextPageCursor}`); + } + } + + await fetchNextPage(apiUrl); + + const items = fullData + .filter((item) => { + if (!tag) { + return true; // No tag filter applied + } + + // Check if item.tags exist and match the criteria based on tagStrategy + const itemTags = item.tags; + + if (!itemTags) { + return false; // If item has no tags and tag filter is applied, exclude it + } + + if (Array.isArray(tag)) { + if (tagStrategy === TAG_STRATEGY_ANY) { + // Filter if any of the tags match + return tag.some((t) => Object.values(itemTags).some((tagObj) => tagObj.name === t)); + } else if (tagStrategy === TAG_STRATEGY_ALL) { + // Filter if all tags match + return tag.every((t) => Object.values(itemTags).some((tagObj) => tagObj.name === t)); + } + } else { + const tagName = tag; + return Object.values(itemTags).some((tagObj) => tagObj.name === tagName); + } + + return false; + }) + .map((item) => ({ + title: item.title, + link: item.source_url, + description: item.summary, + pubDate: parseDate(item.created_at), + author: item.author, + })); + + return { + allowEmpty: true, + title: 'Readwise Reader', + link: 'https://read.readwise.io', + item: items, + }; +} diff --git a/lib/routes/readwise/namespace.ts b/lib/routes/readwise/namespace.ts new file mode 100644 index 00000000000000..a41092eb12edff --- /dev/null +++ b/lib/routes/readwise/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Readwise', + url: 'readwise.io', + lang: 'en', +}; diff --git a/lib/routes/rebase/geekdaily.ts b/lib/routes/rebase/geekdaily.ts new file mode 100644 index 00000000000000..dee8ccbae0db34 --- /dev/null +++ b/lib/routes/rebase/geekdaily.ts @@ -0,0 +1,35 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/geekdaily', + categories: ['new-media', 'popular'], + example: '/rebase/geekdaily', + radar: [ + { + source: ['rebase.network/geekdaily'], + target: '/geekdaily', + }, + ], + name: 'Web3 Geek Daily', + maintainers: ['gaoyifan'], + handler: async () => { + const response = await ofetch('https://db.rebase.network/api/v1/geekdailies?sort=id:desc'); + const data = response.data; + + const items = data.map((item) => ({ + title: item.attributes.title, + link: item.attributes.url, + description: item.attributes.introduce, + pubDate: parseDate(item.attributes.time), + author: item.attributes.author, + })); + + return { + title: 'Web3 Geek Daily', + link: 'https://rebase.network/geekdaily', + item: items, + }; + }, +}; diff --git a/lib/routes/rebase/namespace.ts b/lib/routes/rebase/namespace.ts new file mode 100644 index 00000000000000..3798b73d76fde8 --- /dev/null +++ b/lib/routes/rebase/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Rebase Network', + url: 'rebase.network', + lang: 'en', +}; diff --git a/lib/routes/remnote/namespace.ts b/lib/routes/remnote/namespace.ts index ee1ff24f470d3b..1cdbff59d0d9bf 100644 --- a/lib/routes/remnote/namespace.ts +++ b/lib/routes/remnote/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'RemNote', url: 'remnote.com', + lang: 'en', }; diff --git a/lib/routes/researchgate/namespace.ts b/lib/routes/researchgate/namespace.ts index ed2d0f9274ff27..bde4205243d96e 100644 --- a/lib/routes/researchgate/namespace.ts +++ b/lib/routes/researchgate/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ResearchGate', url: 'researchgate.net', + lang: 'en', }; diff --git a/lib/routes/resonac/namespace.ts b/lib/routes/resonac/namespace.ts new file mode 100644 index 00000000000000..4f8f2582a0c8f7 --- /dev/null +++ b/lib/routes/resonac/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Resonac', + url: 'www.resonac.com', + lang: 'en', +}; diff --git a/lib/routes/resonac/products.ts b/lib/routes/resonac/products.ts new file mode 100644 index 00000000000000..918c8b79ea493d --- /dev/null +++ b/lib/routes/resonac/products.ts @@ -0,0 +1,85 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +// import { parseDate } from '@/utils/parse-date'; +// import timezone from '@/utils/timezone'; + +const baseUrl = 'https://www.resonac.com'; +const host = 'https://www.resonac.com/products?intcid=glnavi_products'; + +export const route: Route = { + path: '/products', + categories: ['other'], + example: '/resonac/products', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Products', + maintainers: ['valuex'], + handler, + description: '', +}; + +async function handler() { + const response = await got(host); + const pageHtml = response.data; + const $ = load(pageHtml); + const groupLists = $('div.m-panel-card-link ul li') + .toArray() + .map((el) => ({ + groupName: $('a', el).text().trim(), + groupURL: baseUrl + $('a', el).attr('href'), + })); + + const lists = await Promise.all( + groupLists.map((productGroup) => + cache.tryGet(productGroup.groupURL, async () => { + const strUrl = productGroup.groupURL; + const response = await got(strUrl); + const $ = load(response.data); + const item = $('dt.m-toggle__title div span a') + .toArray() + .map((el) => ({ + title: $('b', el).text().trim(), + link: baseUrl + $(el).attr('href'), + group: productGroup.groupName, + })); + return item; + }) + ) + ); + + const fullList = lists.flat(1); // flatten array + // fullList = fullList.filter((item) => item.title !== 'Empty'); + + const items = await Promise.all( + fullList.map((item) => + cache.tryGet(item.link, async () => { + try { + const response = await got(item.link); + const $ = load(response.data); + const thisTitle = item.title + ' | ' + item.group; + item.title = thisTitle; + item.description = $('main div.str-section').html(); + return item; + } catch (error) { + return (error as Error).message; + } + }) + ) + ); + + return { + title: 'Resonac_Products', + link: baseUrl, + description: 'Resonac_Products', + item: items, + }; +} diff --git a/lib/routes/reuters/common.ts b/lib/routes/reuters/common.ts index f376dabcf98ba5..908ea65b2d6342 100644 --- a/lib/routes/reuters/common.ts +++ b/lib/routes/reuters/common.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); @@ -11,9 +11,26 @@ import path from 'node:path'; export const route: Route = { path: '/:category/:topic?', - categories: ['traditional-media'], + categories: ['traditional-media', 'popular'], + view: ViewType.Articles, example: '/reuters/world/us', - parameters: { category: 'find it in the URL, or tables below', topic: 'find it in the URL, or tables below' }, + parameters: { + category: { + description: 'find it in the URL, or tables below', + options: [ + { value: 'world', label: 'World' }, + { value: 'business', label: 'Business' }, + { value: 'legal', label: 'Legal' }, + { value: 'markets', label: 'Markets' }, + { value: 'breakingviews', label: 'Breakingviews' }, + { value: 'technology', label: 'Technology' }, + { value: 'graphics', label: 'Graphics' }, + { value: 'authors', label: 'Authors' }, + ], + default: 'world', + }, + topic: 'find it in the URL, or tables below, leave empty for `All`', + }, features: { requireConfig: false, requirePuppeteer: false, @@ -28,7 +45,7 @@ export const route: Route = { }, ], name: 'Category/Topic/Author', - maintainers: ['LyleLee', 'HenryQW', 'proletarius101', 'black-desk', 'nczitzk'], + maintainers: ['LyleLee', 'HenryQW', 'proletarius101', 'black-desk', 'nczitzk', 'pseudoyu'], handler, description: `- \`:category\`: @@ -73,128 +90,166 @@ async function handler(ctx) { const useSophi = ctx.req.query('sophi') === 'true' && topic !== '' && CAN_USE_SOPHI.includes(category); const section_id = `/${category}/${topic ? `${topic}/` : ''}`; - const { title, description, rootUrl, response } = await (async () => { - if (MUST_FETCH_BY_TOPICS.has(category)) { - const rootUrl = 'https://www.reuters.com/pf/api/v3/content/fetch/articles-by-topic-v1'; - const response = await ofetch(rootUrl, { - query: { - query: JSON.stringify({ - offset: 0, - size: limit, - topic_url: section_id, - website: 'reuters', - }), - }, - }); - return { - title: `${response.result.topics[0].name} | Reuters`, - description: response.result.topics[0].entity_id, - rootUrl, - response, - }; - } else { - const rootUrl = 'https://www.reuters.com/pf/api/v3/content/fetch/articles-by-section-alias-or-id-v1'; - const response = await ofetch(rootUrl, { - query: { - query: JSON.stringify({ - offset: 0, - size: limit, - section_id, - website: 'reuters', - ...(useSophi - ? { - fetch_type: 'sophi', - sophi_page: '*', - sophi_widget: 'topic', - } - : {}), - }), - }, - }); - return { - title: response.result.section.title, - description: response.result.section.section_about, - rootUrl, - response, - }; - } - })(); - - let items = response.result.articles.map((e) => ({ - title: e.title, - link: new URL(e.canonical_url, rootUrl).href, - guid: e.id, - pubDate: parseDate(e.published_time), - updated: parseDate(e.updated_time), - author: e.authors.map((e) => e.name).join(', '), - category: e.kicker.names, - description: e.description, - })); - - items = items.filter((e, i) => items.findIndex((f) => e.guid === f.guid) === i); - - const results = await Promise.allSettled( - items.map((item) => - ctx.req.query('mode') === 'fulltext' - ? cache.tryGet(item.link, async () => { - const detailResponse = await ofetch(item.link); - const content = load(detailResponse.data); - - if (detailResponse.url.startsWith('https://www.reuters.com/investigates/')) { - const ldJson = JSON.parse(content('script[type="application/ld+json"]').text()); - content('.special-report-article-container .container, #slide-dek, #slide-end, .share-in-article-container').remove(); - - item.title = ldJson.headline; - item.pubDate = parseDate(ldJson.dateCreated); - item.author = ldJson.creator; - item.category = ldJson.keywords; - item.description = content('.special-report-article-container').html(); + try { + const { title, description, rootUrl, response } = await (async () => { + if (MUST_FETCH_BY_TOPICS.has(category)) { + const rootUrl = 'https://www.reuters.com/pf/api/v3/content/fetch/articles-by-topic-v1'; + const response = await ofetch(rootUrl, { + query: { + query: JSON.stringify({ + offset: 0, + size: limit, + topic_url: section_id, + website: 'reuters', + }), + }, + }); + + return { + title: `${response.result.topics[0].name} | Reuters`, + description: response.result.topics[0].entity_id, + rootUrl, + response, + }; + } else { + const rootUrl = 'https://www.reuters.com/pf/api/v3/content/fetch/articles-by-section-alias-or-id-v1'; + const response = await ofetch(rootUrl, { + query: { + query: JSON.stringify({ + offset: 0, + size: limit, + section_id, + website: 'reuters', + ...(useSophi + ? { + fetch_type: 'sophi', + sophi_page: '*', + sophi_widget: 'topic', + } + : {}), + }), + }, + }); + return { + title: response.result.section.title, + description: response.result.section.section_about, + rootUrl, + response, + }; + } + })(); + + let items = response.result.articles.map((e) => ({ + title: e.title, + link: new URL(e.canonical_url, rootUrl).href, + guid: e.id, + pubDate: parseDate(e.published_time), + updated: parseDate(e.updated_time), + author: e.authors.map((e) => e.name).join(', '), + category: e.kicker.names, + description: e.description, + })); + + items = items.filter((e, i) => items.findIndex((f) => e.guid === f.guid) === i); + + const results = await Promise.allSettled( + items.map((item) => + ctx.req.query('fulltext') === 'true' + ? cache.tryGet(item.link, async () => { + const detailResponse = await ofetch(item.link); + const content = load(detailResponse.data); + + if (detailResponse.url.startsWith('https://www.reuters.com/investigates/')) { + const ldJson = JSON.parse(content('script[type="application/ld+json"]').text()); + content('.special-report-article-container .container, #slide-dek, #slide-end, .share-in-article-container').remove(); + + item.title = ldJson.headline; + item.pubDate = parseDate(ldJson.dateCreated); + item.author = ldJson.creator; + item.category = ldJson.keywords; + item.description = content('.special-report-article-container').html(); + + return item; + } + + const matches = content('script#fusion-metadata') + .text() + .match(/Fusion.globalContent=({[\S\s]*?});/); + + if (matches) { + const data = JSON.parse(matches[1]); + + item.title = data.result.title || item.title; + item.description = art(path.join(__dirname, 'templates/description.art'), { + result: data.result, + }); + item.pubDate = parseDate(data.result.display_time); + item.author = data.result.authors.map((author) => author.name).join(', '); + item.category = data.result.taxonomy.keywords; + + return item; + } + + content('.title').remove(); + content('.article-metadata').remove(); + + item.title = content('meta[property="og:title"]').attr('content'); + item.pubDate = parseDate(detailResponse.data.match(/"datePublished":"(.*?)","dateModified/)[1]); + item.author = detailResponse.data + .match(/{"@type":"Person","name":"(.*?)"}/g) + .map((p) => p.match(/"name":"(.*?)"/)[1]) + .join(', '); + item.description = content('article').html(); return item; - } + }) + : item + ) + ); + items = results.filter((r) => r.status === 'fulfilled').map((r) => r.value); + + return { + title, + description, + image: 'https://www.reuters.com/pf/resources/images/reuters/logo-vertical-default-512x512.png?d=116', + link: `https://www.reuters.com${section_id}`, + item: items, + }; + } catch { + // Fallback to arc outboundfeeds if API fails + const arcUrl = topic ? `https://www.reuters.com/arc/outboundfeeds/v4/mobile/section${section_id}?outputType=json` : `https://www.reuters.com/arc/outboundfeeds/v4/mobile/section/${category}/?outputType=json`; + + const arcResponse = await ofetch(arcUrl); + if (arcResponse.wireitems?.length) { + const items = arcResponse.wireitems + .map((item) => { + const story = item.templates.find((t) => t.template === 'story_with_image')?.story; + if (!story) { + return null; + } + + return { + title: story.hed, + link: item.templates.find((t) => t.template === 'story_with_image')?.template_action?.url, + guid: story.usn, + pubDate: parseDate(story.updated_at), + updated: parseDate(story.updated_at), + description: story.lede, + author: story.authors?.map((author) => author.name).join(', '), + category: [arcResponse.analytics?.topic_channel, arcResponse.analytics?.topic_sub_channel].filter(Boolean), + }; + }) + .filter(Boolean); - const matches = content('script#fusion-metadata') - .text() - .match(/Fusion.globalContent=({[\S\s]*?});/); - - if (matches) { - const data = JSON.parse(matches[1]); - - item.title = data.result.title || item.title; - item.description = art(path.join(__dirname, 'templates/description.art'), { - result: data.result, - }); - item.pubDate = parseDate(data.result.display_time); - item.author = data.result.authors.map((author) => author.name).join(', '); - item.category = data.result.taxonomy.keywords; - - return item; - } - - content('.title').remove(); - content('.article-metadata').remove(); - - item.title = content('meta[property="og:title"]').attr('content'); - item.pubDate = parseDate(detailResponse.data.match(/"datePublished":"(.*?)","dateModified/)[1]); - item.author = detailResponse.data - .match(/{"@type":"Person","name":"(.*?)"}/g) - .map((p) => p.match(/"name":"(.*?)"/)[1]) - .join(', '); - item.description = content('article').html(); - - return item; - }) - : item - ) - ); - items = results.filter((r) => r.status === 'fulfilled').map((r) => r.value); - - return { - title, - description, - image: 'https://www.reuters.com/pf/resources/images/reuters/logo-vertical-default-512x512.png?d=116', - link: `https://www.reuters.com${section_id}`, - item: items, - }; + return { + title: arcResponse.analytics?.title || `${arcResponse.wire_name} | Reuters`, + description: arcResponse.analytics?.content_title, + image: 'https://www.reuters.com/pf/resources/images/reuters/logo-vertical-default-512x512.png?d=116', + link: arcResponse.canonical_action?.url || `https://www.reuters.com${section_id}`, + category: [arcResponse.analytics?.topic_channel, arcResponse.analytics?.topic_sub_channel].filter(Boolean), + item: items.slice(0, limit), + }; + } + } } diff --git a/lib/routes/reuters/investigates.ts b/lib/routes/reuters/investigates.ts index 8cc64eb8db0b98..16204e38197f37 100644 --- a/lib/routes/reuters/investigates.ts +++ b/lib/routes/reuters/investigates.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 { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/investigates', - categories: ['traditional-media'], + categories: ['traditional-media', 'popular'], + view: ViewType.Articles, example: '/reuters/investigates', parameters: {}, features: { diff --git a/lib/routes/reuters/namespace.ts b/lib/routes/reuters/namespace.ts index 25ab99bf9c2b58..8ddc25c8a570d0 100644 --- a/lib/routes/reuters/namespace.ts +++ b/lib/routes/reuters/namespace.ts @@ -1,9 +1,10 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Reuters 路透社', + name: 'Reuters', url: 'reuters.com', - description: `:::tip + description: `::: tip You can use \`sophi=true\` query parameter to invoke the **experimental** method, which can, if possible, fetch more articles(between 20 and 100) with \`limit\` given. But some articles from the old method might not be available. :::`, + lang: 'en', }; diff --git a/lib/routes/rfa/namespace.ts b/lib/routes/rfa/namespace.ts index f028b2d738c0a6..8bc31679a27ad4 100644 --- a/lib/routes/rfa/namespace.ts +++ b/lib/routes/rfa/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Radio Free Asia (RFA) 自由亚洲电台', url: 'rfa.org', + lang: 'en', }; diff --git a/lib/routes/rfi/namespace.ts b/lib/routes/rfi/namespace.ts index b98bdd5b19c646..26aac54e8056e4 100644 --- a/lib/routes/rfi/namespace.ts +++ b/lib/routes/rfi/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Radio France Internationale 法国国际广播电台', url: 'rfi.fr', + lang: 'fr', }; diff --git a/lib/routes/right/namespace.ts b/lib/routes/right/namespace.ts index b2f769d5f401f8..2347e5751e38f7 100644 --- a/lib/routes/right/namespace.ts +++ b/lib/routes/right/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '恩山无线论坛', url: 'right.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/rodong/namespace.ts b/lib/routes/rodong/namespace.ts index 24887a695a4d23..60e50a426203ef 100644 --- a/lib/routes/rodong/namespace.ts +++ b/lib/routes/rodong/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Rodong Sinmun 劳动新闻', url: 'rodong.rep.kp', + lang: 'ko', }; diff --git a/lib/routes/rodong/news.ts b/lib/routes/rodong/news.ts index eb7856417581cd..07258ceef144c7 100644 --- a/lib/routes/rodong/news.ts +++ b/lib/routes/rodong/news.ts @@ -30,8 +30,8 @@ export const route: Route = { handler, url: 'rodong.rep.kp/cn/index.php', description: `| 조선어 | English | 中文 | - | ------ | ------- | ---- | - | ko | en | cn |`, +| ------ | ------- | ---- | +| ko | en | cn |`, }; async function handler(ctx) { diff --git a/lib/routes/routledge/namespace.ts b/lib/routes/routledge/namespace.ts index 1a546087197cdc..6b6a47e9a6cfbe 100644 --- a/lib/routes/routledge/namespace.ts +++ b/lib/routes/routledge/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Routledge', url: 'routledge.com', + lang: 'en', }; diff --git a/lib/routes/rsc/journal.ts b/lib/routes/rsc/journal.ts index 6afa2eabf5a54d..7354846d374dde 100644 --- a/lib/routes/rsc/journal.ts +++ b/lib/routes/rsc/journal.ts @@ -26,13 +26,13 @@ export const route: Route = { name: 'Journal', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip All journals at [Current journals](https://pubs.rsc.org/en/journals) - ::: +::: - | All Recent Articles | Advance Articles | - | ------------------- | ---------------- | - | allrecentarticles | advancearticles |`, +| All Recent Articles | Advance Articles | +| ------------------- | ---------------- | +| allrecentarticles | advancearticles |`, }; async function handler(ctx) { diff --git a/lib/routes/rsc/namespace.ts b/lib/routes/rsc/namespace.ts index e113dcacd738a7..73d35b9e228624 100644 --- a/lib/routes/rsc/namespace.ts +++ b/lib/routes/rsc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Royal Society of Chemistry', url: 'pubs.rsc.org', + lang: 'en', }; diff --git a/lib/routes/rss3/index.ts b/lib/routes/rss3/index.ts new file mode 100644 index 00000000000000..bd96959f408832 --- /dev/null +++ b/lib/routes/rss3/index.ts @@ -0,0 +1,162 @@ +import { Route, type DataItem } from '@/types'; + +import { camelcaseKeys } from '@/utils/camelcase-keys'; +import ofetch from '@/utils/ofetch'; +import { renderItemActionToHTML } from '@rss3/sdk'; + +export const route: Route = { + path: '/:account/:network?/:tag?', + categories: ['social-media'], + example: '/rss3/vitalik.eth', + name: 'Account Activities', + maintainers: ['DIYgod', 'pseudoyu'], + url: 'docs.rss3.io/api-reference#tag/decentralized/GET/decentralized/%7Baccount%7D', + handler, + description: 'Retrieve the activities associated with a specified account in the decentralized system.', + parameters: { + account: { + description: 'Retrieve activities from the specified account. This account is a unique identifier within the decentralized system.', + }, + network: { + description: 'Retrieve activities from the specified network.', + default: 'all', + options: [ + { + value: 'all', + label: 'All', + }, + { + value: 'arbitrum', + label: 'Arbitrum', + }, + { + value: 'arweave', + label: 'Arweave', + }, + { + value: 'avax', + label: 'Avax', + }, + { + value: 'base', + label: 'Base', + }, + { + value: 'binance-smart-chain', + label: 'Binance Smart Chain', + }, + { + value: 'crossbell', + label: 'Crossbell', + }, + { + value: 'ethereum', + label: 'Ethereum', + }, + { + value: 'farcaster', + label: 'Farcaster', + }, + { + value: 'gnosis', + label: 'Gnosis', + }, + { + value: 'linea', + label: 'Linea', + }, + { + value: 'optimism', + label: 'Optimism', + }, + { + value: 'polygon', + label: 'Polygon', + }, + { + value: 'vsl', + label: 'VSL', + }, + ], + }, + tag: { + description: 'Retrieve activities from the specified tag.', + default: 'all', + options: [ + { + value: 'all', + label: 'All', + }, + { + value: 'collectible', + label: 'collectible', + }, + { + value: 'exchange', + label: 'exchange', + }, + { + value: 'metaverse', + label: 'metaverse', + }, + { + value: 'rss', + label: 'rss', + }, + { + value: 'social', + label: 'social', + }, + { + value: 'transaction', + label: 'transaction', + }, + { + value: 'unknown', + label: 'unknown', + }, + ], + }, + }, +}; + +async function handler(ctx) { + const { account, network, tag } = ctx.req.param(); + + // Check if account contains "://" or "/" + if (account.includes('://') || account.includes('/')) { + throw new Error('Account should not contain "://" or path components'); + } + + const { data } = await ofetch( + `https://gi.rss3.io/decentralized/${account}?${new URLSearchParams({ + limit: '20', + ...(network && network !== 'all' && { network }), + ...(tag && tag !== 'all' && { tag }), + })}` + ); + + return { + title: `${account} activities`, + link: 'https://rss3.io', + item: data.map((item) => { + const content = renderItemActionToHTML(camelcaseKeys(item.actions)); + + const description = `New ${item.tag} ${item.type} action on ${item.network}<br /><br />From: ${item.from}<br/>To: ${item.to}`; + return { + title: `New ${item.tag} ${item.type} action on ${item.network}`, + description: content ?? description, + link: item.actions?.[0]?.related_urls?.[0], + guid: item.id, + author: [ + { + name: item.owner, + avatar: `https://cdn.stamp.fyi/avatar/eth:${item.owner}`, + }, + ], + + _extra: { raw: item }, + } as DataItem; + }), + }; +} diff --git a/lib/routes/rss3/interfaces/metadata.ts b/lib/routes/rss3/interfaces/metadata.ts new file mode 100644 index 00000000000000..52b40a8c3758c2 --- /dev/null +++ b/lib/routes/rss3/interfaces/metadata.ts @@ -0,0 +1,67 @@ +import { + CollectibleApproval, + CollectibleBurn, + CollectibleMint, + CollectibleTrade, + CollectibleTransfer, + ExchangeLiquidity, + ExchangeStaking, + ExchangeSwap, + MetaverseBurn, + MetaverseMint, + MetaverseTrade, + MetaverseTransfer, + SocialComment, + SocialDelete, + SocialMint, + SocialPost, + SocialProfile, + SocialProxy, + SocialRevise, + SocialReward, + SocialShare, + StakeStaking, + StakeTransaction, + StakerProfitSnapshot, + TransactionApproval, + TransactionBridge, + TransactionBurn, + TransactionEvent, + TransactionMint, + TransactionTransfer, +} from '@rss3/sdk'; +export type RSS3DataModels = { + CollectibleApproval: CollectibleApproval; + CollectibleBurn: CollectibleBurn; + CollectibleMint: CollectibleMint; + CollectibleTrade: CollectibleTrade; + CollectibleTransfer: CollectibleTransfer; + MetaverseBurn: MetaverseBurn; + MetaverseMint: MetaverseMint; + MetaverseTrade: MetaverseTrade; + MetaverseTransfer: MetaverseTransfer; + SocialComment: SocialComment; + SocialDelete: SocialDelete; + SocialMint: SocialMint; + SocialPost: SocialPost; + SocialProfile: SocialProfile; + SocialProxy: SocialProxy; + SocialRevise: SocialRevise; + SocialReward: SocialReward; + SocialShare: SocialShare; + StakeStaking: StakeStaking; + StakeTransaction: StakeTransaction; + StakerProfitSnapshot: StakerProfitSnapshot; + TransactionApproval: TransactionApproval; + TransactionBridge: TransactionBridge; + TransactionBurn: TransactionBurn; + TransactionEvent: TransactionEvent; + TransactionMint: TransactionMint; + TransactionTransfer: TransactionTransfer; + ExchangeLiquidity: ExchangeLiquidity; + ExchangeStaking: ExchangeStaking; + ExchangeSwap: ExchangeSwap; +}; +export type GetRSS3DataMetadata<FirstKey extends string, SecondKey extends string> = `${Capitalize<FirstKey>}${Capitalize<SecondKey>}` extends keyof RSS3DataModels + ? RSS3DataModels[`${Capitalize<FirstKey>}${Capitalize<SecondKey>}`] + : null; diff --git a/lib/routes/rss3/namespace.ts b/lib/routes/rss3/namespace.ts new file mode 100644 index 00000000000000..382368dddf05d4 --- /dev/null +++ b/lib/routes/rss3/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'RSS3', + url: 'rss3.io', + description: 'The RSS3 Network is the a decentralized network designed to promote the free flow of information on the Open Web .', + lang: 'en', +}; diff --git a/lib/routes/rsshub/namespace.ts b/lib/routes/rsshub/namespace.ts index bdd708414a2958..2cef3ac15d9862 100644 --- a/lib/routes/rsshub/namespace.ts +++ b/lib/routes/rsshub/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'RSSHub', url: 'docs.rsshub.app', + lang: 'en', }; diff --git a/lib/routes/rsshub/routes.ts b/lib/routes/rsshub/routes.ts index 26e4cb9af04814..0090d5c0a280d0 100644 --- a/lib/routes/rsshub/routes.ts +++ b/lib/routes/rsshub/routes.ts @@ -1,12 +1,28 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import { load } from 'cheerio'; +import { Route, ViewType } from '@/types'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; export const route: Route = { path: '/routes/:lang?', - categories: ['program-update'], + categories: ['program-update', 'popular'], + view: ViewType.Notifications, example: '/rsshub/routes/en', - parameters: { lang: 'Language, `zh` means Chinese docs, other values or null means English docs, `en` by default' }, + parameters: { + lang: { + description: 'Language', + options: [ + { + label: 'Chinese', + value: 'zh', + }, + { + label: 'English', + value: 'en', + }, + ], + default: 'en', + }, + }, radar: [ { source: ['docs.rsshub.app/*'], @@ -50,11 +66,9 @@ async function handler(ctx) { ]; const all = await Promise.all( types.map(async (type) => { - const response = await got(`https://docs.rsshub.app/${lang}routes/${type}`); - - const data = response.data; + const response = await ofetch(`https://docs.rsshub.app/${lang}routes/${type}`); - const $ = load(data); + const $ = cheerio.load(response); const page = $('.page').toArray(); const item = $('.routeBlock').toArray(); return { page, item, type }; @@ -68,15 +82,23 @@ async function handler(ctx) { description: isEnglish ? 'Everything is RSSible' : '万物皆可 RSS', language: isEnglish ? 'en-us' : 'zh-cn', item: list.map(({ page, item, type }) => { - const $ = load(page); - item = $(item); - const h2Title = item.prevAll('h2').eq(0); - const h3Title = item.prevAll('h3').eq(0); + const $ = cheerio.load(page); + const $item = $(item); + const h2Title = $item.prevAll('h2').eq(0); + const h3Title = $item.prevAll('h3').eq(0); + + $item.find('.VPBadge').each((_, ele) => { + const $ele = $(ele); + if ($ele.text().includes('Test')) { + $ele.remove(); + } + }); + return { title: `${h2Title.text().trim()} - ${h3Title.text().trim()}`, - description: item.html(), + description: $item.html()?.replaceAll(/<!--.*?-->/g, ''), link: `https://docs.rsshub.app/${lang}routes/${type}#${encodeURIComponent(h2Title.find('.header-anchor').attr('href') && h3Title.find('.header-anchor').attr('href')?.substring(1))}`, - guid: item.attr('id'), + guid: $item.attr('id'), }; }), }; diff --git a/lib/routes/rsshub/transform/html.ts b/lib/routes/rsshub/transform/html.ts index d63c73782c24b1..af1d6471bab62b 100644 --- a/lib/routes/rsshub/transform/html.ts +++ b/lib/routes/rsshub/transform/html.ts @@ -1,8 +1,10 @@ -import { Route } from '@/types'; +import { DataItem, Route } from '@/types'; import got from '@/utils/got'; import { load } from 'cheerio'; import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; +import sanitizeHtml from 'sanitize-html'; +import cache from '@/utils/cache'; export const route: Route = { path: '/transform/html/:url/:routeParams', @@ -23,97 +25,134 @@ export const route: Route = { supportScihub: false, }, name: 'Transformation - HTML', - maintainers: ['ttttmr'], - handler, + maintainers: ['ttttmr', 'hyoban'], description: `Pass URL and transformation rules to convert HTML/JSON into RSS. Specify options (in the format of query string) in parameter \`routeParams\` parameter to extract data from HTML. -| Key | Meaning | Accepted Values | Default | -| ----------------- | -------------------------------------------------------------- | --------------- | ---------------------- | -| \`title\` | The title of the RSS | \`string\` | Extract from \`<title>\` | -| \`item\` | The HTML elements as \`item\` using CSS selector | \`string\` | html | -| \`itemTitle\` | The HTML elements as \`title\` in \`item\` using CSS selector | \`string\` | \`item\` element | -| \`itemTitleAttr\` | The attributes of \`title\` element as title | \`string\` | Element text | -| \`itemLink\` | The HTML elements as \`link\` in \`item\` using CSS selector | \`string\` | \`item\` element | -| \`itemLinkAttr\` | The attributes of \`link\` element as link | \`string\` | \`href\` | -| \`itemDesc\` | The HTML elements as \`descrption\` in \`item\` using CSS selector | \`string\` | \`item\` element | -| \`itemDescAttr\` | The attributes of \`descrption\` element as description | \`string\` | Element html | -| \`itemPubDate\` | The HTML elements as \`pubDate\` in \`item\` using CSS selector | \`string\` | \`item\` element | -| \`itemPubDateAttr\` | The attributes of \`pubDate\` element as pubDate | \`string\` | Element html | +| Key | Meaning | Accepted Values | Default | +| ------------------- | ------------------------------------------------------------------------------------------------------------- | --------------- | ------------------------ | +| \`title\` | The title of the RSS | \`string\` | Extract from \`<title>\` | +| \`item\` | The HTML elements as \`item\` using CSS selector | \`string\` | html | +| \`itemTitle\` | The HTML elements as \`title\` in \`item\` using CSS selector | \`string\` | \`item\` element | +| \`itemTitleAttr\` | The attributes of \`title\` element as title | \`string\` | Element text | +| \`itemLink\` | The HTML elements as \`link\` in \`item\` using CSS selector | \`string\` | \`item\` element | +| \`itemLinkAttr\` | The attributes of \`link\` element as link | \`string\` | \`href\` | +| \`itemDesc\` | The HTML elements as \`descrption\` in \`item\` using CSS selector | \`string\` | \`item\` element | +| \`itemDescAttr\` | The attributes of \`descrption\` element as description | \`string\` | Element html | +| \`itemPubDate\` | The HTML elements as \`pubDate\` in \`item\` using CSS selector | \`string\` | \`item\` element | +| \`itemPubDateAttr\` | The attributes of \`pubDate\` element as pubDate | \`string\` | Element html | +| \`itemContent\` | The HTML elements as \`description\` in \`item\` using CSS selector ( in \`itemLink\` page for full content ) | \`string\` | | +| \`encoding\` | The encoding of the HTML content | \`string\` | utf-8 | Parameters parsing in the above example: - | Parameter | Value | - | ------------- | ----------------------------------------- | - | \`url\` | \`https://wechat2rss.xlab.app/posts/list/\` | - | \`routeParams\` | \`item=div[class='post-content'] p a\` | +| Parameter | Value | +| ------------- | ----------------------------------------- | +| \`url\` | \`https://wechat2rss.xlab.app/posts/list/\` | +| \`routeParams\` | \`item=div[class='post-content'] p a\` | Parsing of \`routeParams\` parameter: - | Parameter | Value | - | --------- | ------------------------------- | - | \`item\` | \`div[class='post-content'] p a\` |`, -}; +| Parameter | Value | +| --------- | ------------------------------- | +| \`item\` | \`div[class='post-content'] p a\` |`, + handler: async (ctx) => { + if (!config.feature.allow_user_supply_unsafe_domain) { + throw new ConfigNotFoundError(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`); + } + const url = ctx.req.param('url'); + const response = await got({ + method: 'get', + url, + responseType: 'arrayBuffer', + }); -async function handler(ctx) { - if (!config.feature.allow_user_supply_unsafe_domain) { - throw new ConfigNotFoundError(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`); - } - const url = ctx.req.param('url'); - const response = await got({ - method: 'get', - url, - }); - - const routeParams = new URLSearchParams(ctx.req.param('routeParams')); - const $ = load(response.data); - const rssTitle = routeParams.get('title') || $('title').text(); - const item = routeParams.get('item') || 'html'; - const items = $(item) - .toArray() - .map((item) => { - try { - item = $(item); - - const titleEle = routeParams.get('itemTitle') ? item.find(routeParams.get('itemTitle')) : item; - const title = routeParams.get('itemTitleAttr') ? titleEle.attr(routeParams.get('itemTitleAttr')) : titleEle.text(); - - let link; - const linkEle = routeParams.get('itemLink') ? item.find(routeParams.get('itemLink')) : item; - if (routeParams.get('itemLinkAttr')) { - link = linkEle.attr(routeParams.get('itemLinkAttr')); - } else { - link = linkEle.is('a') ? linkEle.attr('href') : linkEle.find('a').attr('href'); - } - // 补全绝对链接 - link = link.trim(); - if (link && !link.startsWith('http')) { - link = `${new URL(url).origin}${link}`; + const routeParams = new URLSearchParams(ctx.req.param('routeParams')); + const encoding = routeParams.get('encoding') || 'utf-8'; + const decoder = new TextDecoder(encoding); + + const $ = load(decoder.decode(response.data)); + const rssTitle = routeParams.get('title') || $('title').text(); + const item = routeParams.get('item') || 'html'; + let items: DataItem[] = $(item) + .toArray() + .slice(0, 20) + .map((item) => { + try { + item = $(item); + + const titleEle = routeParams.get('itemTitle') ? item.find(routeParams.get('itemTitle')) : item; + const title = routeParams.get('itemTitleAttr') ? titleEle.attr(routeParams.get('itemTitleAttr')) : titleEle.text(); + + let link; + const linkEle = routeParams.get('itemLink') ? item.find(routeParams.get('itemLink')) : item; + if (routeParams.get('itemLinkAttr')) { + link = linkEle.attr(routeParams.get('itemLinkAttr')); + } else { + link = linkEle.is('a') ? linkEle.attr('href') : linkEle.find('a').attr('href'); + } + // 补全绝对链接或相对链接 + link = link.trim(); + if (link && !link.startsWith('http')) { + link = new URL(link, url).href; + } + + const descEle = routeParams.get('itemDesc') ? item.find(routeParams.get('itemDesc')) : item; + const desc = routeParams.get('itemDescAttr') ? descEle.attr(routeParams.get('itemDescAttr')) : descEle.html(); + + const pubDateEle = routeParams.get('itemPubDate') ? item.find(routeParams.get('itemPubDate')) : item; + const pubDate = routeParams.get('itemPubDateAttr') ? pubDateEle.attr(routeParams.get('itemPubDateAttr')) : pubDateEle.html(); + + return { + title, + link, + description: desc, + pubDate, + }; + } catch { + return null; } + }) + .filter((i) => !!i); - const descEle = routeParams.get('itemDesc') ? item.find(routeParams.get('itemDesc')) : item; - const desc = routeParams.get('itemDescAttr') ? descEle.attr(routeParams.get('itemDescAttr')) : descEle.html(); - - const pubDateEle = routeParams.get('itemPubDate') ? item.find(routeParams.get('itemPubDate')) : item; - const pubDate = routeParams.get('itemPubDateAttr') ? pubDateEle.attr(routeParams.get('itemPubDateAttr')) : pubDateEle.html(); - - return { - title, - link, - description: desc, - pubDate, - }; - } catch { - return null; - } - }) - .filter(Boolean); - - return { - title: rssTitle, - link: url, - description: `Proxy ${url}`, - item: items, - }; -} + const itemContentSelector = routeParams.get('itemContent'); + if (itemContentSelector) { + items = await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(`transform:${item.link}:${itemContentSelector}`, async () => { + const response = await got({ + method: 'get', + url: item.link, + responseType: 'arrayBuffer', + }); + if (!response || typeof response === 'string') { + return item; + } + + const $ = load(decoder.decode(response.data)); + const content = $(itemContentSelector).html(); + if (!content) { + return item; + } + + item.description = sanitizeHtml(content, { allowedTags: [...sanitizeHtml.defaults.allowedTags, 'img'] }); + + return item; + }); + }) + ); + } + + return { + title: rssTitle, + link: url, + description: `Proxy ${url}`, + item: items, + }; + }, +}; diff --git a/lib/routes/rsshub/transform/json.ts b/lib/routes/rsshub/transform/json.ts index ec4ba9601d8982..09d1af6019ed03 100644 --- a/lib/routes/rsshub/transform/json.ts +++ b/lib/routes/rsshub/transform/json.ts @@ -39,34 +39,35 @@ export const route: Route = { handler, description: `Specify options (in the format of query string) in parameter \`routeParams\` parameter to extract data from JSON. -| Key | Meaning | Accepted Values | Default | -| ------------- | ---------------------------------------- | --------------- | ------------------------------------------ | -| \`title\` | The title of the RSS | \`string\` | Extracted from home page of current domain | -| \`item\` | The JSON Path as \`item\` element | \`string\` | Entire JSON response | -| \`itemTitle\` | The JSON Path as \`title\` in \`item\` | \`string\` | None | -| \`itemLink\` | The JSON Path as \`link\` in \`item\` | \`string\` | None | -| \`itemDesc\` | The JSON Path as \`description\` in \`item\` | \`string\` | None | -| \`itemPubDate\` | The JSON Path as \`pubDate\` in \`item\` | \`string\` | None | +| Key | Meaning | Accepted Values | Default | +| ------------------ | -------------------------------------------- | ----------------- | ------------------------------------------ | +| \`title\` | The title of the RSS | \`string\` | Extracted from home page of current domain | +| \`item\` | The JSON Path as \`item\` element | \`string\` | Entire JSON response | +| \`itemTitle\` | The JSON Path as \`title\` in \`item\` | \`string\` | None | +| \`itemLink\` | The JSON Path as \`link\` in \`item\` | \`string\` | None | +| \`itemLinkPrefix\` | Optional Prefix for \`itemLink\` value | \`string\` | None | +| \`itemDesc\` | The JSON Path as \`description\` in \`item\` | \`string\` | None | +| \`itemPubDate\` | The JSON Path as \`pubDate\` in \`item\` | \`string\` | None | -:::tip +::: tip JSON Path only supports format like \`a.b.c\`. if you need to access arrays, like \`a[0].b\`, you can write it as \`a.0.b\`. ::: Parameters parsing in the above example: - | Parameter | Value | - | ------------- | ------------------------------------------------------------------------ | - | \`url\` | \`https://api.github.com/repos/ginuerzh/gost/releases\` | - | \`routeParams\` | \`title=Gost releases&itemTitle=tag_name&itemLink=html_url&itemDesc=body\` | +| Parameter | Value | +| ------------- | ------------------------------------------------------------------------ | +| \`url\` | \`https://api.github.com/repos/ginuerzh/gost/releases\` | +| \`routeParams\` | \`title=Gost releases&itemTitle=tag_name&itemLink=html_url&itemDesc=body\` | Parsing of \`routeParams\` parameter: - | Parameter | Value | - | ----------- | --------------- | - | \`title\` | \`Gost releases\` | - | \`itemTitle\` | \`tag_name\` | - | \`itemLink\` | \`html_url\` | - | \`itemDesc\` | \`body\` |`, +| Parameter | Value | +| ----------- | --------------- | +| \`title\` | \`Gost releases\` | +| \`itemTitle\` | \`tag_name\` | +| \`itemLink\` | \`html_url\` | +| \`itemDesc\` | \`body\` |`, }; async function handler(ctx) { @@ -92,6 +93,11 @@ async function handler(ctx) { const items = jsonGet(response.data, routeParams.get('item')).map((item) => { let link = jsonGet(item, routeParams.get('itemLink')).trim(); + const linkPrefix = routeParams.get('itemLinkPrefix'); + + if (link && linkPrefix) { + link = `${linkPrefix}${link}`; + } // 补全绝对链接 if (link && !link.startsWith('http')) { link = `${new URL(url).origin}${link}`; diff --git a/lib/routes/ruancan/namespace.ts b/lib/routes/ruancan/namespace.ts index baf1c61d2b388a..4f411daa5aada7 100644 --- a/lib/routes/ruancan/namespace.ts +++ b/lib/routes/ruancan/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '软餐', url: 'ruancan.com', + lang: 'zh-CN', }; diff --git a/lib/routes/ruankao/namespace.ts b/lib/routes/ruankao/namespace.ts new file mode 100644 index 00000000000000..63d8e48e6d6373 --- /dev/null +++ b/lib/routes/ruankao/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国计算机职业技术资格考试', + url: 'www.ruankao.org.cn', +}; diff --git a/lib/routes/ruankao/news.ts b/lib/routes/ruankao/news.ts new file mode 100644 index 00000000000000..1d0cff612ccd87 --- /dev/null +++ b/lib/routes/ruankao/news.ts @@ -0,0 +1,106 @@ +import type { DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +const BASE_URL = 'https://www.ruankao.org.cn/index/work.html'; + +const removeFontPresetting = (html: string = ''): string => { + const $ = load(html); + $('[style]').each((_, element) => { + const style = $(element).attr('style') || ''; + const cleanedStyle = style.replaceAll(/font-family:[^;]*;?/gi, '').trim(); + $(element).attr('style', cleanedStyle || null); + }); + $('style').each((_, styleElement) => { + const cssText = $(styleElement).html() || ''; + const cleanedCssText = cssText.replaceAll(/font-family:[^;]*;?/gi, ''); + $(styleElement).html(cleanedCssText); + }); + + return $.html(); +}; + +const handler: Route['handler'] = async () => { + // Fetch the index page + const { data: listResponse } = await got(BASE_URL); + const $ = load(listResponse); + + // Select all list items containing news information + const ITEM_SELECTOR = 'ul[class*="newsList"] > li'; + const listItems = $(ITEM_SELECTOR); + + // Map through each list item to extract details + const contentLinkList = listItems.toArray().map((element) => { + const date = $(element).find('label.time').text().trim().slice(1, -1); + const title = $(element).find('a').attr('title')!; + const link = $(element).find('a').attr('href')!; + + const formattedDate = parseDate(date); + return { + date: formattedDate, + title, + link, + }; + }); + + return { + title: '计算机职业技术资格考试(软考)动态', + description: '计算机职业技术资格考试(软考)消息推送', + link: BASE_URL, + image: 'https://bm.ruankao.org.cn/asset/image/public/logo.png', + item: (await Promise.all( + contentLinkList.map((item) => + cache.tryGet(item.link, async () => { + const CONTENT_SELECTOR = '#contentTxt'; + const { data: contentResponse } = await got(item.link); + const contentPage = load(contentResponse); + const content = removeFontPresetting(contentPage(CONTENT_SELECTOR).html() || ''); + return { + title: item.title, + pubDate: item.date, + link: item.link, + description: content, + category: ['study'], + guid: item.link, + id: item.link, + image: 'https://bm.ruankao.org.cn/asset/image/public/logo.png', + content, + updated: item.date, + language: 'zh-CN', + }; + }) + ) + )) as DataItem[], + allowEmpty: true, + language: 'zh-CN', + feedLink: 'https://rsshub.app/ruankao/news', + id: 'https://rsshub.app/ruankao/news', + }; +}; + +export const route: Route = { + path: '/news', + name: '软考动态', + description: '**注意:** 官方网站限制了国外网络请求,可能需要通过部署在中国大陆内的 RSSHub 实例访问。', + maintainers: ['PrinOrange'], + handler, + categories: ['study'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + supportRadar: true, + }, + radar: [ + { + title: '计算机职业技术资格考试(软考)动态', + source: ['www.ruankao.org.cn/index/work', 'www.ruankao.org.cn'], + target: `/news`, + }, + ], + example: '/ruankao/news', +}; diff --git a/lib/routes/ruc/ai.ts b/lib/routes/ruc/ai.ts new file mode 100644 index 00000000000000..349137a8b7eba2 --- /dev/null +++ b/lib/routes/ruc/ai.ts @@ -0,0 +1,87 @@ +import { 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: '/ai/:category?', + categories: ['university'], + example: '/ruc/ai', + parameters: { category: '分类,见下方说明,默认为首页公告' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['ai.ruc.edu.cn/'], + }, + ], + name: '高瓴人工智能学院', + maintainers: ['yinhanyan'], + handler: async (ctx) => { + const category = ctx.req.param('category')?.replace(/-/g, '/') ?? 'newslist/notice'; + const baseURL = `http://ai.ruc.edu.cn/${category}/`; + const indexUrl = baseURL + 'index.htm'; + const response = await ofetch(indexUrl); + const $ = load(response); + const pageTitle = $('title').text(); + const list = $('div.fr li') + .toArray() + .map((item) => { + item = $(item); + const a = item.find('a').first(); + const link = baseURL + a.attr('href'); + return { + link, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + try { + const response = await ofetch(item.link); + const $ = load(response); + const title = $('title').text(); + item.title = title; + const titleDiv = $('div.tit'); + const date = titleDiv.find('span').first().text(); + item.pubDate = timezone(parseDate(/\d+-\d+-\d+/.exec(date)[0]), +8); + const frame = $('div.fr'); + item.description = frame + .children() + .slice(3) + .map((i, el) => $.html(el)) + .get() + .join(''); + } catch { + item.description = ''; + } + + return item; + }) + ) + ); + + return { + title: pageTitle, + link: indexUrl, + icon: 'https://www.ruc.edu.cn/favicon.ico', + logo: 'http://ai.ruc.edu.cn/images/cn_ruc_logo.png', + item: items, + }; + }, + url: 'ai.ruc.edu.cn/', + description: `::: tip + 分类字段处填写的是对应中国人民大学高瓴人工智能学院分类页网址中介于 **\`http://ai.ruc.edu.cn/\`** 和 **/index.htm** 中间的一段,并将其中的 \`/\` 修改为 \`-\`。 + + 如 [中国人民大学高瓴人工智能学院 - 新闻公告 - 学院新闻](http://ai.ruc.edu.cn/newslist/newsdetail/index.htm) 的网址为 \`http://ai.ruc.edu.cn/newslist/newsdetail/index.htm\` 其中介于 **\`http://ai.ruc.edu.cn/\`** 和 **/index.htm** 中间的一段为 \`newslist/newsdetail\`。随后,并将其中的 \`/\` 修改为 \`-\`,可以得到 \`newslist-newsdetail\`。所以最终我们的路由为 [\`/ruc/ai/newslist-newsdetail\`](https://rsshub.app/ruc/ai/newslist-newsdetail) +:::`, +}; diff --git a/lib/routes/ruc/hr.ts b/lib/routes/ruc/hr.ts index d56dee1e1a1fba..ac9ef700b140c3 100644 --- a/lib/routes/ruc/hr.ts +++ b/lib/routes/ruc/hr.ts @@ -26,11 +26,11 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'hr.ruc.edu.cn/', - description: `:::tip + description: `::: tip 分类字段处填写的是对应中国人民大学人事处分类页网址中介于 **\`http://hr.ruc.edu.cn/\`** 和 **/index.htm** 中间的一段,并将其中的 \`/\` 修改为 \`-\`。 如 [中国人民大学人事处 - 办事机构 - 教师事务办公室 - 教师通知公告](http://hr.ruc.edu.cn/bsjg/bsjsswbgs/jstzgg/index.htm) 的网址为 \`http://hr.ruc.edu.cn/bsjg/bsjsswbgs/jstzgg/index.htm\` 其中介于 **\`http://hr.ruc.edu.cn/\`** 和 **/index.htm** 中间的一段为 \`bsjg/bsjsswbgs/jstzgg\`。随后,并将其中的 \`/\` 修改为 \`-\`,可以得到 \`bsjg-bsjsswbgs-jstzgg\`。所以最终我们的路由为 [\`/ruc/hr/bsjg-bsjsswbgs-jstzgg\`](https://rsshub.app/ruc/hr/bsjg-bsjsswbgs-jstzgg) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/ruc/namespace.ts b/lib/routes/ruc/namespace.ts index 5f6d61edbf10b3..c7b15e9ca4d84f 100644 --- a/lib/routes/ruc/namespace.ts +++ b/lib/routes/ruc/namespace.ts @@ -1,6 +1,10 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: '中国人民大学', - url: 'hr.ruc.edu.cn', + name: 'Renmin University of China', + url: 'ruc.edu.cn', + zh: { + name: '中国人民大学', + }, + lang: 'zh-CN', }; diff --git a/lib/routes/runtrail/namespace.ts b/lib/routes/runtrail/namespace.ts index 015a9d01030382..e66907fda59792 100644 --- a/lib/routes/runtrail/namespace.ts +++ b/lib/routes/runtrail/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '跑野大爆炸', url: 'runtrail.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/rustcc/namespace.ts b/lib/routes/rustcc/namespace.ts index 61a89cbbc4c896..a3e789a06ffaf5 100644 --- a/lib/routes/rustcc/namespace.ts +++ b/lib/routes/rustcc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Rust 语言中文社区', url: 'rustcc.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sakurazaka46/blog.ts b/lib/routes/sakurazaka46/blog.ts index f3c3061c96aa15..2bc756d95bacb7 100644 --- a/lib/routes/sakurazaka46/blog.ts +++ b/lib/routes/sakurazaka46/blog.ts @@ -23,42 +23,42 @@ export const route: Route = { handler, description: `Member ID - | Member ID | Name | - | --------- | ------------ | - | 2000 | 三期生リレー | - | 69 | 山下 瞳月 | - | 68 | 村山 美羽 | - | 67 | 村井 優 | - | 66 | 向井 純葉 | - | 65 | 的野 美青 | - | 64 | 中嶋 優月 | - | 63 | 谷口 愛季 | - | 62 | 小島 凪紗 | - | 61 | 小田倉 麗奈 | - | 60 | 遠藤 理子 | - | 59 | 石森 璃花 | - | 58 | 守屋 麗奈 | - | 57 | 増本 綺良 | - | 56 | 幸阪 茉里乃 | - | 55 | 大沼 晶保 | - | 54 | 大園 玲 | - | 53 | 遠藤 光莉 | - | 51 | 山﨑 天 | - | 50 | 森田 ひかる | - | 48 | 松田 里奈 | - | 47 | 藤吉 夏鈴 | - | 46 | 田村 保乃 | - | 45 | 武元 唯衣 | - | 44 | 関 有美子 | - | 43 | 井上 梨名 | - | 15 | 原田 葵 | - | 14 | 土生 瑞穂 | - | 11 | 菅井 友香 | - | 08 | 齋藤 冬優花 | - | 07 | 小林 由依 | - | 06 | 小池 美波 | - | 04 | 尾関 梨香 | - | 03 | 上村 莉菜 |`, +| Member ID | Name | +| --------- | ------------ | +| 2000 | 三期生リレー | +| 69 | 山下 瞳月 | +| 68 | 村山 美羽 | +| 67 | 村井 優 | +| 66 | 向井 純葉 | +| 65 | 的野 美青 | +| 64 | 中嶋 優月 | +| 63 | 谷口 愛季 | +| 62 | 小島 凪紗 | +| 61 | 小田倉 麗奈 | +| 60 | 遠藤 理子 | +| 59 | 石森 璃花 | +| 58 | 守屋 麗奈 | +| 57 | 増本 綺良 | +| 56 | 幸阪 茉里乃 | +| 55 | 大沼 晶保 | +| 54 | 大園 玲 | +| 53 | 遠藤 光莉 | +| 51 | 山﨑 天 | +| 50 | 森田 ひかる | +| 48 | 松田 里奈 | +| 47 | 藤吉 夏鈴 | +| 46 | 田村 保乃 | +| 45 | 武元 唯衣 | +| 44 | 関 有美子 | +| 43 | 井上 梨名 | +| 15 | 原田 葵 | +| 14 | 土生 瑞穂 | +| 11 | 菅井 友香 | +| 08 | 齋藤 冬優花 | +| 07 | 小林 由依 | +| 06 | 小池 美波 | +| 04 | 尾関 梨香 | +| 03 | 上村 莉菜 |`, }; async function handler(ctx) { diff --git a/lib/routes/sakurazaka46/namespace.ts b/lib/routes/sakurazaka46/namespace.ts index aa773a04c31254..a67805a2929156 100644 --- a/lib/routes/sakurazaka46/namespace.ts +++ b/lib/routes/sakurazaka46/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Sakamichi Series 坂道系列官网资讯', url: 'sakurazaka46.com', + lang: 'zh-CN', }; diff --git a/lib/routes/samd/namespace.ts b/lib/routes/samd/namespace.ts new file mode 100644 index 00000000000000..f1812c2d669086 --- /dev/null +++ b/lib/routes/samd/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '深圳市医疗器械行业协会', + url: 'www.samd.org.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/samd/news.ts b/lib/routes/samd/news.ts new file mode 100644 index 00000000000000..097c42de41e40a --- /dev/null +++ b/lib/routes/samd/news.ts @@ -0,0 +1,74 @@ +import { Route, DataItem } 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 '@/utils/ofetch'; + +const dict = { '434': '行业资讯', '436': '协会动态', '438': '重要通知', '440': '政策法规' }; + +export const route: Route = { + path: '/news/:typeId', + categories: ['government'], + example: '/samd/news/440', + parameters: { type: '文章类型ID,见下表' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + description: `| 行业资讯 | 协会动态 | 重要通知 | 政策法规 | +| --- | --- | --- | --- | +| 434 | 436 | 438 | 440 |`, + name: '资讯信息', + maintainers: ['hualiong'], + handler: async (ctx) => { + const baseURL = 'https://www.samd.org.cn/home'; + const typeId = ctx.req.param('typeId'); + + const { rows } = await ofetch('/GetNewsByTagId', { + baseURL, + method: 'POST', + query: { + page: 1, + rows: 10, + typeId, + status: 1, + }, + }); + + const list: DataItem[] = rows.map((row) => ({ + title: row.title, + category: [row.tag_names], + link: `${baseURL}/newsDetail?id=${row.auto_id}&typeId=${typeId}`, + image: row.img_url ? baseURL + row.img_url : null, + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link!, async () => { + const $ = load(await ofetch(item.link!)); + + const content = $('.content'); + item.author = content.find('.author span').text(); + item.pubDate = timezone(parseDate(content.find('.time').text(), '发布时间:YYYY-MM-DD HH:mm:ss'), +8); + + content.children('.titles').remove(); + content.children('.auxi').remove(); + item.description = content.html()!; + + return item; + }) + ) + ); + + return { + title: `${dict[typeId]} - 深圳市医疗器械行业协会`, + link: 'https://www.samd.org.cn/home/newsList', + item: items as DataItem[], + }; + }, +}; diff --git a/lib/routes/samsung/namespace.ts b/lib/routes/samsung/namespace.ts index e6da53bb293fa5..3fd0bb68b40e13 100644 --- a/lib/routes/samsung/namespace.ts +++ b/lib/routes/samsung/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Samsung', url: 'research.samsung.com', + lang: 'en', }; diff --git a/lib/routes/sara/index.ts b/lib/routes/sara/index.ts index 1b85c6371580ea..53ad1de1812c1e 100644 --- a/lib/routes/sara/index.ts +++ b/lib/routes/sara/index.ts @@ -1,7 +1,7 @@ import { Route, DataItem } from '@/types'; import cache from '@/utils/cache'; import { load } from 'cheerio'; -import { ofetch } from 'ofetch'; +import ofetch from '@/utils/ofetch'; const typeMap = { dynamic: '协会动态', @@ -23,8 +23,8 @@ export const route: Route = { supportScihub: false, }, description: `| 协会动态 | 通知公告 |行业动态 | - | -------- | ------------ | -------- | - | dynamic | announcement | industry |`, +| -------- | ------------ | -------- | +| dynamic | announcement | industry |`, name: '新闻资讯', maintainers: ['HChenZi'], diff --git a/lib/routes/sara/namespace.ts b/lib/routes/sara/namespace.ts index 71ef8ba5f0bb25..bdf15001debb4e 100644 --- a/lib/routes/sara/namespace.ts +++ b/lib/routes/sara/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海业余无线电协会', url: 'www.sara.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/saraba1st/namespace.ts b/lib/routes/saraba1st/namespace.ts index a975ccee988a74..106c418ed9f688 100644 --- a/lib/routes/saraba1st/namespace.ts +++ b/lib/routes/saraba1st/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Saraba1st', url: 'bbs.saraba1st.com', + lang: 'zh-CN', }; diff --git a/lib/routes/sass/gs/index.ts b/lib/routes/sass/gs/index.ts index 507ba1181e94ce..6400849adcf2e7 100644 --- a/lib/routes/sass/gs/index.ts +++ b/lib/routes/sass/gs/index.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['yanbot-team'], handler, description: `| 硕士统考招生 | 硕士推免招生 | - | ------------ | ------------ | - | 1793 | sstmzs |`, +| ------------ | ------------ | +| 1793 | sstmzs |`, }; async function handler(ctx) { diff --git a/lib/routes/sass/namespace.ts b/lib/routes/sass/namespace.ts index 9e100f2c5e46c7..3aff89aedbc384 100644 --- a/lib/routes/sass/namespace.ts +++ b/lib/routes/sass/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海社会科学院', url: 'gs.sass.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/scau/namespace.ts b/lib/routes/scau/namespace.ts index 568a6280536dd9..b1e632d8fb3b82 100644 --- a/lib/routes/scau/namespace.ts +++ b/lib/routes/scau/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '华南农业大学', url: 'yzb.scau.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/science/blogs.ts b/lib/routes/science/blogs.ts index 86049f484c8d6c..1ddd29ec81f8cb 100644 --- a/lib/routes/science/blogs.ts +++ b/lib/routes/science/blogs.ts @@ -52,8 +52,8 @@ async function handler(ctx) { const response = await page.content(); - page.close(); - browser.close(); + await page.close(); + await browser.close(); return response; }, config.cache.routeExpire, @@ -68,9 +68,15 @@ async function handler(ctx) { return { title: item.find('title').text().trim(), link: item.find('link').text().trim(), - author: item.find('dc\\:creator').text().trim(), + author: item + .find(String.raw`dc\:creator`) + .text() + .trim(), pubDate: parseDate(item.find('pubDate').text().trim()), - description: item.find('content\\:encoded').text().trim(), + description: item + .find(String.raw`content\:encoded`) + .text() + .trim(), }; }); diff --git a/lib/routes/science/current.ts b/lib/routes/science/current.ts index 6d74a64d9c4b02..759421fc485109 100644 --- a/lib/routes/science/current.ts +++ b/lib/routes/science/current.ts @@ -37,13 +37,13 @@ export const route: Route = { maintainers: ['y9c', 'TonyRL'], handler, description: `| Short name | Full name of the journal | Route | - | :---------: | :----------------------------: | ------------------------------------------------------------------------------ | - | science | Science | [/science/current/science](https://rsshub.app/science/current/science) | - | sciadv | Science Advances | [/science/current/sciadv](https://rsshub.app/science/current/sciadv) | - | sciimmunol | Science Immunology | [/science/current/sciimmunol](https://rsshub.app/science/current/sciimmunol) | - | scirobotics | Science Robotics | [/science/current/scirobotics](https://rsshub.app/science/current/scirobotics) | - | signaling | Science Signaling | [/science/current/signaling](https://rsshub.app/science/current/signaling) | - | stm | Science Translational Medicine | [/science/current/stm](https://rsshub.app/science/current/stm) | +| :---------: | :----------------------------: | ------------------------------------------------------------------------------ | +| science | Science | [/science/current/science](https://rsshub.app/science/current/science) | +| sciadv | Science Advances | [/science/current/sciadv](https://rsshub.app/science/current/sciadv) | +| sciimmunol | Science Immunology | [/science/current/sciimmunol](https://rsshub.app/science/current/sciimmunol) | +| scirobotics | Science Robotics | [/science/current/scirobotics](https://rsshub.app/science/current/scirobotics) | +| signaling | Science Signaling | [/science/current/signaling](https://rsshub.app/science/current/signaling) | +| stm | Science Translational Medicine | [/science/current/stm](https://rsshub.app/science/current/stm) | - Using route (\`/science/current/\` + "short name for a journal") to get current issue of a journal from AAAS. - Leaving it empty (\`/science/current\`) to get update from Science.`, diff --git a/lib/routes/science/namespace.ts b/lib/routes/science/namespace.ts index f648fcebf179ce..52a2ff2a01c305 100644 --- a/lib/routes/science/namespace.ts +++ b/lib/routes/science/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Science Magazine', url: 'science.org', + lang: 'en', }; diff --git a/lib/routes/sciencedirect/call-for-paper.ts b/lib/routes/sciencedirect/call-for-paper.ts new file mode 100644 index 00000000000000..b63e0df9cff6d3 --- /dev/null +++ b/lib/routes/sciencedirect/call-for-paper.ts @@ -0,0 +1,80 @@ +import { Route } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { art } from '@/utils/render'; +import path from 'node:path'; +const __dirname = getCurrentPath(import.meta.url); + +export const route: Route = { + path: '/call-for-paper/:subject', + categories: ['journal'], + example: '/sciencedirect/call-for-paper/education', + parameters: { + subject: '学科分类,例如“education”', + }, + radar: [ + { + source: ['sciencedirect.com'], + }, + ], + name: 'Call for Papers', + maintainers: ['etShaw-zh'], + handler, + url: 'sciencedirect.com/browse/calls-for-papers', + description: '`sciencedirect.com/browse/calls-for-papers?subject=education` -> `/sciencedirect/call-for-paper/education`', +}; + +async function handler(ctx) { + const { subject = '' } = ctx.req.param(); + const apiUrl = `https://www.sciencedirect.com/browse/calls-for-papers?subject=${subject}`; + const headers = { + accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', // need this to avoid 403, 503 error + }; + const response = await got(apiUrl, { headers }); + const $ = load(response.body); + + const scriptJSON = $('script[data-iso-key="_0"]').text(); + if (!scriptJSON) { + throw new Error('Cannot find the script with data-iso-key="_0"'); + } + + let data; + try { + data = JSON.parse(JSON.parse(scriptJSON)); + } catch (error: any) { + throw new Error(`Failed to parse embedded script JSON: ${error.message}`); + } + + const cfpList = data?.callsForPapers?.list || []; + if (!cfpList.length) { + throw new Error('No Calls for Papers found'); + } + + const items = cfpList.map((cfp) => { + const link = `https://www.sciencedirect.com/special-issue/${cfp.contentId}/${cfp.url}`; + const description = art(path.join(__dirname, 'templates/description.art'), { + summary: cfp.summary, + submissionDeadline: cfp.submissionDeadline, + displayName: cfp.journal.displayName, + impactFactor: cfp.journal.impactFactor, + citeScore: cfp.journal.citeScore, + }); + + return { + title: cfp.title, + author: `${cfp.journal.displayName} (IF: ${cfp.journal.impactFactor})`, + link, + description, + pubDate: cfp.submissionDeadline || '', + }; + }); + + return { + title: `ScienceDirect Calls for Papers - ${subject}`, + link: apiUrl, + description: `Calls for Papers on ScienceDirect for subject: ${subject}`, + item: items, + }; +} diff --git a/lib/routes/sciencedirect/cf-email.ts b/lib/routes/sciencedirect/cf-email.ts index 50205dadb3d18b..39f7cc651d648e 100644 --- a/lib/routes/sciencedirect/cf-email.ts +++ b/lib/routes/sciencedirect/cf-email.ts @@ -23,7 +23,6 @@ const encodeCFEmail = (email) => { * The alogrithm is well-explained in https://andrewlock.net/simple-obfuscation-of-email-addresses-using-javascript/ */ export { - // eslint-disable-next-line lines-around-comment /** * Returns decoded email address using CloudFlare's email address obfuscation. * @param {String} encoded - encoded email, (`cfemail` attribute in `__cf_email__` tag) diff --git a/lib/routes/sciencedirect/current-issue.ts b/lib/routes/sciencedirect/current-issue.ts new file mode 100644 index 00000000000000..b031b490303da5 --- /dev/null +++ b/lib/routes/sciencedirect/current-issue.ts @@ -0,0 +1,93 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import sanitizeHtml from 'sanitize-html'; +import { config } from '@/config'; + +export const route: Route = { + path: '/journal/:id/current', + categories: ['journal'], + example: '/sciencedirect/journal/journal-of-financial-economics/current', + parameters: { id: 'Journal id, can be found in URL' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['sciencedirect.com/journal/:id', 'sciencedirect.com/'], + }, + ], + name: 'Current Issue', + maintainers: ['TonyRL'], + handler, +}; + +async function handler(ctx) { + const id = ctx.req.param('id'); + + const baseUrl = 'https://www.sciencedirect.com'; + const currentUrl = `${baseUrl}/journal/${id}`; + + const pageResponse = await ofetch(currentUrl, { + headers: { + 'User-Agent': config.trueUA, + }, + }); + const $page = load(pageResponse); + const pageData = JSON.parse(JSON.parse($page('script[type="application/json"]').text())); + + const issueUrl = `${currentUrl}${pageData.latestIssues.issues[0].uriLookup}`; + const issueResponse = await ofetch(issueUrl, { + headers: { + 'User-Agent': config.trueUA, + }, + }); + const $issue = load(issueResponse); + + const issueData = JSON.parse(JSON.parse($issue('script[type="application/json"]').text())); + const titleMetadata = issueData.titleMetadata; + const currentIssue = issueData.articles.ihp.data; + + const list = currentIssue.issueBody.issueSec.flatMap((section) => + section.includeItem.map((item) => ({ + title: item.title, + link: `${baseUrl}${item.href}`, + author: item.authors.map((author) => `${author.givenName} ${author.surname}`).join(', '), + doi: item.doi, + pubDate: parseDate(item.coverDateStart), + pii: item.pii, + })) + ); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(`https://www.sciencedirect.com/journal/0304405X/abstract?pii=${item.pii}`, { + headers: { + 'User-Agent': config.trueUA, + }, + }); + + item.description = response.data[0].abstracts[0].html ?? ''; + + return item; + }) + ) + ); + + return { + title: `${titleMetadata.displayName} | ${currentIssue.volIssueSupplementText}, (${currentIssue.coverDateText}) | ScienceDirect.com by Elsevier`, + description: sanitizeHtml(titleMetadata.aimsAndScopeV2, { allowedTags: [], allowedAttributes: {} }), + image: titleMetadata.largeCoverUrl ?? titleMetadata.smallCoverUrl, + link: issueUrl, + item: items, + }; +} diff --git a/lib/routes/sciencedirect/namespace.ts b/lib/routes/sciencedirect/namespace.ts index 9bc71f8052a4e9..6208eff8123831 100644 --- a/lib/routes/sciencedirect/namespace.ts +++ b/lib/routes/sciencedirect/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ScienceDirect', url: 'sciencedirect.com', + lang: 'en', }; diff --git a/lib/routes/sciencedirect/templates/description.art b/lib/routes/sciencedirect/templates/description.art new file mode 100644 index 00000000000000..d1ecbf0ab98a00 --- /dev/null +++ b/lib/routes/sciencedirect/templates/description.art @@ -0,0 +1,5 @@ +<div> + <p><strong>Summary:</strong> {{summary}} </p> + <p><strong>Submission Deadline:</strong> {{submissionDeadline}}</p> + <p><strong>Journal:</strong> {{displayName}} (IF: {{impactFactor}}, CiteScore: {{citeScore}})</p> +</div> diff --git a/lib/routes/sciencenet/blog.ts b/lib/routes/sciencenet/blog.ts index c6d43419d5d9f3..af867f18aab933 100644 --- a/lib/routes/sciencenet/blog.ts +++ b/lib/routes/sciencenet/blog.ts @@ -24,21 +24,21 @@ export const route: Route = { handler, description: `类型 - | 精选 | 最新 | 热门 | - | --------- | ---- | ---- | - | recommend | new | hot | +| 精选 | 最新 | 热门 | +| --------- | ---- | ---- | +| recommend | new | hot | 时间 - | 36 小时内精选博文 | 一周内精选博文 | 一月内精选博文 | 半年内精选博文 | 所有时间精选博文 | - | ----------------- | -------------- | -------------- | -------------- | ---------------- | - | 1 | 2 | 3 | 4 | 5 | +| 36 小时内精选博文 | 一周内精选博文 | 一月内精选博文 | 半年内精选博文 | 所有时间精选博文 | +| ----------------- | -------------- | -------------- | -------------- | ---------------- | +| 1 | 2 | 3 | 4 | 5 | 排序 - | 按发表时间排序 | 按评论数排序 | 按点击数排序 | - | -------------- | ------------ | ------------ | - | 1 | 2 | 3 |`, +| 按发表时间排序 | 按评论数排序 | 按点击数排序 | +| -------------- | ------------ | ------------ | +| 1 | 2 | 3 |`, }; async function handler(ctx) { diff --git a/lib/routes/sciencenet/namespace.ts b/lib/routes/sciencenet/namespace.ts index 2c785b6be15361..85286ea12f5275 100644 --- a/lib/routes/sciencenet/namespace.ts +++ b/lib/routes/sciencenet/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '科学网', url: 'blog.sciencenet.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sciencenet/user.ts b/lib/routes/sciencenet/user.ts index fbf3754566232d..9d4f79239e9195 100644 --- a/lib/routes/sciencenet/user.ts +++ b/lib/routes/sciencenet/user.ts @@ -31,6 +31,7 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 50; const rootUrl = 'https://blog.sciencenet.cn'; const currentUrl = `${rootUrl}/u/${id}`; @@ -51,8 +52,9 @@ async function handler(ctx) { $ = load(response.data); let items = $('item') - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 50) + .slice(-limit) .toArray() + .reverse() .map((item) => { item = $(item); diff --git a/lib/routes/scientificamerican/namespace.ts b/lib/routes/scientificamerican/namespace.ts new file mode 100644 index 00000000000000..1ae9b33482da4c --- /dev/null +++ b/lib/routes/scientificamerican/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Scientific American', + url: 'scientificamerican.com', + categories: ['new-media'], + description: 'Scientific American is the essential guide to the most awe-inspiring advances in science and technology, explaining how they change our understanding of the world and shape our lives.', + lang: 'en', +}; diff --git a/lib/routes/scientificamerican/podcast.ts b/lib/routes/scientificamerican/podcast.ts new file mode 100644 index 00000000000000..c82c1bc214fdc7 --- /dev/null +++ b/lib/routes/scientificamerican/podcast.ts @@ -0,0 +1,270 @@ +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 cache from '@/utils/cache'; +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); + +export const handler = async (ctx: Context): Promise<Data> => { + const { id } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '12', 10); + + const baseUrl: string = 'https://www.scientificamerican.com'; + const targetUrl: string = new URL(`podcast${id ? `/${id}` : 's'}/`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').attr('lang') ?? 'en'; + const data: string | undefined = response.match(/window\.__DATA__=JSON\.parse\(`(.*?)`\)/)?.[1]; + const parsedData = data ? JSON.parse(data.replaceAll('\\\\', '\\')) : undefined; + + let items: DataItem[] = []; + + items = parsedData + ? parsedData.initialData.props.results.slice(0, limit).map((item): DataItem => { + const title: string = item.title; + const image: string | undefined = item.image_url; + const description: string = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: item.image_alt_text || title, + width: item.image_width, + height: item.image_height, + }, + ] + : undefined, + intro: item.summary, + }); + const pubDate: number | string = item.date_published; + const linkUrl: string | undefined = item.url; + const categories: string[] = [...new Set([item.category, item.subtype, item.column, item.digital_column].filter(Boolean))]; + const authors: DataItem['author'] = item.authors.map((author) => ({ + name: author.name, + url: author.url ? new URL(author.url, baseUrl).href : undefined, + avatar: author.picture_file, + })); + const guid: string = `-${item.id}`; + const updated: number | string = item.release_date ?? pubDate; + + let processedItem: DataItem = { + title, + description, + pubDate: pubDate ? timezone(parseDate(pubDate), +8) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + category: categories, + author: authors, + doi: item.article_doi, + guid, + id: guid, + content: { + html: description, + text: item.summary ?? description, + }, + image, + banner: image, + updated: updated ? timezone(parseDate(updated), +8) : undefined, + language, + }; + + const enclosureUrl: string | undefined = item.media_url; + + if (enclosureUrl) { + const enclosureType: string = `audio/${enclosureUrl.replace(/\?.*$/, '').split(/\./).pop()}`; + + processedItem = { + ...processedItem, + enclosure_url: enclosureUrl, + enclosure_type: enclosureType, + enclosure_title: title, + itunes_item_image: image, + }; + } + + return processedItem; + }) + : []; + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + + const detailData: string | undefined = detailResponse.match(/window\.__DATA__=JSON\.parse\(`(.*?)`\)/)?.[1]; + const parsedDetailData = detailData ? JSON.parse(detailData.replaceAll('\\\\', '\\')) : undefined; + + if (!parsedDetailData) { + return item; + } + + const articleData = parsedDetailData.initialData.article; + + const title: string = articleData.title; + const image: string | undefined = articleData.image_url; + const description: string = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: articleData.image_alt_text || title, + width: articleData.image_width, + height: articleData.image_height, + }, + ] + : undefined, + intro: articleData.summary, + content: articleData.content, + }); + const pubDate: number | string = articleData.published_at_date_time; + const categories: string[] = [...new Set([articleData.display_category, articleData.primary_category, articleData.subcategory, ...(articleData.categories ?? []), articleData.podcast_series_name])]; + const authors: DataItem['author'] = articleData.authors.map((author) => ({ + name: author.name, + url: author.url ? new URL(author.url, baseUrl).href : undefined, + avatar: author.picture_file, + })); + const guid: string = `scientificamerican-${articleData.id}`; + const updated: number | string = articleData.updated_at_date_time ?? pubDate; + + let processedItem: DataItem = { + title, + description, + pubDate: pubDate ? timezone(parseDate(pubDate), +8) : undefined, + category: categories, + author: authors, + doi: articleData.article_doi, + guid, + id: guid, + content: { + html: description, + text: articleData.summary ?? description, + }, + image, + banner: image, + updated: updated ? timezone(parseDate(updated), +8) : undefined, + language, + }; + + const enclosureUrl: string | undefined = articleData.media_url; + + if (enclosureUrl) { + const enclosureType: string = `audio/${enclosureUrl.replace(/\?.*$/, '').split(/\./).pop()}`; + + processedItem = { + ...processedItem, + enclosure_url: enclosureUrl, + enclosure_type: enclosureType, + enclosure_title: title, + itunes_item_image: image, + }; + } + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[name="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('meta[property="og:image"]').attr('content'), + author: $('meta[property="og:site_name"]').attr('content'), + language, + feedLink: $('link[type="application/rss+xml"]').attr('href'), + itunes_author: $('meta[property="og:site_name"]').attr('content'), + itunes_category: 'Science', + id: $('meta[property="og:url"]').attr('content'), + }; +}; + +export const route: Route = { + path: '/podcast/:id?', + name: 'Podcasts', + url: 'www.scientificamerican.com', + maintainers: ['nczitzk'], + handler, + example: '/scientificamerican/podcast', + parameters: { + id: 'ID, see below', + }, + description: `:::tip +If you subscribe to [Science Quickly](https://www.scientificamerican.com/podcast/science-quickly/),where the URL is \`https://www.scientificamerican.com/podcast/science-quickly/\`, extract the part \`https://www.scientificamerican.com/podcast/\` to the end, which is \`science-quickly\`, and use it as the parameter to fill in. Therefore, the route will be [\`/scientificamerican/podcast/science-quickly\`](https://rsshub.app/scientificamerican/podcast/science-quickly). +::: + +| All | Science Quickly | Uncertain | +| --- | --------------- | ------------ | +| | science-quickly | science-talk | +`, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.scientificamerican.com/podcasts/', 'www.scientificamerican.com/podcast/:id'], + target: (params) => { + const id: string = params.id; + + return `/scientificamerican/podcast${id ? `/${id}` : ''}`; + }, + }, + { + title: 'Science Quickly', + source: ['www.scientificamerican.com/podcast/science-quickly/'], + target: '/podcast/science-quickly', + }, + { + title: 'Uncertain', + source: ['www.scientificamerican.com/podcast/science-talk/'], + target: '/podcast/science-talk', + }, + ], + view: ViewType.Articles, + + zh: { + path: '/podcast/:id?', + name: 'Podcasts', + url: 'www.scientificamerican.com', + maintainers: ['nczitzk'], + handler, + example: '/scientificamerican/podcast', + parameters: { + id: 'ID,见下表', + }, + description: `:::tip +若订阅 [Science Quickly](https://www.scientificamerican.com/podcast/science-quickly/),网址为 \`https://www.scientificamerican.com/podcast/science-quickly/\`,请截取 \`https://www.scientificamerican.com/podcast/\` 到末尾 \`/\` 的部分 \`science-quickly\` 作为 \`id\` 参数填入,此时目标路由为 [\`/scientificamerican/podcast/science-quickly\`](https://rsshub.app/scientificamerican/podcast/science-quickly)。 +::: + +| 全部 | Science Quickly | Uncertain | +| ---- | --------------- | ------------ | +| | science-quickly | science-talk | +`, + }, +}; diff --git a/lib/routes/scientificamerican/templates/description.art b/lib/routes/scientificamerican/templates/description.art new file mode 100644 index 00000000000000..ec6912ddf58022 --- /dev/null +++ b/lib/routes/scientificamerican/templates/description.art @@ -0,0 +1,31 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} + <figure> + <img + {{ if image.alt }} + alt="{{ image.alt }}" + {{ /if }} + {{ if image.width }} + alt="{{ image.width }}" + {{ /if }} + {{ if image.height }} + alt="{{ image.height }}" + {{ /if }} + src="{{ image.src }}"> + </figure> + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} + {{@ intro }} +{{ /if }} + +{{ if content }} + {{ each content c }} + <{{ c.tag }}> + {{@ c.content }} + </{{ c.tag }}> + {{ /each }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/scmp/coronavirus.ts b/lib/routes/scmp/coronavirus.ts index 7745656b475b16..193267e80f2133 100644 --- a/lib/routes/scmp/coronavirus.ts +++ b/lib/routes/scmp/coronavirus.ts @@ -18,5 +18,5 @@ export const route: Route = { }; function handler(ctx) { - ctx.redirect('/scmp/topics/coronavirus-pandemic-all-stories'); + ctx.set('redirect', '/scmp/topics/coronavirus-pandemic-all-stories'); } diff --git a/lib/routes/scmp/index.ts b/lib/routes/scmp/index.ts index 6c279811b68701..98a3a050da38dc 100644 --- a/lib/routes/scmp/index.ts +++ b/lib/routes/scmp/index.ts @@ -42,8 +42,8 @@ async function handler(ctx) { .map((elem) => { const item = $(elem); const enclosure = item.find('enclosure').first(); - const mediaContent = item.find('media\\:content').toArray()[0]; - const thumbnail = item.find('media\\:thumbnail').toArray()[0]; + const mediaContent = item.find(String.raw`media\:content`).toArray()[0]; + const thumbnail = item.find(String.raw`media\:thumbnail`).toArray()[0]; return { title: item.find('title').text(), description: item.find('description').text(), diff --git a/lib/routes/scmp/namespace.ts b/lib/routes/scmp/namespace.ts index be4d0f4add9c33..b3d7c1d98522fc 100644 --- a/lib/routes/scmp/namespace.ts +++ b/lib/routes/scmp/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Corona Virus Disease 2019', url: 'scmp.com', + lang: 'en', }; diff --git a/lib/routes/scmp/utils.ts b/lib/routes/scmp/utils.ts index 4012480bf9d692..ddfa87762c37f8 100644 --- a/lib/routes/scmp/utils.ts +++ b/lib/routes/scmp/utils.ts @@ -1,6 +1,6 @@ import { load } from 'cheerio'; -import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; export const renderHTML = (node) => { if (!node) { @@ -60,7 +60,7 @@ export const renderHTML = (node) => { }; export const parseItem = async (item) => { - const { data: response, url } = await got(item.link); + const { _data: response, url } = await ofetch.raw(item.link); if (new URL(url).hostname !== 'www.scmp.com') { // e.g., https://multimedia.scmp.com/ diff --git a/lib/routes/scnu/namespace.ts b/lib/routes/scnu/namespace.ts index d6cbfa5fbe587f..80b218b0bf3b18 100644 --- a/lib/routes/scnu/namespace.ts +++ b/lib/routes/scnu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '华南师范大学', url: 'cs.scnu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sctv/namespace.ts b/lib/routes/sctv/namespace.ts index 52e9a6ec556cea..75121d6ec55744 100644 --- a/lib/routes/sctv/namespace.ts +++ b/lib/routes/sctv/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '四川广播电视台', url: 'sctv.com', + lang: 'zh-CN', }; diff --git a/lib/routes/sctv/programme.ts b/lib/routes/sctv/programme.ts index 3a9dc1143320fb..4115037f9d6fef 100644 --- a/lib/routes/sctv/programme.ts +++ b/lib/routes/sctv/programme.ts @@ -25,59 +25,59 @@ export const route: Route = { name: '电视回放', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 参数 **是否仅获取完整视频** 设置为 \`true\` \`yes\` \`t\` \`y\` 等值后,路由仅返回当期节目的完整视频,而不会返回节目所提供的节选视频。 查看更多电视节目请前往 [电视回放](https://www.sctv.com/column/list) - ::: - - | 节目 | id | - | ---------------------- | ------- | - | 四川新闻联播 | 1 | - | 早安四川 | 2 | - | 今日视点 | 3 | - | 龙门阵摆四川 | 10523 | - | 非常话题 | 1014756 | - | 新闻现场 | 8385 | - | 黄金三十分 | 8386 | - | 全媒直播间 | 8434 | - | 晚报十点半 | 8435 | - | 现场快报 | 8436 | - | 四川乡村新闻 | 3673 | - | 四川文旅报道 | 8174 | - | 乡村会客厅 | 3674 | - | 金字招牌 | 3675 | - | 问您所 “?” | 3677 | - | 蜀你最能 | 3679 | - | 美丽乡村印象 | 3678 | - | 美丽乡村 | 3676 | - | 乡村大篷车 | 3680 | - | 华西论健 | 3681 | - | 乡村聚乐部 | 3682 | - | 医保近距离 | 6403 | - | 音你而来 | 7263 | - | 吃八方 | 7343 | - | 世界那么大 | 7344 | - | 风云川商 | 7345 | - | 麻辣烫 | 7346 | - | 财经快报 | 7473 | - | 医生来了 | 7873 | - | 安逸的旅途 | 8383 | - | 运动 + | 8433 | - | 好戏连台 | 9733 | - | 防癌大讲堂 | 1018673 | - | 消费新观察 | 1017153 | - | 天天耍大牌 | 1014753 | - | 廉洁四川 | 1014754 | - | 看世界 | 1014755 | - | 金熊猫说教育(资讯版) | 1014757 | - | 她说 | 1014759 | - | 嗨宝贝 | 1014762 | - | 萌眼看世界 | 1014764 | - | 乡村大讲堂 | 1014765 | - | 四川党建 | 1014766 | - | 健康四川 | 1014767 | - | 技能四川 | 12023 |`, +::: + +| 节目 | id | +| ---------------------- | ------- | +| 四川新闻联播 | 1 | +| 早安四川 | 2 | +| 今日视点 | 3 | +| 龙门阵摆四川 | 10523 | +| 非常话题 | 1014756 | +| 新闻现场 | 8385 | +| 黄金三十分 | 8386 | +| 全媒直播间 | 8434 | +| 晚报十点半 | 8435 | +| 现场快报 | 8436 | +| 四川乡村新闻 | 3673 | +| 四川文旅报道 | 8174 | +| 乡村会客厅 | 3674 | +| 金字招牌 | 3675 | +| 问您所 “?” | 3677 | +| 蜀你最能 | 3679 | +| 美丽乡村印象 | 3678 | +| 美丽乡村 | 3676 | +| 乡村大篷车 | 3680 | +| 华西论健 | 3681 | +| 乡村聚乐部 | 3682 | +| 医保近距离 | 6403 | +| 音你而来 | 7263 | +| 吃八方 | 7343 | +| 世界那么大 | 7344 | +| 风云川商 | 7345 | +| 麻辣烫 | 7346 | +| 财经快报 | 7473 | +| 医生来了 | 7873 | +| 安逸的旅途 | 8383 | +| 运动 + | 8433 | +| 好戏连台 | 9733 | +| 防癌大讲堂 | 1018673 | +| 消费新观察 | 1017153 | +| 天天耍大牌 | 1014753 | +| 廉洁四川 | 1014754 | +| 看世界 | 1014755 | +| 金熊猫说教育(资讯版) | 1014757 | +| 她说 | 1014759 | +| 嗨宝贝 | 1014762 | +| 萌眼看世界 | 1014764 | +| 乡村大讲堂 | 1014765 | +| 四川党建 | 1014766 | +| 健康四川 | 1014767 | +| 技能四川 | 12023 |`, }; async function handler(ctx) { diff --git a/lib/routes/scu/jwc/tzgg.ts b/lib/routes/scu/jwc/tzgg.ts new file mode 100644 index 00000000000000..dcc7383e3dc7f2 --- /dev/null +++ b/lib/routes/scu/jwc/tzgg.ts @@ -0,0 +1,75 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; // 可以使用类似 jQuery 的 API HTML 解析器 + +const baseUrl = 'https://jwc.scu.edu.cn/tzgg.htm'; +const baseIndexUrl = 'https://jwc.scu.edu.cn/'; + +export const route: Route = { + path: '/jwc', + categories: ['university'], + example: '/scu/jwc', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['jwc.scu.edu.cn'], + target: '/jwc', + }, + ], + name: '教务处通知公告', + maintainers: ['Kyle-You'], + handler, +}; + +async function handler() { + const { data: response } = await got.get(baseUrl); + const $ = load(response); + + const links: string[] = $('.tz-list ul li a') + .toArray() + .map((item) => { + item = $(item); + const link: string = item.attr('href'); + return link.startsWith('http') ? link : baseIndexUrl + link; + }); + + const items = await Promise.all( + links.map((link) => + cache.tryGet(link, async () => { + const { data: info } = await got.get(link); + const $ = load(info); + + // 获取head里的meta标签 + const title = $('head meta[name="ArticleTitle"]').attr('content') ?? ''; + const pubDate = parseDate($('head meta[name="PubDate"]').attr('content') ?? ''); + const description = $('.v_news_content').html(); + return { + title, + link, + pubDate, + description, + }; + }) + ) + ); + + return { + title: '四川大学教务处', + link: baseIndexUrl, + description: '四川大学教务处通知公告', + item: items, + language: 'zh-cn', + image: 'https://www.scu.edu.cn/__local/B/67/25/DFAF986CCD6529E52D7830F180D_C37C7DEE_4340.png', + logo: 'https://www.scu.edu.cn/__local/B/67/25/DFAF986CCD6529E52D7830F180D_C37C7DEE_4340.png', + icon: 'https://www.scu.edu.cn/__local/B/67/25/DFAF986CCD6529E52D7830F180D_C37C7DEE_4340.png', + }; +} diff --git a/lib/routes/scu/namespace.ts b/lib/routes/scu/namespace.ts new file mode 100644 index 00000000000000..75b8db5fdb8e5b --- /dev/null +++ b/lib/routes/scu/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '四川大学', + url: 'www.scu.edu.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/scu/scupi/_utils.ts b/lib/routes/scu/scupi/_utils.ts new file mode 100644 index 00000000000000..295220cfc39554 --- /dev/null +++ b/lib/routes/scu/scupi/_utils.ts @@ -0,0 +1,62 @@ +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { config } from '@/config'; + +export async function getNotifList() { + try { + const response = await got.get('https://scupi.scu.edu.cn/activities/notice', { + headers: { + 'User-Agent': config.ua, + }, + }); + const html = response.body; + const $ = load(html); + + const listElement = $('body > div.wrapper > main > section > div > div > div.news > div > ul'); + return listElement + .find('article') + .toArray() + .map((articleElement) => { + const titleElement = $(articleElement).find('li > div > div.news-text > h4 > a'); + const timeElement = $(articleElement).find('li > div > div.news-text > span'); + const imageElement = $(articleElement).find('li > div > div.news-img > a > img'); + + const link = titleElement.attr('href'); + const title = titleElement.attr('title'); + const pubDate = timeElement.text().trim(); + + return { + title, + link, + itunes_item_image: imageElement.attr('src'), + pubDate: parseDate(pubDate, 'YYYY-MM-DD'), + }; + }); + } catch { + // console.error(error); + } + + return []; +} + +export async function getArticle(item) { + try { + const response = await got.get(item.link, { + headers: { + 'User-Agent': config.ua, + }, + }); + const html = response.body; + const $ = load(html); + const articleContentElement = $('body > div > main > section > div > div > div.post-content-contaier > div'); + const content = articleContentElement.html(); + + item.description = content; + return item; + } catch { + // console.error(error); + } + + return item; +} diff --git a/lib/routes/scu/scupi/notice.ts b/lib/routes/scu/scupi/notice.ts new file mode 100644 index 00000000000000..8457223004ff0f --- /dev/null +++ b/lib/routes/scu/scupi/notice.ts @@ -0,0 +1,41 @@ +// Warning: The author still knows nothing about javascript! + +import { getNotifList, getArticle } from './_utils'; +import { Route } from '@/types'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/scupi', + categories: ['university'], + example: '/scu/scupi', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '匹兹堡学院通知', + maintainers: ['sitdownkevin'], + url: 'scupi.scu.edu.cn/activities/notice', + handler, + description: ``, +}; + +async function handler() { + // feed the data to rss + const items = await getNotifList(); + const itemsWithContent = await Promise.all(items.map((item) => cache.tryGet(item.link, () => getArticle(item)))); + + return { + title: '四川大学匹兹堡学院', + description: '四川大学匹兹堡学院官网通知', + language: 'zh-cn', + image: 'https://upload.wikimedia.org/wikipedia/zh/4/45/Sichuan_University_logo.svg', + logo: 'https://upload.wikimedia.org/wikipedia/zh/4/45/Sichuan_University_logo.svg', + link: 'https://scupi.scu.edu.cn/', + item: itemsWithContent, + }; +} diff --git a/lib/routes/scut/gzic/media.ts b/lib/routes/scut/gzic/media.ts new file mode 100644 index 00000000000000..6d94de2a8ec28f --- /dev/null +++ b/lib/routes/scut/gzic/media.ts @@ -0,0 +1,71 @@ +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 ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/gzic/media', + categories: ['university'], + example: '/scut/gzic/media', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '广州国际校区 - 媒体报道', + maintainers: ['gdzhht'], + handler, + description: `::: warning +由于学校网站对非大陆 IP 的访问存在限制,可能需自行部署。 +:::`, +}; + +async function handler() { + const url = 'https://www2.scut.edu.cn/gzic/30281/list.htm'; + + const { data: response } = await got(url); + const $ = load(response); + + const list = $('.right-nr .row .col-lg-4') + .toArray() + .map((item) => { + item = $(item); + const a = item.find('.thr-box a'); + const pubDate = item.find('.thr-box a span'); + return { + title: item.find('.thr-box a p').text(), + link: a.attr('href')?.startsWith('http') ? a.attr('href') : `https://www2.scut.edu.cn${a.attr('href')}`, + pubDate: parseDate(pubDate.text()), + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + try { + const response = await ofetch(item.link); + const $ = load(response); + item.description = $('div.wp_articlecontent').html(); + } catch (error) { + if (error.response && error.response.status === 404) { + item.description = ''; + } else { + throw error; + } + } + return item; + }) + ) + ); + + return { + title: '华南理工大学广州国际校区 - 媒体报道', + link: url, + item: items, + }; +} diff --git a/lib/routes/scut/gzic/news.ts b/lib/routes/scut/gzic/news.ts new file mode 100644 index 00000000000000..d295d929b3d88a --- /dev/null +++ b/lib/routes/scut/gzic/news.ts @@ -0,0 +1,65 @@ +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 ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/gzic/news', + categories: ['university'], + example: '/scut/gzic/news', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '广州国际校区 - 新闻聚焦', + maintainers: ['gdzhht'], + handler, + description: `::: warning +由于学校网站对非大陆 IP 的访问存在限制,可能需自行部署。 +:::`, +}; + +async function handler() { + const baseUrl = 'https://www2.scut.edu.cn'; + const url = 'https://www2.scut.edu.cn/gzic/30279/list.htm'; + + const { data: response } = await got(url); + const $ = load(response); + + const list = $('.right-nr .row .col-lg-4') + .toArray() + .map((item) => { + item = $(item); + const a = item.find('.li-img a'); + const pubDate = item.find('.li-img a span'); + return { + title: item.find('.li-img a p').text(), + link: a.attr('href')?.startsWith('http') ? a.attr('href') : `${baseUrl}${a.attr('href')}`, + pubDate: parseDate(pubDate.text().replaceAll(/年|月/g, '-').replaceAll('日', '')), + itunes_item_image: `${baseUrl}${item.find('.li-img img').attr('src')}`, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + item.description = item.link.startsWith('https://mp.weixin.qq.com/') ? $('div.rich_media_content section').html() : $('div.wp_articlecontent').html(); + return item; + }) + ) + ); + + return { + title: '华南理工大学广州国际校区 - 新闻聚焦', + link: url, + item: items, + }; +} diff --git a/lib/routes/scut/gzic/notice.ts b/lib/routes/scut/gzic/notice.ts new file mode 100644 index 00000000000000..ce55a124f4ce07 --- /dev/null +++ b/lib/routes/scut/gzic/notice.ts @@ -0,0 +1,88 @@ +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 ofetch from '@/utils/ofetch'; + +const categoryMap = { + xsyg: { title: '学术预告', tag: '30284' }, + jytz: { title: '教研通知', tag: '30307' }, + hwxx: { title: '海外学习', tag: 'hwxx' }, + swtz: { title: '事务通知', tag: '30283' }, +}; + +export const route: Route = { + path: '/gzic/notice/:category?', + categories: ['university'], + example: '/scut/gzic/notice/swtz', + parameters: { category: '通知分类,默认为 `swtz`' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '广州国际校区 - 通知公告', + maintainers: ['gdzhht'], + handler, + description: `| 学术预告 | 教研通知 | 海外学习 | 事务通知 | +| -------- | -------- | -------- | -------- | +| xsyg | jytz | hwxx | swtz | + +::: warning +由于学校网站对非大陆 IP 的访问存在限制,可能需自行部署。 +部分通知详情页可能会被删除(返回 404),或在校园网外无法访问。 +:::`, +}; + +async function handler(ctx) { + const baseUrl = 'https://www2.scut.edu.cn'; + + const categoryName = ctx.req.param('category') || 'swtz'; + const categoryMeta = categoryMap[categoryName]; + const url = `${baseUrl}/gzic/${categoryMeta.tag}/list.htm`; + + const { data: response } = await got(url); + const $ = load(response); + + const list = $('.right-nr .row .col-lg-4') + .toArray() + .map((item) => { + item = $(item); + const a = item.find('.thr-box a'); + const pubDate = item.find('.thr-box a span'); + return { + title: item.find('.thr-box a p').text(), + link: a.attr('href')?.startsWith('http') ? a.attr('href') : `${baseUrl}${a.attr('href')}`, + pubDate: parseDate(pubDate.text()), + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + try { + const response = await ofetch(item.link); + const $ = load(response); + item.description = $('div.wp_articlecontent').html(); + } catch (error) { + if (error.response && error.response.status === 404) { + item.description = ''; + } else { + throw error; + } + } + return item; + }) + ) + ); + + return { + title: `华南理工大学广州国际校区 - ${categoryMeta.title}`, + link: url, + item: items, + }; +} diff --git a/lib/routes/scut/jwc/notice.ts b/lib/routes/scut/jwc/notice.ts index 3be1efc2d36620..0dd85213c7d37e 100644 --- a/lib/routes/scut/jwc/notice.ts +++ b/lib/routes/scut/jwc/notice.ts @@ -72,8 +72,8 @@ export const route: Route = { maintainers: ['imkero'], handler, description: `| 全部 | 选课 | 考试 | 实践 | 交流 | 教师 | 信息 | - | ---- | ------ | ---- | -------- | ------------- | ------- | ---- | - | all | course | exam | practice | communication | teacher | info |`, +| ---- | ------ | ---- | -------- | ------------- | ------- | ---- | +| all | course | exam | practice | communication | teacher | info |`, }; async function handler(ctx) { diff --git a/lib/routes/scut/jwc/school.ts b/lib/routes/scut/jwc/school.ts index 3fc24a533afd97..8e2c46200586d1 100644 --- a/lib/routes/scut/jwc/school.ts +++ b/lib/routes/scut/jwc/school.ts @@ -69,8 +69,8 @@ export const route: Route = { maintainers: ['imkero', 'Rongronggg9'], handler, description: `| 全部 | 选课 | 考试 | 信息 | - | ---- | ------ | ---- | ---- | - | all | course | exam | info |`, +| ---- | ------ | ---- | ---- | +| all | course | exam | info |`, }; async function handler(ctx) { diff --git a/lib/routes/scut/namespace.ts b/lib/routes/scut/namespace.ts index 1c22a98583119d..487b751df12f2c 100644 --- a/lib/routes/scut/namespace.ts +++ b/lib/routes/scut/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '华南理工大学', url: 'jw.scut.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/scut/seie/news-ccenter.ts b/lib/routes/scut/seie/news-ccenter.ts index f08dbbcf4688c7..c42d405a5821db 100644 --- a/lib/routes/scut/seie/news-ccenter.ts +++ b/lib/routes/scut/seie/news-ccenter.ts @@ -26,7 +26,7 @@ export const route: Route = { maintainers: ['auto-bot-ty'], handler, url: 'www2.scut.edu.cn/ee/16285/list.htm', - description: `:::warning + description: `::: warning 由于学院官网对非大陆 IP 的访问存在限制,需自行部署。 :::`, }; diff --git a/lib/routes/scut/smae/notice.ts b/lib/routes/scut/smae/notice.ts index e534261cce7cc1..206ca57ce8610e 100644 --- a/lib/routes/scut/smae/notice.ts +++ b/lib/routes/scut/smae/notice.ts @@ -32,8 +32,8 @@ export const route: Route = { maintainers: ['Ermaotie'], handler, description: `| 公务信息 | 党建工作 | 人事工作 | 学生工作 | 科研实验室 | 本科生教务 | 研究生教务 | - | -------- | -------- | -------- | -------- | ---------- | ---------- | ---------- | - | gwxx | djgz | rsgz | xsgz | kysys | bksjw | yjsjw |`, +| -------- | -------- | -------- | -------- | ---------- | ---------- | ---------- | +| gwxx | djgz | rsgz | xsgz | kysys | bksjw | yjsjw |`, }; async function handler(ctx) { diff --git a/lib/routes/scvtc/namespace.ts b/lib/routes/scvtc/namespace.ts index b9deea44437276..7643fee8dfc84c 100644 --- a/lib/routes/scvtc/namespace.ts +++ b/lib/routes/scvtc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '四川职业技术学院', url: 'scvtc.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sdu/cmse.ts b/lib/routes/sdu/cmse.ts index 6cdc04292c2ad5..b057db57fb77c3 100644 --- a/lib/routes/sdu/cmse.ts +++ b/lib/routes/sdu/cmse.ts @@ -25,8 +25,8 @@ export const route: Route = { maintainers: ['Ji4n1ng'], handler, description: `| 通知公告 | 学院新闻 | 本科生教育 | 研究生教育 | 学术动态 | - | -------- | -------- | ---------- | ---------- | -------- | - | 0 | 1 | 2 | 3 | 4 |`, +| -------- | -------- | ---------- | ---------- | -------- | +| 0 | 1 | 2 | 3 | 4 |`, }; async function handler(ctx) { diff --git a/lib/routes/sdu/cs.ts b/lib/routes/sdu/cs/index.ts similarity index 58% rename from lib/routes/sdu/cs.ts rename to lib/routes/sdu/cs/index.ts index daa6671f5b4734..a471d22b5e2aad 100644 --- a/lib/routes/sdu/cs.ts +++ b/lib/routes/sdu/cs/index.ts @@ -6,14 +6,26 @@ import { parseDate } from '@/utils/parse-date'; import { finishArticleItem } from '@/utils/wechat-mp'; const host = 'https://www.cs.sdu.edu.cn/'; -const typelist = ['学院公告', '学术报告', '科技简讯']; -const urlList = ['xygg.htm', 'xsbg.htm', 'kjjx.htm']; +const urlMap = { + announcement: 'xygg.htm', + academic: 'xsbg.htm', + technology: 'kjjx.htm', + undergraduate: 'bkjy.htm', + postgraduate: 'yjsjy.htm', +}; +const titleMap = { + announcement: '学院公告', + academic: '学术报告', + technology: '科技简讯', + undergraduate: '本科教育', + postgraduate: '研究生教育', +}; export const route: Route = { - path: '/cs/:type?', + path: '/cs/index/:type?', categories: ['university'], - example: '/sdu/cs/0', - parameters: { type: '默认为 `0`' }, + example: '/sdu/cs/index/announcement', + parameters: { type: '默认为 `announcement`' }, features: { requireConfig: false, requirePuppeteer: false, @@ -22,17 +34,39 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, + radar: [ + { + source: ['www.cs.sdu.edu.cn/', 'www.cs.sdu.edu.cn/xygg.htm'], + target: '/cs/index/announcement', + }, + { + source: ['www.cs.sdu.edu.cn/xsbg.htm'], + target: '/cs/index/academic', + }, + { + source: ['www.cs.sdu.edu.cn/kjjx.htm'], + target: '/cs/index/technology', + }, + { + source: ['www.cs.sdu.edu.cn/bkjy.htm'], + target: '/cs/index/undergraduate', + }, + { + source: ['www.cs.sdu.edu.cn/yjsjy.htm'], + target: '/cs/index/postgraduate', + }, + ], name: '计算机科学与技术学院通知', - maintainers: ['Ji4n1ng'], + maintainers: ['Ji4n1ng', 'wiketool'], handler, - description: `| 学院公告 | 学术报告 | 科技简讯 | - | -------- | -------- | -------- | - | 0 | 1 | 2 |`, + description: `| 学院公告 | 学术报告 | 科技简讯 | 本科教育 | 研究生教育 | +| -------- | -------- | -------- | -------- | -------- | +| announcement | academic | technology | undergraduate | postgraduate |`, }; async function handler(ctx) { - const type = ctx.req.param('type') ? Number.parseInt(ctx.req.param('type')) : 0; - const link = new URL(urlList[type], host).href; + const type = ctx.req.param('type') ?? 'announcement'; + const link = new URL(urlMap[type], host).href; const response = await got(link); @@ -72,7 +106,7 @@ async function handler(ctx) { ); return { - title: `山东大学计算机科学与技术学院${typelist[type]}通知`, + title: `山东大学计算机科学与技术学院${titleMap[type]}`, description: $('title').text(), link, item, diff --git a/lib/routes/sdu/cs/yjsgz.ts b/lib/routes/sdu/cs/yjsgz.ts new file mode 100644 index 00000000000000..3ebffc9ef78adf --- /dev/null +++ b/lib/routes/sdu/cs/yjsgz.ts @@ -0,0 +1,85 @@ +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 { finishArticleItem } from '@/utils/wechat-mp'; + +const host = 'https://csyh.sdu.edu.cn/'; +const typeMap = { + zytz: 'zytz.htm', + gsl: 'gsl.htm', +}; +const titleMap = { + zytz: '重要通知', + gsl: '公示栏', +}; + +export const route: Route = { + path: '/cs/yjsgz/:type?', + categories: ['university'], + example: '/sdu/cs/yjsgz/zytz', + parameters: { type: '默认为`zytz`' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '计算机科学与技术学院研究生工作网站', + maintainers: ['kukeya', 'wiketool'], + handler, + description: `| 重要通知 | 公示栏 | +| -------- | -------- | +| zytz | gsl |`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type') ?? 'zytz'; + + const link = new URL(typeMap[type], host).href; + + const response = await got(link); + + const $ = load(response.data); + + let item = $('.ss li') + .toArray() + .map((e) => { + e = $(e); + const a = e.find('a'); + return { + title: a.text().trim(), + link: a.attr('href').startsWith('info/') ? host + a.attr('href') : a.attr('href'), + pubDate: parseDate(e.find('span').text().trim(), 'YYYY-MM-DD'), + }; + }); + + item = await Promise.all( + item.map((item) => + cache.tryGet(item.link, async () => { + const hostname = new URL(item.link).hostname; + if (hostname === 'mp.weixin.qq.com') { + return finishArticleItem(item); + } + const response = await got(item.link); + const $ = load(response.data); + + item.description = $('.v_news_content').html(); + + return item; + }) + ) + ); + + return { + title: `山东大学计算机科学与技术学院研究生工作网站${titleMap[type]}`, + description: $('title').text(), + link, + item, + icon: 'https://assets.i-scmp.com/static/img/icons/scmp-icon-256x256.png', + logo: 'https://assets.i-scmp.com/static/img/icons/scmp-icon-256x256.png', + }; +} diff --git a/lib/routes/sdu/epe.ts b/lib/routes/sdu/epe.ts index 82ee1992d91b80..d52e5e5001a617 100644 --- a/lib/routes/sdu/epe.ts +++ b/lib/routes/sdu/epe.ts @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['Ji4n1ng'], handler, description: `| 学院动态 | 通知公告 | 学术论坛 | - | -------- | -------- | -------- | - | 0 | 1 | 2 |`, +| -------- | -------- | -------- | +| 0 | 1 | 2 |`, }; async function handler(ctx) { diff --git a/lib/routes/sdu/gjsw.ts b/lib/routes/sdu/gjsw.ts new file mode 100644 index 00000000000000..27d2ef4f685e81 --- /dev/null +++ b/lib/routes/sdu/gjsw.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 { finishArticleItem } from '@/utils/wechat-mp'; + +const host = 'https://www.ipo.sdu.edu.cn/'; + +const typeMap = { + tzgg: { + title: '通知公告', + url: 'tzgg.htm', + }, +}; + +export const route: Route = { + path: '/gjsw/:type?', + categories: ['university'], + example: '/sdu/gjsw/tzgg', + parameters: { type: '默认为`tzgg`' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '国际事务部', + maintainers: ['kukeya'], + handler, + description: `| 通知公告 | +| -------- | +| tzgg | `, +}; + +async function handler(ctx) { + const type = ctx.req.param('type') ?? 'tzgg'; + + const link = new URL(typeMap[type].url, host).href; + const response = await got(link); + + const $ = load(response.data); + + let item = $('.dqlb ul li') + .toArray() + .map((e) => { + e = $(e); + const a = e.find('a'); + return { + title: a.text().trim(), + link: a.attr('href').startsWith('wdhcontent') ? host + a.attr('href') : a.attr('href'), + pubDate: parseDate(e.find('.fr').text().trim(), 'YYYY-MM-DD'), + }; + }); + + item = await Promise.all( + item.map((item) => + cache.tryGet(item.link, async () => { + const hostname = new URL(item.link).hostname; + if (hostname === 'mp.weixin.qq.com') { + return finishArticleItem(item); + } + const response = await got(item.link); + const $ = load(response.data); + item.description = $('.v_news_content').html(); + + return item; + }) + ) + ); + + return { + title: `山东大学国际事务部${typeMap[type].title}`, + description: $('title').text(), + link, + item, + }; +} diff --git a/lib/routes/sdu/mech.ts b/lib/routes/sdu/mech.ts index 02b37de813caa5..e4be28000ecc58 100644 --- a/lib/routes/sdu/mech.ts +++ b/lib/routes/sdu/mech.ts @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['Ji4n1ng'], handler, description: `| 通知公告 | 院所新闻 | 教学信息 | 学术动态 | 学院简报 | - | -------- | -------- | -------- | -------- | -------- | - | 0 | 1 | 2 | 3 | 4 |`, +| -------- | -------- | -------- | -------- | -------- | +| 0 | 1 | 2 | 3 | 4 |`, }; async function handler(ctx) { diff --git a/lib/routes/sdu/namespace.ts b/lib/routes/sdu/namespace.ts index 2b52456a9fcb86..46f6e326350553 100644 --- a/lib/routes/sdu/namespace.ts +++ b/lib/routes/sdu/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: '山东大学(威海)', - url: 'xinwen.wh.sdu.edu.cn', + name: '山东大学', + url: 'www.sdu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sdu/qd/xszxqd.ts b/lib/routes/sdu/qd/xszxqd.ts new file mode 100644 index 00000000000000..8c75a0ea255a61 --- /dev/null +++ b/lib/routes/sdu/qd/xszxqd.ts @@ -0,0 +1,98 @@ +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 { finishArticleItem } from '@/utils/wechat-mp'; + +const host = 'https://www.onlineqd.sdu.edu.cn/'; + +const typeMap = { + 'xttz-yjs': { + title: '学团通知-研究生', + url: 'list.jsp?urltype=tree.TreeTempUrl&wbtreeid=1027', + }, + 'xttz-bks': { + title: '学团通知-本科生', + url: 'list.jsp?urltype=tree.TreeTempUrl&wbtreeid=1025', + }, + 'xttz-tx': { + title: '学团通知-团学', + url: 'list.jsp?urltype=tree.TreeTempUrl&wbtreeid=1026', + }, + 'xttz-xl': { + title: '学团通知-心理', + url: 'list.jsp?urltype=tree.TreeTempUrl&wbtreeid=1029', + }, + xtyw: { + title: '学团要闻', + url: 'list.jsp?urltype=tree.TreeTempUrl&wbtreeid=1008', + }, +}; + +export const route: Route = { + path: '/qd/xszxqd/:type?', + categories: ['university'], + example: '/sdu/qd/xszxqd/xtyw', + parameters: { type: '默认为`xtyw`' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '学生在线(青岛)', + maintainers: ['kukeya'], + handler, + description: `| 学团通知-研究生 | 学团通知-本科生 | 学团通知-团学 | 学团通知-心理 | 学团要闻 +| -------- | -------- |-------- |-------- |-------- | +| xttz-yjs | xttz-bks | xttz-tx | xttz-xl | xtyw |`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type') ?? 'xtyw'; + const link = new URL(typeMap[type].url, host).href; + + const response = await got(link); + + const $ = load(response.data); + + let item = $('.list_box li') + .toArray() + .map((e) => { + e = $(e); + const a = e.find('a'); + const link = a.attr('href').startsWith('tz_content') || a.attr('href').startsWith('content') ? host + a.attr('href') : a.attr('href'); + return { + title: a.text().trim(), + link, + pubDate: parseDate(e.find('span').text().trim(), 'YYYY-MM-DD'), + }; + }); + + item = await Promise.all( + item.map((item) => + cache.tryGet(item.link, async () => { + const hostname = new URL(item.link).hostname; + if (hostname === 'mp.weixin.qq.com') { + return finishArticleItem(item); + } + const response = await got(item.link); + const $ = load(response.data); + item.description = $('.v_news_content').html(); + + return item; + }) + ) + ); + + return { + title: `山东大学学生在线(青岛)${typeMap[type].title}`, + description: $('title').text(), + link, + item, + icon: 'https://www.onlineqd.sdu.edu.cn/img/logo.png', + }; +} diff --git a/lib/routes/sdu/qd/xyb.ts b/lib/routes/sdu/qd/xyb.ts new file mode 100644 index 00000000000000..905ebac4b9c02a --- /dev/null +++ b/lib/routes/sdu/qd/xyb.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'; +import { finishArticleItem } from '@/utils/wechat-mp'; + +const host = 'https://xyb.qd.sdu.edu.cn/'; + +const typeMap = { + gztz: { + title: '工作通知', + url: 'gztz.htm', + }, +}; + +export const route: Route = { + path: '/qd/xyb/:type?', + categories: ['university'], + example: '/sdu/qd/xyb/gztz', + parameters: { type: '默认为`gztz`' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '青岛校区学科建设与研究生教育办公室', + maintainers: ['kukeya'], + handler, + description: `| 工作通知 | +| -------- | +| gztz | `, +}; + +async function handler(ctx) { + const type = ctx.req.param('type') ?? 'gztz'; + + const link = new URL(typeMap[type].url, host).href; + + const response = await got(link); + + const $ = load(response.data); + + let item = $('.list li') + .toArray() + .map((e) => { + e = $(e); + const a = e.find('a'); + return { + title: a.text().slice(1).trim(), + link: a.attr('href').startsWith('info/') ? host + a.attr('href') : a.attr('href'), + pubDate: parseDate(e.find('b').text().trim(), 'YYYY-MM-DD'), + }; + }); + + item = await Promise.all( + item.map((item) => + cache.tryGet(item.link, async () => { + const hostname = new URL(item.link).hostname; + if (hostname === 'mp.weixin.qq.com') { + return finishArticleItem(item); + } + const response = await got(item.link); + const $ = load(response.data); + + item.description = $('.v_news_content').html(); + + return item; + }) + ) + ); + + return { + title: `山东大学(青岛)学研办${typeMap[type].title}`, + description: $('title').text(), + link, + item, + }; +} diff --git a/lib/routes/sdu/sc.ts b/lib/routes/sdu/sc.ts index 98ed037dd6848d..dee998626daa6c 100644 --- a/lib/routes/sdu/sc.ts +++ b/lib/routes/sdu/sc.ts @@ -25,8 +25,8 @@ export const route: Route = { maintainers: ['Ji4n1ng'], handler, description: `| 通知公告 | 学术动态 | 本科教育 | 研究生教育 | - | -------- | -------- | -------- | ---------- | - | 0 | 1 | 2 | 3 |`, +| -------- | -------- | -------- | ---------- | +| 0 | 1 | 2 | 3 |`, }; async function handler(ctx) { diff --git a/lib/routes/sdu/wh/jwc.ts b/lib/routes/sdu/wh/jwc.ts index 645398d70de64c..e33de99dfeb7ca 100644 --- a/lib/routes/sdu/wh/jwc.ts +++ b/lib/routes/sdu/wh/jwc.ts @@ -24,8 +24,8 @@ export const route: Route = { maintainers: ['kxxt'], handler, description: `| 规章制度 | 专业建设 | 实践教学 | 支部风采 | 服务指南 | 教务要闻 | 工作通知 | 教务简报 | 常用下载 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | gzzd | zyjs | sjjx | zbfc | fwzn | jwyw | gztz | jwjb | cyxz |`, +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| gzzd | zyjs | sjjx | zbfc | fwzn | jwyw | gztz | jwjb | cyxz |`, }; async function handler(ctx) { diff --git a/lib/routes/sdu/wh/news.ts b/lib/routes/sdu/wh/news.ts index 94e2fe66cfc9a5..42b8d7dae2a138 100644 --- a/lib/routes/sdu/wh/news.ts +++ b/lib/routes/sdu/wh/news.ts @@ -23,8 +23,8 @@ export const route: Route = { maintainers: ['kxxt'], handler, description: `| 校园要闻 | 学生动态 | 综合新闻 | 山大视点 | 菁菁校园 | 校园简讯 | 玛珈之窗 | 热点专题 | 媒体视角 | 高教视野 | 理论学习 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | xyyw | xsdt | zhxw | sdsd | jjxy | xyjx | mjzc | rdzt | mtsj | gjsy | llxx |`, +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| xyyw | xsdt | zhxw | sdsd | jjxy | xyjx | mjzc | rdzt | mtsj | gjsy | llxx |`, }; async function handler(ctx) { diff --git a/lib/routes/sdu/ygb.ts b/lib/routes/sdu/ygb.ts new file mode 100644 index 00000000000000..56e99782e6da8c --- /dev/null +++ b/lib/routes/sdu/ygb.ts @@ -0,0 +1,90 @@ +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 { finishArticleItem } from '@/utils/wechat-mp'; + +const host = 'https://www.ygb.sdu.edu.cn/'; + +const typeMap = { + zytz: { + title: '重要通知', + url: 'zytz.htm', + }, + glfw: { + title: '管理服务', + url: 'glfw.htm', + }, + cxsj: { + title: '创新实践', + url: 'cxsj.htm', + }, +}; + +export const route: Route = { + path: '/ygb/:type?', + categories: ['university'], + example: '/sdu/ygb/zytz', + parameters: { type: '默认为`zytz`' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '研工部', + maintainers: ['kukeya'], + handler, + description: `| 重要通知 | 管理服务 | 创新实践 | +| -------- | -------- |-------- | +| zytz | glfw | cxsj | `, +}; + +async function handler(ctx) { + const type = ctx.req.param('type') ?? 'zytz'; + const link = new URL(typeMap[type].url, host).href; + + const response = await got(link); + + const $ = load(response.data); + let item = $('.zytz-list li') + .toArray() + .map((e) => { + e = $(e); + const a = e.find('a'); + const link = a.attr('href').startsWith('info/') || a.attr('href').startsWith('content') ? host + a.attr('href') : a.attr('href'); + return { + title: a.text().trim(), + link, + pubDate: parseDate(e.find('b').text().trim().slice(1, -1), 'YYYY-MM-DD'), + }; + }); + + item = await Promise.all( + item.map((item) => + cache.tryGet(item.link, async () => { + const hostname = new URL(item.link).hostname; + if (hostname === 'mp.weixin.qq.com') { + return finishArticleItem(item); + } + const response = await got(item.link); + const $ = load(response.data); + + item.description = $('.v_news_content').html(); + + return item; + }) + ) + ); + + return { + title: `山东大学研工部${typeMap[type].title}`, + description: $('title').text(), + link, + item, + icon: 'https://www.ygb.sdu.edu.cn/img/logo.png', + }; +} diff --git a/lib/routes/sdust/namespace.ts b/lib/routes/sdust/namespace.ts index 69786c300d7e7a..1d33a6faf28ef4 100644 --- a/lib/routes/sdust/namespace.ts +++ b/lib/routes/sdust/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '山东科技大学', url: 'sdust.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sdust/yjsy/zhaosheng.ts b/lib/routes/sdust/yjsy/zhaosheng.ts index dae4baa9996a84..c033d6ca7a60a7 100644 --- a/lib/routes/sdust/yjsy/zhaosheng.ts +++ b/lib/routes/sdust/yjsy/zhaosheng.ts @@ -22,13 +22,13 @@ export const route: Route = { handler, description: `栏目 - | 招生简章 | 专业目录 | 往届录取 | 管理规定 | 资料下载 | - | -------- | -------- | -------- | -------- | -------- | - | zsjz | zyml | wjlq | glgd | zlxz | +| 招生简章 | 专业目录 | 往届录取 | 管理规定 | 资料下载 | +| -------- | -------- | -------- | -------- | -------- | +| zsjz | zyml | wjlq | glgd | zlxz | - | 通知公告 | 博士招生 | 硕士招生 | 推免生招生 | 招生宣传 | - | -------- | -------- | -------- | ---------- | -------- | - | tzgg | bszs | sszs | tms | zsxc |`, +| 通知公告 | 博士招生 | 硕士招生 | 推免生招生 | 招生宣传 | +| -------- | -------- | -------- | ---------- | -------- | +| tzgg | bszs | sszs | tms | zsxc |`, }; async function handler(ctx) { diff --git a/lib/routes/sdzk/index.ts b/lib/routes/sdzk/index.ts index 33f899c4f1c724..c75848145f2c30 100644 --- a/lib/routes/sdzk/index.ts +++ b/lib/routes/sdzk/index.ts @@ -20,11 +20,11 @@ export const route: Route = { name: '新闻', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 若订阅 [信息与政策](https://www.sdzk.cn/NewsList.aspx?BCID=1),网址为 \`https://www.sdzk.cn/NewsList.aspx?BCID=1\`。截取 \`BCID=1\` 作为参数,此时路由为 [\`/sdzk/1\`](https://rsshub.app/sdzk/1)。 若订阅 [通知公告](https://www.sdzk.cn/NewsList.aspx?BCID=1\&CID=16),网址为 \`https://www.sdzk.cn/NewsList.aspx?BCID=1&CID=16\`。截取 \`BCID=1\` 与 \`CID=16\` 作为参数,此时路由为 [\`/sdzk/1/16\`](https://rsshub.app/sdzk/1/16)。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/sdzk/namespace.ts b/lib/routes/sdzk/namespace.ts index 4711c53fc00788..d05cfee80e7ec6 100644 --- a/lib/routes/sdzk/namespace.ts +++ b/lib/routes/sdzk/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '山东省教育招生考试院', url: 'sdzk.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sec-in/namespace.ts b/lib/routes/sec-in/namespace.ts index 20ed716787406e..baf2cb5a8070b8 100644 --- a/lib/routes/sec-in/namespace.ts +++ b/lib/routes/sec-in/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'SecIN 信息安全技术社区', url: 'sec-in.com', + lang: 'zh-CN', }; diff --git a/lib/routes/sec-wiki/namespace.ts b/lib/routes/sec-wiki/namespace.ts index cc30205e358eab..13485217273ca5 100644 --- a/lib/routes/sec-wiki/namespace.ts +++ b/lib/routes/sec-wiki/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'SecWiki - 安全维基', url: 'www.sec-wiki.com', + lang: 'zh-CN', }; diff --git a/lib/routes/secrss/namespace.ts b/lib/routes/secrss/namespace.ts index 11123c82b0b1c9..2b709c18d3320f 100644 --- a/lib/routes/secrss/namespace.ts +++ b/lib/routes/secrss/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '安全内参', url: 'secrss.com', + lang: 'zh-CN', }; diff --git a/lib/routes/seekingalpha/index.ts b/lib/routes/seekingalpha/index.ts index e1f2fad9168bb5..291d3ef3cd0f67 100644 --- a/lib/routes/seekingalpha/index.ts +++ b/lib/routes/seekingalpha/index.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| Analysis | News | Transcripts | Press Releases | Related Analysis | - | -------- | ---- | ----------- | -------------- | ---------------- | - | analysis | news | transcripts | press-releases | related-analysis |`, +| -------- | ---- | ----------- | -------------- | ---------------- | +| analysis | news | transcripts | press-releases | related-analysis |`, }; const getMachineCookie = () => @@ -66,7 +66,7 @@ async function handler(ctx) { 'filter[until]': 0, id: symbol.toLowerCase(), include: 'author,primaryTickers,secondaryTickers,sentiments', - 'page[size]': ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : category === 'news' ? 40 : 20, + 'page[size]': ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : (category === 'news' ? 40 : 20), 'page[number]': 1, }, }); diff --git a/lib/routes/seekingalpha/namespace.ts b/lib/routes/seekingalpha/namespace.ts index 4eb4528e66abbc..e81abe1ba652f8 100644 --- a/lib/routes/seekingalpha/namespace.ts +++ b/lib/routes/seekingalpha/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Seeking Alpha', url: 'seekingalpha.com', + lang: 'en', }; diff --git a/lib/routes/sega/namespace.ts b/lib/routes/sega/namespace.ts index 794d05bdb7b298..890b67ee02e3e4 100644 --- a/lib/routes/sega/namespace.ts +++ b/lib/routes/sega/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'SEGA', url: 'pjsekai.sega.jp', + lang: 'ja', }; diff --git a/lib/routes/segmentfault/namespace.ts b/lib/routes/segmentfault/namespace.ts index a022f72428cc18..192e5099c63051 100644 --- a/lib/routes/segmentfault/namespace.ts +++ b/lib/routes/segmentfault/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'SegmentFault', url: 'segmentfault.com', + lang: 'zh-CN', }; diff --git a/lib/routes/segmentfault/user.ts b/lib/routes/segmentfault/user.ts index 4d2dfd9b4f5245..2beca2b64359cc 100644 --- a/lib/routes/segmentfault/user.ts +++ b/lib/routes/segmentfault/user.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 { host, acw_sc__v2, parseList, parseItems } from './utils'; export const route: Route = { @@ -30,8 +30,8 @@ async function handler(ctx) { const name = ctx.req.param('name'); const apiURL = `${host}/gateway/homepage/${name}/timeline?size=20&offset=`; - const response = await got(apiURL); - const data = response.data.rows; + const response = await ofetch(apiURL); + const data = response.rows; const list = parseList(data); const { author } = list[0]; diff --git a/lib/routes/segmentfault/utils.ts b/lib/routes/segmentfault/utils.ts index bf5c4dbe44b230..1a0489a500a49e 100644 --- a/lib/routes/segmentfault/utils.ts +++ b/lib/routes/segmentfault/utils.ts @@ -1,5 +1,4 @@ -import zlib from 'zlib'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; import { config } from '@/config'; @@ -11,21 +10,12 @@ const acw_sc__v2 = (link, tryGet) => tryGet( 'segmentfault:acw_sc__v2', async () => { - const response = await got(link, { - decompress: false, - }); - - const unzipData = zlib.createUnzip(); - unzipData.write(response.body); + const response = await ofetch(link); let acw_sc__v2 = ''; - for await (const data of unzipData) { - const strData = data.toString(); - const matches = strData.match(/var arg1='(.*?)';/); - if (matches) { - acw_sc__v2 = getAcwScV2ByArg1(matches[1]); - break; - } + const matches = response.match(/var arg1='(.*?)';/); + if (matches) { + acw_sc__v2 = getAcwScV2ByArg1(matches[1]); } return acw_sc__v2; }, @@ -38,22 +28,28 @@ const parseList = (data) => title: item.title, link: new URL(item.url, host).href, author: item.user.name, - pubDate: parseDate(item.created, 'X'), + pubDate: parseDate(item.created || item.modified, 'X'), + description: item.excerpt, })); const parseItems = (cookie, item, tryGet) => tryGet(item.link, async () => { - const response = await got(item.link, { - headers: { - cookie: `acw_sc__v2=${cookie};`, - }, - }); - const content = load(response.data); - - item.description = content('article').html(); - item.category = content('.badge-tag') - .toArray() - .map((item) => content(item).text()); + let response; + try { + response = await ofetch(item.link, { + headers: { + cookie: `acw_sc__v2=${cookie};`, + }, + }); + const content = load(response); + + item.description = content('article').html(); + item.category = content('.badge-tag') + .toArray() + .map((item) => content(item).text()); + } catch { + // ignore + } return item; }); diff --git a/lib/routes/sehuatang/index.ts b/lib/routes/sehuatang/index.ts index 0739165d9dd9b0..a96efde1fa9def 100644 --- a/lib/routes/sehuatang/index.ts +++ b/lib/routes/sehuatang/index.ts @@ -1,9 +1,10 @@ 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'; +import logger from '@/utils/logger'; +import puppeteer from '@/utils/puppeteer'; const host = 'https://www.sehuatang.net/'; @@ -41,18 +42,21 @@ export const route: Route = { path: ['/bt/:subforumid?', '/picture/:subforumid', '/:subforumid?/:type?', '/:subforumid?', ''], name: 'Unknown', maintainers: ['qiwihui', 'junfengP', 'nczitzk'], + features: { + requirePuppeteer: true, + }, handler, description: `**原创 BT 电影** - | 国产原创 | 亚洲无码原创 | 亚洲有码原创 | 高清中文字幕 | 三级写真 | VR 视频 | 素人有码 | 欧美无码 | 韩国主播 | 动漫原创 | 综合讨论 | - | -------- | ------------ | ------------ | ------------ | -------- | ------- | -------- | -------- | -------- | -------- | -------- | - | gcyc | yzwmyc | yzymyc | gqzwzm | sjxz | vr | srym | omwm | hgzb | dmyc | zhtl | +| 国产原创 | 亚洲无码原创 | 亚洲有码原创 | 高清中文字幕 | 三级写真 | VR 视频 | 素人有码 | 欧美无码 | 韩国主播 | 动漫原创 | 综合讨论 | +| -------- | ------------ | ------------ | ------------ | -------- | ------- | -------- | -------- | -------- | -------- | -------- | +| gcyc | yzwmyc | yzymyc | gqzwzm | sjxz | vr | srym | omwm | hgzb | dmyc | zhtl | **色花图片** - | 原创自拍 | 转贴自拍 | 华人街拍 | 亚洲性爱 | 欧美性爱 | 卡通动漫 | 套图下载 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | yczp | ztzp | hrjp | yzxa | omxa | ktdm | ttxz |`, +| 原创自拍 | 转贴自拍 | 华人街拍 | 亚洲性爱 | 欧美性爱 | 卡通动漫 | 套图下载 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| yczp | ztzp | hrjp | yzxa | omxa | ktdm | ttxz |`, }; async function handler(ctx) { @@ -61,16 +65,24 @@ async function handler(ctx) { const type = ctx.req.param('type'); const typefilter = type ? `&filter=typeid&typeid=${type}` : ''; const link = `${host}forum.php?mod=forumdisplay&orderby=dateline&fid=${subformId}${typefilter}`; - const headers = { - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - Cookie: '_safe=vqd37pjm4p5uodq339yzk6b7jdt6oich', - }; - const response = await got(link, { - headers, + const browser = await puppeteer(); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (request) => { + request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + }); + logger.http(`Requesting ${link}`); + await page.goto(link, { + waitUntil: 'domcontentloaded', }); - const $ = load(response.data); + await page.waitForSelector('a.enter-btn', { visible: true }); + + await Promise.all([page.click('a.enter-btn'), page.waitForNavigation({ waitUntil: 'domcontentloaded' })]); + const response = await page.content(); + await page.close(); + + const $ = load(response); const list = $('#threadlisttableid tbody[id^=normalthread]') .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 25) @@ -89,11 +101,19 @@ async function handler(ctx) { const out = await Promise.all( list.map((info) => cache.tryGet(info.link, async () => { - const response = await got(info.link, { - headers, + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (request) => { + request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); }); - const $ = load(response.data); + await page.goto(info.link, { + // 指定页面等待载入的时间 + waitUntil: 'domcontentloaded', + }); + const response = await page.content(); + await page.close(); + const $ = load(response); const postMessage = $("td[id^='postmessage']").slice(0, 1); const images = $(postMessage).find('img'); for (const image of images) { @@ -131,12 +151,11 @@ async function handler(ctx) { info.enclosure_url = enclosureUrl; info.enclosure_type = isMag ? 'application/x-bittorrent' : 'application/octet-stream'; } - return info; }) ) ); - + await browser.close(); return { title: `色花堂 - ${$('#pt > div:nth-child(1) > a:last-child').text()}`, link, diff --git a/lib/routes/sehuatang/namespace.ts b/lib/routes/sehuatang/namespace.ts index 6327b48f437ffb..eb61ac0a576a63 100644 --- a/lib/routes/sehuatang/namespace.ts +++ b/lib/routes/sehuatang/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '色花堂', url: 'sehuatang.net', + lang: 'zh-CN', }; diff --git a/lib/routes/sensortower/blog.ts b/lib/routes/sensortower/blog.ts index 7bc1a161270b4b..bec9f9c2c1a501 100644 --- a/lib/routes/sensortower/blog.ts +++ b/lib/routes/sensortower/blog.ts @@ -33,8 +33,8 @@ export const route: Route = { handler, url: 'sensortower.com/blog', description: `| English | Chinese | Japanese | Korean | - | ------- | ------- | -------- | ------ | - | | zh-CN | ja | ko |`, +| ------- | ------- | -------- | ------ | +| | zh-CN | ja | ko |`, }; async function handler(ctx) { diff --git a/lib/routes/sensortower/namespace.ts b/lib/routes/sensortower/namespace.ts index 2b7507c438ac03..f0b2a7a82fec53 100644 --- a/lib/routes/sensortower/namespace.ts +++ b/lib/routes/sensortower/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Sensor Tower', url: 'sensortower.com', + lang: 'en', }; diff --git a/lib/routes/setn/index.ts b/lib/routes/setn/index.ts index 6b30d1b9faad41..63ab5dc92ea264 100644 --- a/lib/routes/setn/index.ts +++ b/lib/routes/setn/index.ts @@ -67,16 +67,16 @@ export const route: Route = { handler, url: 'setn.com/ViewAll.aspx', description: `| 即時 | 熱門 | 娛樂 | 政治 | 社會 | - | ---- | ---- | ---- | ---- | ---- | +| ---- | ---- | ---- | ---- | ---- | - | 國際 | 兩岸 | 生活 | 健康 | 旅遊 | - | ---- | ---- | ---- | ---- | ---- | +| 國際 | 兩岸 | 生活 | 健康 | 旅遊 | +| ---- | ---- | ---- | ---- | ---- | - | 運動 | 地方 | 財經 | 富房網 | 名家 | - | ---- | ---- | ---- | ------ | ---- | +| 運動 | 地方 | 財經 | 富房網 | 名家 | +| ---- | ---- | ---- | ------ | ---- | - | 新奇 | 科技 | 汽車 | 寵物 | 女孩 | HOT 焦點 | - | ---- | ---- | ---- | ---- | ---- | -------- |`, +| 新奇 | 科技 | 汽車 | 寵物 | 女孩 | HOT 焦點 | +| ---- | ---- | ---- | ---- | ---- | -------- |`, }; async function handler(ctx) { diff --git a/lib/routes/setn/namespace.ts b/lib/routes/setn/namespace.ts index 711efdae2fa78e..a4f6a1857de3fd 100644 --- a/lib/routes/setn/namespace.ts +++ b/lib/routes/setn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '三立新聞網', url: 'setn.com', + lang: 'zh-TW', }; diff --git a/lib/routes/seu/cse/index.ts b/lib/routes/seu/cse/index.ts index 35524343a7913a..27925e2c69cfe6 100644 --- a/lib/routes/seu/cse/index.ts +++ b/lib/routes/seu/cse/index.ts @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['LogicJake'], handler, description: `| 学院新闻 | 通知公告 | 教务信息 | 就业信息 | 学工事务 | - | -------- | -------- | -------- | -------- | -------- | - | xyxw | tzgg | jwxx | jyxx | xgsw |`, +| -------- | -------- | -------- | -------- | -------- | +| xyxw | tzgg | jwxx | jyxx | xgsw |`, }; async function handler(ctx) { diff --git a/lib/routes/seu/namespace.ts b/lib/routes/seu/namespace.ts index 5b7d535da2eee7..34a77ae8b2b22c 100644 --- a/lib/routes/seu/namespace.ts +++ b/lib/routes/seu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '东南大学', url: 'cse.seu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/seu/yzb/index.ts b/lib/routes/seu/yzb/index.ts index 44578a06e93b2f..57aed72cbb4f82 100644 --- a/lib/routes/seu/yzb/index.ts +++ b/lib/routes/seu/yzb/index.ts @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['fuzy112'], handler, description: `| 硕士招生 | 博士招生 | 港澳台及中外合作办学 | - | -------- | -------- | -------------------- | - | 6676 | 6677 | 6679 |`, +| -------- | -------- | -------------------- | +| 6676 | 6677 | 6679 |`, }; async function handler(ctx) { diff --git a/lib/routes/sfacg/namespace.ts b/lib/routes/sfacg/namespace.ts index 348f9d63edd53f..7e6cad11a0831f 100644 --- a/lib/routes/sfacg/namespace.ts +++ b/lib/routes/sfacg/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'SF 轻小说', url: 'book.sfacg.com', + lang: 'zh-CN', }; diff --git a/lib/routes/shcstheatre/namespace.ts b/lib/routes/shcstheatre/namespace.ts index f5fd55e8cb273f..e2289847f5327e 100644 --- a/lib/routes/shcstheatre/namespace.ts +++ b/lib/routes/shcstheatre/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海文化广场', url: 'www.shcstheatre.com', + lang: 'zh-CN', }; diff --git a/lib/routes/shiep/config.ts b/lib/routes/shiep/config.ts index b930fe4a61c45f..25ea4ed6ac0d14 100644 --- a/lib/routes/shiep/config.ts +++ b/lib/routes/shiep/config.ts @@ -2,44 +2,37 @@ const config = { bwc: { title: '武装部保卫处', id: 'tzgg' }, career: { title: '本科就业信息网', id: 'tzgg', listSelector: 'ul.newsList', pubDateSelector: 'li.span2.y', descriptionSelector: '.aContent' }, cyb: { title: '资产经营公司/产业办', id: '367' }, - dangban: { title: '党委办公室', id: '4014' }, + dangban: { title: '党委办公室', id: '4013' }, djfwzxdcs: { title: '党建服务中心/党建督查室', id: 'tzgg', listSelector: 'li.news', pubDateSelector: 'span.news_meta' }, dqxy: { title: '电气工程学院', id: '2462' }, dwllc: { title: '对外联络处', id: '2649' }, dxxy: { title: '电子与信息工程学院', id: 'tzgg', pubDateSelector: 'div.article-publishdate' }, energy: { title: '能源与机械工程学院', id: '892' }, - 'energy-saving': { title: '上海热交换系统节能工程技术研究中心', id: 'tzgg' }, - english: { title: 'Shanghai University of Electric Power', id: 'events' }, fao: { title: '国际交流与合作处(港澳台办公室)', id: 'tzgg' }, - fgw: { title: '妇工委', id: '1411' }, fzghc: { title: '发展规划处', id: '291' }, gec: { title: '上海新能源人才技术教育交流中心', id: '1959' }, - gonghui: { title: '工会', id: '1806', listSelector: 'table.wp_article_list_table tr', pubDateSelector: 'td[align="right"]' }, + gonghui: { title: '工会(妇工委)', id: '1806', listSelector: 'table.wp_article_list_table tr', pubDateSelector: 'td[align="right"]' }, 'green-energy': { title: '上海绿色能源并网技术研究中心', id: '118' }, - hhsyzx: { title: '能源化学实验教学中心', id: '3709' }, hhxy: { title: '环境与化学工程学院', id: '5559', listSelector: 'li.list-item', pubDateSelector: 'div.item-publishdate' }, hqglc: { title: '后勤管理处(后勤服务中心)', id: '1616' }, + hsfdyjy: { title: '海上风电研究院', id: '5748' }, ieetc: { title: '创新创业工程训练中心', id: 'cxcy', pubDateSelector: 'div.article-publishdate' }, jgdw: { title: '机关党委', id: '3205' }, jgxy: { title: '经济与管理学院', id: '3633' }, jijian: { title: '纪委(监察专员办公室)', id: '59' }, jjc: { title: '基建处', id: '327' }, jjxy: { title: '继续教育学院(国际教育学院)', id: '2582' }, - jsjxfzzx: { title: '教师教学发展中心', id: '3909' }, jsjxy: { title: '计算机科学与技术学院', id: 'xygg', listSelector: 'div.xylist', pubDateSelector: 'span:nth-child(2)' }, jszyzx: { title: '技术转移中心', id: '4247' }, jwc: { title: '教务处', id: '227', listSelector: 'div.text-list li', pubDateSelector: 'span.time' }, - jxfz: { title: '电力装备设计与制造虚拟仿真中心', id: '3330' }, kczx: { title: '能源电力科创中心', id: '3946' }, kyc: { title: '科研处/融合办', id: '834' }, lgxq: { title: '临港新校区建设综合办公室', id: '377' }, library: { title: '图书馆', id: '4866' }, metc: { title: '现代教育技术中心/信息办', id: 'tzgg', pubDateSelector: 'div.article-publishdate' }, - mpep: { title: '上海市电力材料防护与新材料重点实验室', id: '1134' }, news: { title: '新闻网', id: 'notice' }, nydlzk: { title: '能源电力智库', id: 'tzgg' }, office: { title: '校长办公室(档案馆)', id: '389' }, - rpstec: { title: '国家新能源电力系统实验教学示范中心', id: '1366' }, rsc: { title: '党委教师工作部/人事处', id: '1695' }, rwysxy: { title: '人文艺术学院', id: '3089', listSelector: 'li.list-item', pubDateSelector: 'div.item-publishdate' }, sjc: { title: '审计处', id: '199' }, @@ -57,6 +50,7 @@ const config = { xsc: { title: '学生处', id: '3482' }, xunchaban: { title: '巡查办', id: '5044' }, xxgk: { title: '信息公开网', id: 'zxgkxx' }, + xxjy: { title: '“学条例 守党纪”专题网', id: '5973', listSelector: 'li.list-item', pubDateSelector: 'div.item-publishdate' }, yjsc: { title: '研究生院/研工部', id: '1161' }, zdhxy: { title: '自动化工程学院', id: '2002' }, zs: { title: '本科招生网', id: 'zxxx' }, diff --git a/lib/routes/shiep/index.ts b/lib/routes/shiep/index.ts index ed49dc73e25fa2..a962a7c6522d12 100644 --- a/lib/routes/shiep/index.ts +++ b/lib/routes/shiep/index.ts @@ -26,31 +26,31 @@ export const route: Route = { 学院一览: - | 能源与机械工程学院 | 环境与化学工程学院 | 电气工程学院 | 自动化工程学院 | 计算机科学与技术学院 | 电子与信息工程学院 | 经济与管理学院 | 数理学院 | 外国语学院 | 体育学院 | 马克思主义学院 | 人文艺术学院 | 继续教育学院(国际教育学院) | - | ------------------ | ------------------ | ------------ | -------------- | -------------------- | ------------------ | -------------- | -------- | ---------- | -------- | -------------- | ------------ | ---------------------------- | - | energy | hhxy | dqxy | zdhxy | jsjxy | dxxy | jgxy | slxy | wgyxy | tyb | skb | rwysxy | jjxy | - | 892 | 5559 | 2462 | 2002 | xygg | tzgg | 3633 | 2063 | tzgg | 2891 | 1736 | 3089 | 2582 | +| 能源与机械工程学院 | 环境与化学工程学院 | 电气工程学院 | 自动化工程学院 | 计算机科学与技术学院 | 电子与信息工程学院 | 经济与管理学院 | 数理学院 | 外国语学院 | 体育学院 | 马克思主义学院 | 人文艺术学院 | 继续教育学院(国际教育学院) | 海上风电研究院 | +| ------------------ | ------------------ | ------------ | -------------- | -------------------- | ------------------ | -------------- | -------- | ---------- | -------- | -------------- | ------------ | ---------------------------- | -------------- | +| energy | hhxy | dqxy | zdhxy | jsjxy | dxxy | jgxy | slxy | wgyxy | tyb | skb | rwysxy | jjxy | hsfdyjy | +| 892 | 5559 | 2462 | 2002 | xygg | tzgg | 3633 | 2063 | tzgg | 2891 | 1736 | 3089 | 2582 | 5748 | 党群部门: - | 党委办公室 | 组织部(老干部处、党校) | 党建服务中心 / 党建督查室 | 宣传部(文明办、融媒体中心) | 统战部 | 机关党委 | 纪委(监察专员办公室) | 巡查办 | 武装部 | 学生工作部 | 团委 | 工会(妇工委) | 教师工作部 | 离退休党委 | 研究生工作部 | - | ---------- | ------------------------ | ------------------------- | ---------------------------- | ------ | -------- | ---------------------- | --------- | ------ | ---------- | ---- | -------------- | ---------- | ---------- | ------------ | - | dangban | zzb | djfwzxdcs | xcb | tzb | jgdw | jijian | xunchaban | bwc | xsc | tw | gonghui | rsc | tgb | yjsc | - | 4014 | 1534 | tzgg | 2925 | 3858 | 3205 | 59 | 5044 | tzgg | 3482 | 2092 | 1806 | 1695 | notice | 1161 | +| 党委办公室 | 组织部(老干部处、党校) | 党建服务中心 / 党建督查室 | 宣传部(文明办、融媒体中心) | 统战部 | 机关党委 | 纪委(监察专员办公室) | 巡查办 | 武装部 | 学生工作部 | 团委 | 工会(妇工委) | 教师工作部 | 离退休党委 | 研究生工作部 | +| ---------- | ------------------------ | ------------------------- | ---------------------------- | ------ | -------- | ---------------------- | --------- | ------ | ---------- | ---- | -------------- | ---------- | ---------- | ------------ | +| dangban | zzb | djfwzxdcs | xcb | tzb | jgdw | jijian | xunchaban | bwc | xsc | tw | gonghui | rsc | tgb | yjsc | +| 4013 | 1534 | tzgg | 2925 | 3858 | 3205 | 59 | 5044 | tzgg | 3482 | 2092 | 1806 | 1695 | notice | 1161 | 行政部门: - | 校长办公室(档案馆) | 对外联络处 | 发展规划处 | 审计处 | 保卫处 | 学生处 | 人事处 | 退管办 | 国际交流与合作处(港澳台办公室) | 科研处 / 融合办 | 教务处 | 研究生院 | 后勤管理处(后勤服务中心) | 实验室与资产管理处 | 基建处 | 临港新校区建设综合办公室 | 图书馆 | 现代教育技术中心 / 信息办 | 创新创业工程训练中心 | 资产经营公司 / 产业办 | 能源电力科创中心 | 技术转移中心 | - | -------------------- | ---------- | ---------- | ------ | ------ | ------ | ------ | ------ | -------------------------------- | --------------- | ------ | -------- | -------------------------- | ------------------ | ------ | ------------------------ | ------- | ------------------------- | -------------------- | --------------------- | ---------------- | ------------ | - | office | dwllc | fzghc | sjc | bwc | xsc | rsc | tgb | fao | kyc | jwc | yjsc | hqglc | sysyzcglc | jjc | lgxq | library | metc | ieetc | cyb | kczx | jszyzx | - | 389 | 2649 | 291 | 199 | tzgg | 3482 | 1695 | notice | tzgg | 834 | 227 | 1161 | 1616 | 312 | 327 | 377 | 4866 | tzgg | cxcy | 367 | 3946 | 4247 | +| 校长办公室(档案馆) | 对外联络处 | 发展规划处 | 审计处 | 保卫处 | 学生处 | 人事处 | 退管办 | 国际交流与合作处(港澳台办公室) | 科研处 / 融合办 | 教务处 | 研究生院 | 后勤管理处(后勤服务中心) | 实验室与资产管理处 | 基建处 | 临港新校区建设综合办公室 | 图书馆 | 现代教育技术中心 / 信息办 | 创新创业工程训练中心 | 资产经营公司 / 产业办 | 能源电力科创中心 | 技术转移中心 | +| -------------------- | ---------- | ---------- | ------ | ------ | ------ | ------ | ------ | -------------------------------- | --------------- | ------ | -------- | -------------------------- | ------------------ | ------ | ------------------------ | ------- | ------------------------- | -------------------- | --------------------- | ---------------- | ------------ | +| office | dwllc | fzghc | sjc | bwc | xsc | rsc | tgb | fao | kyc | jwc | yjsc | hqglc | sysyzcglc | jjc | lgxq | library | metc | ieetc | cyb | kczx | jszyzx | +| 389 | 2649 | 291 | 199 | tzgg | 3482 | 1695 | notice | tzgg | 834 | 227 | 1161 | 1616 | 312 | 327 | 377 | 4866 | tzgg | cxcy | 367 | 3946 | 4247 | 其它: - | 新闻网 | Shanghai University of Electric Power | 信息公开网 | 本科招生网 | 本科就业信息网 | 妇工委 | 文明办 | 学习路上 | 上海热交换系统节能工程技术研究中心 | 上海新能源人才技术教育交流中心 | 上海绿色能源并网技术研究中心 | 能源化学实验教学中心 | 教师教学发展中心 | 电力装备设计与制造虚拟仿真中心 | 上海市电力材料防护与新材料重点实验室 | 能源电力智库 | 国家新能源电力系统实验教学示范中心 | 智能发电实验教学中心 | - | ------ | ------------------------------------- | ---------- | ---------- | -------------- | ------ | ------- | -------- | ---------------------------------- | ------------------------------ | ---------------------------- | -------------------- | ---------------- | ------------------------------ | ------------------------------------ | ------------ | ---------------------------------- | -------------------- | - | news | english | xxgk | zs | career | fgw | wenming | ztjy | energy-saving | gec | green-energy | hhsyzx | jsjxfzzx | jxfz | mpep | nydlzk | rpstec | spgc | - | notice | events | zxgkxx | zxxx | tzgg | 1411 | 2202 | 5575 | tzgg | 1959 | 118 | 3709 | 3909 | 3330 | 1134 | tzgg | 1366 | 4449 | +| 新闻网 | 信息公开网 | 本科招生网 | 本科就业信息网 | 文明办 | 学习路上 | “学条例 守党纪”专题网 | 上海新能源人才技术教育交流中心 | 上海绿色能源并网技术研究中心 | 能源电力智库 | 智能发电实验教学中心 | +| ------ | ---------- | ---------- | -------------- | ------- | -------- | --------------------- | ------------------------------ | ---------------------------- | ------------ | -------------------- | +| news | xxgk | zs | career | wenming | ztjy | xxjy | gec | green-energy | nydlzk | spgc | +| notice | zxgkxx | zxxx | tzgg | 2202 | 5575 | 5973 | 1959 | 118 | tzgg | 4449 | 参数与来源页面对应规则为:\`https://\${type}.shiep.edu.cn/\${id}/list.htm\``, }; diff --git a/lib/routes/shiep/namespace.ts b/lib/routes/shiep/namespace.ts index d7acb533547752..aedd29146505dc 100644 --- a/lib/routes/shiep/namespace.ts +++ b/lib/routes/shiep/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海电力大学', url: 'bwc.shiep.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/shisu/en.ts b/lib/routes/shisu/en.ts new file mode 100644 index 00000000000000..6d89518ccf3bec --- /dev/null +++ b/lib/routes/shisu/en.ts @@ -0,0 +1,75 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +const url = 'http://en.shisu.edu.cn'; +const urlBackup = 'https://en.shisu.edu.cn'; + +export const route: Route = { + path: '/en/:section', + categories: ['university'], + example: '/shisu/en/news', + parameters: { section: 'The name of resources' }, + radar: [ + { + source: ['en.shisu.edu.cn/resources/:section/'], + target: '/en/:section', + }, + ], + name: 'SISU TODAY | FEATURED STORIES', + maintainers: ['Duuckjing'], + handler, + description: `- features: Read a series of in-depth stories about SISU faculty, students, alumni and beyond campus. + - news: SISU TODAY English site.`, +}; + +async function process(baseUrl: string, section: any) { + const r = await ofetch(`${baseUrl}/resources/${section}/`); + const $ = load(r); + const itemsoup = $('.tab-con:nth-child(1) ul li') + .toArray() + .map((i0) => { + const i = $(i0); + const img = i.find('img').attr('src'); + const link = `${baseUrl}${i.find('h3>a').attr('href')}`; + return { + title: i.find('h3>a').text().trim(), + link, + pubDate: parseDate(i.find('p.time').text()), + itunes_item_image: `${baseUrl}${img}`, + }; + }); + const items = await Promise.all( + itemsoup.map((j) => + cache.tryGet(j.link, async () => { + const r = await ofetch(j.link); + const $ = load(r); + j.description = $('.details-con') + .html()! + .replaceAll(/<o:p>[\S\s]*?<\/o:p>/g, '') + .replaceAll(/(<p[^>]*> <\/p>\s*)+/gm, '<p> </p>'); + return j; + }) + ) + ); + return { + title: String(section) === 'features' ? 'FEATURED STORIES' : 'SISU TODAY', + link: `${url}/resources/${section}/`, + item: items, + }; +} + +async function handler(ctx) { + const { section } = ctx.req.param(); + let res: any; + try { + await ofetch(url); + res = process(url, section); + } catch { + await ofetch(urlBackup); + res = process(urlBackup, section); + } + return res; +} diff --git a/lib/routes/shisu/namespace.ts b/lib/routes/shisu/namespace.ts index e25ff24e13267b..67b47080e240e5 100644 --- a/lib/routes/shisu/namespace.ts +++ b/lib/routes/shisu/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海外国语大学', - url: 'news.shisu.edu.cn', + url: 'shisu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/shisu/news.ts b/lib/routes/shisu/news.ts index 6fed5ae4201c22..79b983b9c675eb 100644 --- a/lib/routes/shisu/news.ts +++ b/lib/routes/shisu/news.ts @@ -1,18 +1,17 @@ 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'; -import timezone from '@/utils/timezone'; const url = 'https://news.shisu.edu.cn'; const banner = 'https://news.shisu.edu.cn/news/index/39adf3d9ae414bc39c6d3b9316ae531f.png'; export const route: Route = { - path: '/news/:category', + path: '/news/:section', categories: ['university'], - example: '/shisu/news/notice', - parameters: { category: '新闻的分类可根据自己的需要选择,首页为全部新闻' }, + example: '/shisu/news/news', + parameters: { section: '主站的新闻类别' }, features: { requireConfig: false, requirePuppeteer: false, @@ -23,20 +22,21 @@ export const route: Route = { }, radar: [ { - source: ['news.shisu.edu.cn/:category/index.html'], + source: ['news.shisu.edu.cn/:section/index.html'], + target: '/news/:section', }, ], name: '上外新闻', maintainers: ['Duuckjing'], handler, description: `| 首页 | 特稿 | 学术 | 教学 | 国际 | 校园 | 人物 | 视讯 | 公告 | - | ---- | ------- | --------- | ---------- | ------------- | ------ | ------ | ---------- | ------ | - | news | gazette | research- | academics- | international | campus | people | multimedia | notice |`, +| ---- | ------- | --------- | ---------- | ------------- | ------ | ------ | ---------- | ------ | +| news | gazette | research- | academics- | international | campus | people | multimedia | notice |`, }; async function handler(ctx) { const { section = 'news' } = ctx.req.param(); - const { data: r } = await got(`${url}/${section}/index.html`); + const r = await ofetch(`${url}/${section}/index.html`); const $ = load(r); let itemsoup; switch (section) { @@ -60,7 +60,7 @@ async function handler(ctx) { .map((i0) => { const i = $(i0); return { - title: i.find('h3>a').attr('title').trim(), + title: i.find('h3>a').attr('title')?.trim(), link: `${url}${i.find('h3>a').attr('href')}`, category: i.find('p>span:nth-child(1)').text(), }; @@ -69,12 +69,12 @@ async function handler(ctx) { const items = await Promise.all( itemsoup.map((j) => cache.tryGet(j.link, async () => { - const { data: r } = await got(j.link); + const r = await ofetch(j.link); const $ = load(r); const img = $('.tempWrap > ul > li:nth-child(1)> img').attr('src'); j.description = $('.ot_main_r .content').html(); j.author = $('.math_time_l > span:nth-child(3)').text().trim(); - j.pubDate = timezone(parseDate($('.math_time_l > span:nth-child(2)').text(), 'YYYY-MM-DD'), +8); + j.pubDate = parseDate($('.math_time_l > span:nth-child(2)').text(), 'YYYY-MM-DD'); if (!j.itunes_item_image) { j.itunes_item_image = img ? `${url}${img}` : banner; } @@ -84,8 +84,8 @@ async function handler(ctx) { ); return { - title: `上外新闻|SISU TODAY -${section.charAt(0).toUpperCase() + section.slice(1)}`, - image: 'https://bkimg.cdn.bcebos.com/pic/8d5494eef01f3a296b70affa9825bc315c607c4d?x-bce-process=image/resize,m_lfit,w_536,limit_1/quality,Q_70', + title: `上外新闻|SISU TODAY - ${section.charAt(0).toUpperCase() + section.slice(1)}`, + image: 'https://upload.wikimedia.org/wikipedia/zh/thumb/0/06/Shanghai_International_Studies_University_logo.svg/300px-Shanghai_International_Studies_University_logo.svg.png', link: `${url}/${section}/index.html`, item: items, }; diff --git a/lib/routes/shmeea/index.ts b/lib/routes/shmeea/index.ts index 13cb21bf20c05b..900820b1f8ca8a 100644 --- a/lib/routes/shmeea/index.ts +++ b/lib/routes/shmeea/index.ts @@ -21,13 +21,13 @@ export const route: Route = { name: '消息', maintainers: ['jialinghui', 'Misaka13514'], handler, - description: `:::tip + description: `::: tip 例如:消息速递的网址为 \`https://www.shmeea.edu.cn/page/08000/index.html\`,则页面 ID 为 \`08000\`。 - ::: +::: - :::warning +::: warning 暂不支持大类分类和[院内动态](https://www.shmeea.edu.cn/page/19000/index.html) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/shmeea/namespace.ts b/lib/routes/shmeea/namespace.ts index c3bc5507072377..7c68c30501ceb0 100644 --- a/lib/routes/shmeea/namespace.ts +++ b/lib/routes/shmeea/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: '上海市教育考试院', url: 'www.shmeea.edu.cn', description: `官方网址:[https://www.shmeea.edu.cn](https://www.shmeea.edu.cn)`, + lang: 'zh-CN', }; diff --git a/lib/routes/shmtu/namespace.ts b/lib/routes/shmtu/namespace.ts index 664e2b931d6d6a..8474099c0d9cf3 100644 --- a/lib/routes/shmtu/namespace.ts +++ b/lib/routes/shmtu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海海事大学', url: 'jwc.shmtu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/shoac/namespace.ts b/lib/routes/shoac/namespace.ts index a0b4cc8c06f4ef..311311758027f8 100644 --- a/lib/routes/shoac/namespace.ts +++ b/lib/routes/shoac/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海东方艺术中心', url: 'shoac.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/shopback/namespace.ts b/lib/routes/shopback/namespace.ts index ab77fd20112136..fade71929cf95f 100644 --- a/lib/routes/shopback/namespace.ts +++ b/lib/routes/shopback/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ShopBack', url: 'shopback.com.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/shopify/apps/[handle].reviews.ts b/lib/routes/shopify/apps/[handle].reviews.ts index f0e796ed14f02a..c8b37a889bab39 100644 --- a/lib/routes/shopify/apps/[handle].reviews.ts +++ b/lib/routes/shopify/apps/[handle].reviews.ts @@ -72,7 +72,7 @@ async function handler(ctx: Context): Promise<Data> { title: `Reviews handle:${handle} page:${page} – Shopify App Store`, link: `${baseURL}/${handle}/reviews`, allowEmpty: true, - language: 'en-US', + language: 'en-us', item: items, }; } diff --git a/lib/routes/shopify/apps/search.ts b/lib/routes/shopify/apps/search.ts index b8388efc3d1132..9da81cc6d998c6 100644 --- a/lib/routes/shopify/apps/search.ts +++ b/lib/routes/shopify/apps/search.ts @@ -15,8 +15,11 @@ export const route: Route = { { source: ['apps.shopify.com/search'], target: (_params, url) => { - const { searchParams } = new URL(url).searchParams; - return searchParams.has('q') ? `/shopify/apps/search/${searchParams.get('q')}` : null; + const searchParams = new URL(url).searchParams; + if (!searchParams.has('q')) { + return ''; + } + return `/shopify/apps/search/${searchParams.get('q')}`; }, }, ], @@ -76,7 +79,7 @@ async function handler(ctx: Context): Promise<Data> { link: `https://apps.shopify.com/search?q=${q}`, // description: `Search results for "${q}" – Shopify App Store`, allowEmpty: true, - language: 'en-US', + language: 'en-us', item: items, }; } diff --git a/lib/routes/shopify/namespace.ts b/lib/routes/shopify/namespace.ts index 6d971caef4a9b3..d73886b6938240 100644 --- a/lib/routes/shopify/namespace.ts +++ b/lib/routes/shopify/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Shopify', url: 'shopify.com', + lang: 'en', }; diff --git a/lib/routes/shoppingdesign/namespace.ts b/lib/routes/shoppingdesign/namespace.ts index 02e790135486f5..dd620146a22b8e 100644 --- a/lib/routes/shoppingdesign/namespace.ts +++ b/lib/routes/shoppingdesign/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Shopping Design', url: 'www.shoppingdesign.com.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/showstart/artist.ts b/lib/routes/showstart/artist.ts index 062c1716004851..564253e7a8231d 100644 --- a/lib/routes/showstart/artist.ts +++ b/lib/routes/showstart/artist.ts @@ -1,6 +1,7 @@ -import { Route } from '@/types'; +import { Data, Route } from '@/types'; import { TITLE, HOST } from './const'; import { fetchPerformerInfo } from './service'; +import type { Context } from 'hono'; export const route: Route = { path: '/artist/:id', @@ -20,15 +21,15 @@ export const route: Route = { source: ['www.showstart.com/artist/:id'], }, ], - name: '音乐人 - 演出更新', + name: '按音乐人 - 演出更新', maintainers: ['lchtao26'], handler, - description: `:::tip -音乐人 ID 查询: \`/showstart/search/artist/:keyword\`,如: [https://rsshub.app/showstart/search/artist/ 周杰伦](https://rsshub.app/showstart/search/artist/周杰伦) + description: `::: tip +音乐人 ID 查询: \`/showstart/search/artist/:keyword\`,如: [https://rsshub.app/showstart/search/artist/周杰伦](https://rsshub.app/showstart/search/artist/周杰伦) :::`, }; -async function handler(ctx) { +async function handler(ctx: Context): Promise<Data> { const id = ctx.req.param('id'); const artist = await fetchPerformerInfo({ performerId: id, diff --git a/lib/routes/showstart/brand.ts b/lib/routes/showstart/brand.ts index 51f5b001097aec..ffb7c70c1b9a24 100644 --- a/lib/routes/showstart/brand.ts +++ b/lib/routes/showstart/brand.ts @@ -1,6 +1,7 @@ -import { Route } from '@/types'; +import { Data, Route } from '@/types'; import { TITLE, HOST } from './const'; import { fetchBrandInfo } from './service'; +import type { Context } from 'hono'; export const route: Route = { path: '/brand/:id', @@ -20,15 +21,15 @@ export const route: Route = { source: ['www.showstart.com/host/:id'], }, ], - name: '厂牌 - 演出更新', + name: '按厂牌 - 演出更新', maintainers: ['lchtao26'], handler, - description: `:::tip -厂牌 ID 查询: \`/showstart/search/brand/:keyword\`,如: [https://rsshub.app/showstart/search/brand/ 声场](https://rsshub.app/showstart/search/brand/声场) + description: `::: tip +厂牌 ID 查询: \`/showstart/search/brand/:keyword\`,如: [https://rsshub.app/showstart/search/brand/声场](https://rsshub.app/showstart/search/brand/声场) :::`, }; -async function handler(ctx) { +async function handler(ctx: Context): Promise<Data> { const id = ctx.req.param('id'); const brand = await fetchBrandInfo({ brandId: id, diff --git a/lib/routes/showstart/event.ts b/lib/routes/showstart/event.ts index 45d26e253df171..5b822baa98eb55 100644 --- a/lib/routes/showstart/event.ts +++ b/lib/routes/showstart/event.ts @@ -1,6 +1,7 @@ -import { Route } from '@/types'; +import { Data, Route } from '@/types'; import { TITLE, HOST } from './const'; import { fetchActivityList, fetchDictionary } from './service'; +import type { Context } from 'hono'; export const route: Route = { path: '/event/:cityCode/:showStyle?', @@ -15,19 +16,19 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, - name: '演出更新', + name: '按城市 - 演出更新', maintainers: ['lchtao26'], handler, - description: `:::tip -- 演出城市 \`cityCode\` 查询: \`/showstart/search/city/:keyword\`, 如: [https://rsshub.app/showstart/search/city/ 杭州](https://rsshub.app/showstart/search/city/杭州) + description: `::: tip +- 演出城市 \`cityCode\` 查询: \`/showstart/search/city/:keyword\`, 如: [https://rsshub.app/showstart/search/city/杭州](https://rsshub.app/showstart/search/city/杭州) -- 演出风格 \`showStyle\` 查询: \`/showstart/search/style/:keyword\`,如: [https://rsshub.app/showstart/search/style/ 摇滚](https://rsshub.app/showstart/search/style/摇滚) +- 演出风格 \`showStyle\` 查询: \`/showstart/search/style/:keyword\`,如: [https://rsshub.app/showstart/search/style/摇滚](https://rsshub.app/showstart/search/style/摇滚) :::`, }; -async function handler(ctx) { - const cityCode = Number.parseInt(ctx.req.param('cityCode')); - const showStyle = Number.parseInt(ctx.req.param('showStyle')); +async function handler(ctx: Context): Promise<Data> { + const cityCode = Number.parseInt(ctx.req.param('cityCode')).toString(); + const showStyle = Number.parseInt(ctx.req.param('showStyle')).toString(); const items = await fetchActivityList({ cityCode, showStyle, diff --git a/lib/routes/showstart/namespace.ts b/lib/routes/showstart/namespace.ts index d267d925b4683b..f91e20372c376d 100644 --- a/lib/routes/showstart/namespace.ts +++ b/lib/routes/showstart/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '秀动网', url: 'www.showstart.com', + lang: 'zh-CN', }; diff --git a/lib/routes/showstart/search.ts b/lib/routes/showstart/search.ts index e06dc76e43174a..0794ac08fb5474 100644 --- a/lib/routes/showstart/search.ts +++ b/lib/routes/showstart/search.ts @@ -1,12 +1,44 @@ -import { Route } from '@/types'; +import { Data, Route } from '@/types'; import { TITLE, HOST } from './const'; -import { fetchActivityList, fetchPerformerList, fetchBrandList, fetchCityList, fetchStyleList } from './service'; +import { fetchActivityList, fetchPerformerList, fetchSiteList, fetchBrandList, fetchCityList, fetchStyleList } from './service'; +import type { Context } from 'hono'; export const route: Route = { path: '/search/:type/:keyword?', categories: ['shopping'], example: '/showstart/search/live', - parameters: { type: '类别', keyword: '搜索关键词' }, + parameters: { + keyword: '搜索关键词', + type: { + description: '类别', + options: [ + { + value: 'event', + label: '演出', + }, + { + value: 'artist', + label: '音乐人', + }, + { + value: 'site', + label: '场地', + }, + { + value: 'brand', + label: '厂牌', + }, + { + value: 'city', + label: '城市', + }, + { + value: 'style', + label: '风格', + }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -20,7 +52,7 @@ export const route: Route = { handler, }; -async function handler(ctx) { +async function handler(ctx: Context): Promise<Data> { const type = ctx.req.param('type') || ''; const keyword = ctx.req.param('keyword') || ''; @@ -31,39 +63,41 @@ async function handler(ctx) { link: HOST, item: await fetchActivityList({ keyword }), }; - break; case 'artist': return { title: `${TITLE} - 搜艺人 - ${keyword || '全部'}`, link: HOST, item: await fetchPerformerList({ searchKeyword: keyword }), }; - break; + case 'site': + return { + title: `${TITLE} - 搜场地 - ${keyword || '全部'}`, + link: HOST, + item: await fetchSiteList({ searchKeyword: keyword }), + }; case 'brand': return { title: `${TITLE} - 搜厂牌 - ${keyword || '全部'}`, link: HOST, item: await fetchBrandList({ searchKeyword: keyword }), }; - break; case 'city': return { title: `${TITLE} - 搜城市 - ${keyword || '全部'}`, link: HOST, item: await fetchCityList(keyword), }; - break; case 'style': return { title: `${TITLE} - 搜风格 - ${keyword || '全部'}`, link: HOST, item: await fetchStyleList(keyword), }; - break; default: return { title: `${TITLE} - 搜演出 - ${type || '全部'}`, link: HOST, + allowEmpty: true, item: await fetchActivityList({ keyword: type }), }; } diff --git a/lib/routes/showstart/service.ts b/lib/routes/showstart/service.ts index a7f6ba71de8234..0ba5b8cbcc3703 100644 --- a/lib/routes/showstart/service.ts +++ b/lib/routes/showstart/service.ts @@ -2,37 +2,39 @@ import { HOST } from './const'; import { getAccessToken, post, sortBy, uniqBy } from './utils'; async function fetchActivityList( - params = { - pageNo: '1', - pageSize: '30', - cityCode: '', - activityIds: '', - coupon: '', - keyword: '', - organizerId: '', - performerId: '', - showStyle: '', - showTime: '', - showType: '', - siteId: '', - sortType: '', - themeId: '', - timeRange: '', - tourId: '', - type: '', - tag: '', - } + params: Partial<{ + pageNo: string; + pageSize: string; + cityCode: string; + activityIds: string; + coupon: string; + keyword: string; + organizerId: string; + performerId: string; + showStyle: string; + showTime: string; + showType: string; + siteId: string; + sortType: string; + themeId: string; + timeRange: string; + tourId: string; + type: string; + tag: string; + }> ) { + params.pageNo = params.pageNo || '1'; + params.pageSize = params.pageSize || '30'; const accessToken = await getAccessToken(); const resp = await post('/web/activity/list', accessToken, params); return resp.result.result.map((item) => formatActivity(item)); } -const image = (src) => (src ? `<img src="${src}" />` : ''); -const time = (time) => (time ? `<p>演出时间:${time}</p>` : ''); -const address = (cityName, siteName) => (cityName || siteName ? `<p>地址:${[cityName, siteName].join(' - ')}</p>` : ''); -const performers = (name) => (name ? `<p>艺人:${name}</p>` : ''); -const price = (price) => (price ? `<p>票价:${price}</p>` : ''); +const image = (src: string) => (src ? `<img src="${src}" />` : ''); +const time = (time: string) => (time ? `<p>演出时间:${time}</p>` : ''); +const address = (cityName: string, siteName: string) => (cityName || siteName ? `<p>地址:${[cityName, siteName].join(' - ')}</p>` : ''); +const performers = (name: string) => (name ? `<p>艺人:${name}</p>` : ''); +const price = (price: string) => (price ? `<p>票价:${price}</p>` : ''); function formatActivity(item) { return { @@ -43,13 +45,15 @@ function formatActivity(item) { } async function fetchPerformerList( - params = { - pageNo: '1', - pageSize: '30', - searchKeyword: '', - styleId: '', - } + params: Partial<{ + pageNo: string; + pageSize: string; + searchKeyword: string; + styleId: string; + }> ) { + params.pageNo = params.pageNo || '1'; + params.pageSize = params.pageSize || '30'; const accessToken = await getAccessToken(); const resp = await post('/web/performer/list', accessToken, params); return resp.result.result.map((item) => ({ @@ -59,15 +63,11 @@ async function fetchPerformerList( })); } -async function fetchPerformerInfo( - params = { - performerId: '', - } -) { +async function fetchPerformerInfo(params: { performerId: string }) { const accessToken = await getAccessToken(); const resp = await post('/web/performer/info', accessToken, params); return { - id: params.id, + id: params.performerId, name: resp.result.name, content: resp.result.content, avatar: resp.result.avatar, @@ -77,15 +77,11 @@ async function fetchPerformerInfo( }; } -async function fetchBrandInfo( - params = { - brandId: '', - } -) { +async function fetchBrandInfo(params: { brandId: string }) { const accessToken = await getAccessToken(); const resp = await post('/web/brand/info', accessToken, params); return { - id: params.id, + id: params.brandId, name: resp.result.name, content: resp.result.content, avatar: resp.result.avatar, @@ -94,13 +90,45 @@ async function fetchBrandInfo( }; } +async function fetchSiteList( + params: Partial<{ + pageNo: string; + pageSize: string; + searchKeyword: string; + }> +) { + params.pageNo = params.pageNo || '1'; + params.pageSize = params.pageSize || '30'; + const accessToken = await getAccessToken(); + const resp = await post('/web/site/list', accessToken, params); + return resp.result.result.map((item) => ({ + title: `${item.cityName} - ${item.name}`, + link: `${HOST}/venue/${item.id}`, + description: `id: ${item.id}`, + })); +} + +async function fetchSiteInfo(params: { siteId: string }) { + const accessToken = await getAccessToken(); + const resp = await post('/web/site/info', accessToken, params); + return { + id: params.siteId, + name: `${resp.result.cityName} - ${resp.result.name}`, + address: resp.result.address, + avatar: resp.result.avatar, + poster: resp.result.poster, + }; +} + async function fetchBrandList( - params = { - pageNo: '1', - pageSize: '30', - searchKeyword: '', - } + params: Partial<{ + pageNo: string; + pageSize: string; + searchKeyword: string; + }> ) { + params.pageNo = params.pageNo || '1'; + params.pageSize = params.pageSize || '30'; const accessToken = await getAccessToken(); const resp = await post('/web/brand/list', accessToken, params); return resp.result.result.map((item) => ({ @@ -131,7 +159,7 @@ async function fetchCityList(keyword = '') { // so we need to fetch all city items and then extract styles from them async function fetchStyleList(keyword = '') { const resp = await fetchParams(); - let styles = resp.result.flatMap((item) => item.styles); + let styles = resp.result.flatMap((item) => item.styles) as Array<{ key: string; showName: string }>; styles = uniqBy(styles, 'key'); styles = sortBy(styles, 'key'); return styles @@ -143,16 +171,16 @@ async function fetchStyleList(keyword = '') { })); } -async function fetchDictionary(cityCode, showStyle) { +async function fetchDictionary(cityCode: string, showStyle: string) { const resp = await fetchParams(); - const target = resp.result.find((item) => item.cityCode === cityCode); + const target = resp.result.find((item) => String(item.cityCode) === cityCode); if (!target) { return {}; } return { cityName: target.cityName, - showName: target.styles.find((item) => item.key === showStyle)?.showName, + showName: target.styles.find((item) => String(item.key) === showStyle)?.showName, }; } -export { fetchActivityList, fetchCityList, fetchStyleList, fetchPerformerList, fetchPerformerInfo, fetchBrandList, fetchBrandInfo, fetchDictionary }; +export { fetchActivityList, fetchCityList, fetchStyleList, fetchPerformerList, fetchPerformerInfo, fetchSiteList, fetchSiteInfo, fetchBrandList, fetchBrandInfo, fetchDictionary }; diff --git a/lib/routes/showstart/site.ts b/lib/routes/showstart/site.ts new file mode 100644 index 00000000000000..46da8ca9519356 --- /dev/null +++ b/lib/routes/showstart/site.ts @@ -0,0 +1,41 @@ +import { Data, Route } from '@/types'; +import { TITLE, HOST } from './const'; +import { fetchActivityList, fetchSiteInfo } from './service'; +import { Context } from 'hono'; + +export const route: Route = { + path: '/site/:siteId', + categories: ['shopping'], + example: '/showstart/site/3583', + parameters: { siteId: '演出场地 (编号)' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.showstart.com/venue/:id'], + }, + ], + name: '按场地 - 演出更新', + maintainers: ['lchtao26'], + handler, + description: `::: tip +- 演出场地 ID 查询: \`/showstart/search/site/:keyword\`, 如: [https://rsshub.app/showstart/search/site/酒球会](https://rsshub.app/showstart/search/site/酒球会) +:::`, +}; + +async function handler(ctx: Context): Promise<Data> { + const siteId = Number.parseInt(ctx.req.param('siteId')).toString(); + const [activityList, siteInfo] = await Promise.all([fetchActivityList({ siteId }), fetchSiteInfo({ siteId })]); + return { + title: `${TITLE} - ${siteInfo.name}`, + description: siteInfo.address, + link: HOST, + item: activityList, + }; +} diff --git a/lib/routes/showstart/utils.ts b/lib/routes/showstart/utils.ts index 996d1468819c2f..b3178f1448ff12 100644 --- a/lib/routes/showstart/utils.ts +++ b/lib/routes/showstart/utils.ts @@ -1,9 +1,9 @@ -import got from '@/utils/got'; import md5 from '@/utils/md5'; +import ofetch from '@/utils/ofetch'; const uuid = (length = 20) => { const e = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + Date.now(); - const r = []; + const r: string[] = []; for (let i = 0; i < length; i++) { r.push(e.charAt(Math.floor(Math.random() * e.length))); } @@ -35,10 +35,11 @@ const getAccessToken = async () => { return cookieMap.get('accessToken'); }; -const post = async (requestPath, accessToken = md5(Date.now().toString()), payload) => { +const post = async (requestPath: string, accessToken = md5(Date.now().toString()), payload?: any) => { const traceId = uuid(32) + Date.now(); - const { data: response } = await got.post(`https://www.showstart.com/api${requestPath}`, { + const response = await ofetch(`https://www.showstart.com/api${requestPath}`, { + method: 'POST', headers: { cdeviceinfo: encodeURIComponent(JSON.stringify(devioceInfo)), cdeviceno: cookieMap.get('token'), @@ -53,14 +54,14 @@ const post = async (requestPath, accessToken = md5(Date.now().toString()), paylo cusname: '', cusut: '', cversion: '999', - }, - json: payload, + } as HeadersInit, + body: payload, }); return response; }; -function sortBy(items, key) { +function sortBy(items: any[], key: string) { return items.sort((a, b) => { if (a[key] < b[key]) { return -1; @@ -72,7 +73,7 @@ function sortBy(items, key) { }); } -function uniqBy(items, key) { +function uniqBy(items: any[], key: string) { const set = new Set(); return items.filter((item) => { if (set.has(item[key])) { diff --git a/lib/routes/shu/global.ts b/lib/routes/shu/global.ts new file mode 100644 index 00000000000000..553df66190070d --- /dev/null +++ b/lib/routes/shu/global.ts @@ -0,0 +1,95 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; // cheerio@1.0.0 +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const noticeType = { + tzgg: { title: '上海大学国际部港澳台-通知公告', url: 'https://global.shu.edu.cn/cd/tzgg/3.htm' }, +}; + +export const route: Route = { + path: '/global/:type?', + categories: ['university'], + example: '/shu/global/tzgg', + parameters: { type: '分类,默认为通知公告' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['global.shu.edu.cn/'], + target: '/global', + }, + ], + name: '国际部港澳台办公室', + maintainers: ['GhhG123'], + handler, + url: 'global.shu.edu.cn/', + description: `| 通知公告 | +| -------- | +| tzgg |`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type') ?? 'tzgg'; + const rootUrl = 'https://global.shu.edu.cn'; + + // 发起 HTTP GET 请求 + const response = await got({ + method: 'get', + + /* headers: { + 'user-agent': UA, + cookie: await getCookie(ctx), + }, */ + url: noticeType[type].url, + }); + + const $ = load(response.data); + + const list = $('div.only-list1 ul li') // 定位到HTML结构中的li元素 + .toArray() + .map((el) => { + const item = $(el); // 使用Cheerio包装每个li元素 + const rawLink = item.find('a').attr('href'); + const pubDate = item.find('span').text().trim(); // 提取日期 + + return { + title: item.find('a').text().trim(), // 获取标题 + link: rawLink ? new URL(rawLink, rootUrl).href : rootUrl, // 生成完整链接 + pubDate: timezone(parseDate(pubDate, 'YYYY年MM月DD日'), +8), // 解析并转换日期 + description: '', // 没有提供简要描述,设为空字符串 + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got({ + method: 'get', + url: item.link, + }); // 获取详情页内容 + const content = load(detailResponse.data); // 使用cheerio解析内容 + + item.description = content('#vsb_content_2 .v_news_content').html() || '内容无法提取'; // 提取内容区详情 + + return item; // 返回完整的item + }) + ) + ); + + return { + title: noticeType[type].title, + description: noticeType[type].title, + link: noticeType[type].url, + image: 'https://www.shu.edu.cn/__local/0/08/C6/1EABE492B0CF228A5564D6E6ABE_779D1EE3_5BF7.png', + item: items, + }; +} diff --git a/lib/routes/shu/gs.ts b/lib/routes/shu/gs.ts new file mode 100644 index 00000000000000..9b693ae89c0756 --- /dev/null +++ b/lib/routes/shu/gs.ts @@ -0,0 +1,107 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; // cheerio@1.0.0 +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const noticeType = { + zhxw: { title: '上海大学研究生院-综合新闻', url: 'https://gs.shu.edu.cn/xwlb/zh.htm' }, // 综合新闻 + pygl: { title: '上海大学研究生院-培养管理', url: 'https://gs.shu.edu.cn/xwlb/py.htm' }, // local //BUG error: Request https://gs1.shu.edu.cn:8080/py/KCBInfo.asp fail: TypeError: fetch failed + gjjl: { title: '上海大学研究生院-国际交流', url: 'https://gs.shu.edu.cn/xwlb/gjjl.htm' }, +}; + +export const route: Route = { + path: '/gs/:type?', + categories: ['university'], + example: '/shu/gs/zhxw', + parameters: { type: '分类,默认为学术公告' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['gs.shu.edu.cn/'], + target: '/gs', + }, + ], + name: '研究生院', + maintainers: ['GhhG123'], + handler, + url: 'gs.shu.edu.cn/', + description: `| 综合新闻 | 培养管理 | 国际交流 | +| -------- | --------- | --------- | +| zhxw | pygl | gjjl |`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type') ?? 'zhxw'; + const rootUrl = 'https://gs.shu.edu.cn'; + + // 发起 HTTP GET 请求 + const response = await got({ + method: 'get', + + /* headers: { + 'user-agent': UA, + cookie: await getCookie(ctx), + }, */ + url: noticeType[type].url, + }); + + const $ = load(response.data); + + const list = $('tr[id^="line_u17_"]') // 定位到每个包含新闻的<tr>元素 + .toArray() + .map((el) => { + const item = $(el); // 使用Cheerio包装每个<tr>元素 + const rawLink = item.find('a').attr('href'); // 获取链接 + const title = item.find('a').text().trim(); // 获取标题 + const dateParts = item.find('td').eq(1).text().trim(); // 获取日期 + + return { + title, // 获取标题 + link: rawLink ? new URL(rawLink, rootUrl).href : rootUrl, // 生成完整链接 + pubDate: timezone(parseDate(dateParts, 'YYYY/MM/DD HH:mm:ss'), +8), // 解析日期 + description: item.find('td').eq(2).text().trim(), // 提取访问次数或其他信息 + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const url = new URL(item.link); // 创建 URL 对象以验证链接 + // 确保链接是以正确的域名开头,并且不为空 + if (url.hostname === 'gs1.shu.edu.cn') { + // 需校内访问 + // Skip or handle differently for URLs with gs1.shu.edu.cn domain + item.description = 'gs1.shu.edu.cn, 无法直接获取'; + return item; + } + + const detailResponse = await got({ + method: 'get', + url: item.link, + }); // 获取详情页内容 + const content = load(detailResponse.data); // 使用cheerio解析内容 + + item.description = content('#vsb_content .v_news_content').html() || item.description; + + return item; // 返回完整的item + }) + ) + ); + + return { + title: noticeType[type].title, + description: noticeType[type].title, + link: noticeType[type].url, + image: 'https://www.shu.edu.cn/__local/0/08/C6/1EABE492B0CF228A5564D6E6ABE_779D1EE3_5BF7.png', + item: items, + }; +} diff --git a/lib/routes/shu/index.ts b/lib/routes/shu/index.ts index 33dabbd888d754..abdf79a0b1f29c 100644 --- a/lib/routes/shu/index.ts +++ b/lib/routes/shu/index.ts @@ -1,23 +1,20 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; -import { load } from 'cheerio'; +import { load } from 'cheerio'; // cheerio@1.0.0 import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; -const host = 'https://www.shu.edu.cn/'; -const alias = new Map([ - ['news', 'zhxw'], // 综合新闻 - ['research', 'kydt1'], // 科研动态 - ['kydt', 'kydt1'], // 科研动态 - ['notice', 'tzgg'], // 通知公告 - ['important', 'zyxw'], // 重要新闻 -]); +const noticeType = { + tzgg: { title: '上海大学 - 通知公告', url: 'https://www.shu.edu.cn/tzgg.htm' }, + zyxw: { title: '上海大学 - 重要新闻', url: 'https://www.shu.edu.cn/zyxw.htm' }, +}; export const route: Route = { - path: '/:type?', + path: '/news/:type?', categories: ['university'], - example: '/shu/news', - parameters: { type: '消息类型,默认为`news`' }, + example: '/shu/news/tzgg', + parameters: { type: '分类,默认为通知公告' }, features: { requireConfig: false, requirePuppeteer: false, @@ -28,50 +25,71 @@ export const route: Route = { }, radar: [ { - source: ['www.shu.edu.cn/:type'], - target: '/:type', + source: ['www.shu.edu.cn/'], + target: '/news', }, ], - name: '官网信息', - maintainers: ['lonelyion'], + name: '官网通知公告', + maintainers: ['lonelyion', 'GhhG123'], handler, - description: `| 综合新闻 | 科研动态 | 通知公告 | 重要新闻 | - | -------- | -------- | -------- | --------- | - | news | research | notice | important |`, + url: 'www.shu.edu.cn/', + description: `| 通知公告 | 重要新闻 | +| -------- | --------- | +| tzgg | zyxw |`, }; async function handler(ctx) { - const type = ctx.req.param('type') || 'news'; - const link = `https://www.shu.edu.cn/${alias.get(type) || type}.htm`; - const respond = await got.get(link); - const $ = load(respond.data); - const title = $('title').text(); - const list = $('.ej_main .list') - .find('li') - .slice(0, 5) + const type = ctx.req.param('type') ?? 'tzgg'; + const rootUrl = 'https://www.shu.edu.cn'; + + // 发起 HTTP GET 请求 + const response = await got({ + method: 'get', + + /* headers: { + 'user-agent': UA, + cookie: await getCookie(ctx), + }, */ + url: noticeType[type].url, + }); + + const $ = load(response.data); + + const list = $('div.list ul li') // 以下获取信息需要根据网页结构定制 + // For cheerio 1.x.x . The item parameter in the .map callback is now explicitly typed as a Cheerio<Element>, not just Element. --fixed .toArray() - .map((ele) => ({ - title: $(ele).find('.bt').text(), - link: new URL($(ele).find('a').attr('href'), host).href, - date: $(ele).find('.sj').text(), - })); + .map((el) => { + const item = $(el); // Wrap `el` in a Cheerio object + const rawLink = item.find('a').attr('href'); + return { + title: item.find('p.bt').text().trim(), + link: rawLink ? new URL(rawLink, rootUrl).href : rootUrl, + pubDate: timezone(parseDate(item.find('p.sj').text().trim(), 'YYYY.MM.DD'), +8), + description: item.find('p.zy').text().trim(), + }; + }); - const all = await Promise.all( + const items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { - const response = await got.get(item.link); - const $ = load(response.data); - item.author = $('.xx>:nth-child(2)').text().trim().slice(3); // 投稿:xxx - item.pubDate = parseDate(item.date, 'YYYY.MM.DD'); - item.description = $('.v_news_content').html() || item.title; + const detailResponse = await got({ + method: 'get', + url: item.link, + }); + const content = load(detailResponse.data); + + item.description = content('#vsb_content .v_news_content').html() || item.description; + return item; }) ) ); return { - title, - link, - item: all, + title: noticeType[type].title, + description: noticeType[type].title, + link: noticeType[type].url, + image: 'https://www.shu.edu.cn/__local/0/08/C6/1EABE492B0CF228A5564D6E6ABE_779D1EE3_5BF7.png', + item: items, }; } diff --git a/lib/routes/shu/jwb.ts b/lib/routes/shu/jwb.ts index 6870cf345e1907..61d908777f64b7 100644 --- a/lib/routes/shu/jwb.ts +++ b/lib/routes/shu/jwb.ts @@ -8,23 +8,23 @@ const host = 'https://jwb.shu.edu.cn/'; const alias = new Map([ ['notice', 'tzgg'], // 通知公告 ['news', 'xw'], // 新闻动态 - ['policy', 'zcwj'], // 政策文件 + /* ['policy', 'zcwj'], 政策文件 //BUG */ ]); export const route: Route = { - path: ['/jwc/:type?', '/jwb/:type?'], + path: ['/jwb/:type?'], radar: [ { - source: ['www.shu.edu.cn/:type'], - target: '/:type', + source: ['www.shu.edu.cn/index'], + target: '/:type?', }, ], - name: 'Unknown', - maintainers: [], + name: '教务部', + maintainers: ['tuxinghuan', 'GhhG123'], handler, - description: `| 通知通告 | 新闻 | 政策文件 | - | -------- | ---- | -------- | - | notice | news | policy |`, + description: `| 通知通告 | 新闻 | 政策文件(bug) | +| -------- | ---- | -------- | +| notice | news | policy |`, }; async function handler(ctx) { @@ -58,6 +58,7 @@ async function handler(ctx) { return { title, link, + image: 'https://www.shu.edu.cn/__local/0/08/C6/1EABE492B0CF228A5564D6E6ABE_779D1EE3_5BF7.png', item: all, }; } diff --git a/lib/routes/shu/namespace.ts b/lib/routes/shu/namespace.ts index 46b8e5f50a49b7..35930c2a216759 100644 --- a/lib/routes/shu/namespace.ts +++ b/lib/routes/shu/namespace.ts @@ -2,5 +2,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海大学', - url: 'jwb.shu.edu.cn', + url: 'www.shu.edu.cn', + description: '上海大学相关网网站', + lang: 'zh-CN', }; diff --git a/lib/routes/shu/society.ts b/lib/routes/shu/society.ts new file mode 100644 index 00000000000000..6da04f7760ad8d --- /dev/null +++ b/lib/routes/shu/society.ts @@ -0,0 +1,62 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/journals/society/current', + categories: ['journal'], + example: '/journals/society/current', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '《社会》杂志当期目录', + maintainers: ['CNYoki'], + handler, +}; + +async function handler() { + const url = 'https://www.society.shu.edu.cn/CN/1004-8804/current.shtml'; + const response = await got(url); + const $ = load(response.body); + + // 提取刊出日期 + const pubDateText = $('.dqtab .njq') + .text() + .match(/刊出日期:(\d{4}-\d{2}-\d{2})/); + const pubDate = pubDateText ? parseDate(pubDateText[1]) : null; + + const items = $('.wenzhanglanmu') + .nextAll('.noselectrow') + .toArray() + .map((item) => { + const $item = $(item); + const titles = $item.find('.biaoti').text().trim(); + const links = $item.find('.biaoti').attr('href'); + const authors = $item.find('.zuozhe').text().trim(); + const abstract = $item.find('div[id^="Abstract"]').text().trim(); + + if (titles && links) { + return { + title: titles, + link: links, + description: abstract, + author: authors, + pubDate, + }; + } + return null; + }) + .filter((item) => item !== null); + + return { + title: '《社会》当期目录', + link: url, + item: items, + }; +} diff --git a/lib/routes/shu/xykd.ts b/lib/routes/shu/xykd.ts new file mode 100644 index 00000000000000..15a3cef593eca8 --- /dev/null +++ b/lib/routes/shu/xykd.ts @@ -0,0 +1,99 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; // cheerio@1.0.0 +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const noticeType = { + whxx: { title: '上海大学 - 文化信息', url: 'https://www.shu.edu.cn/xnrc/whxx.htm' }, + xsbg: { title: '上海大学 - 学术报告', url: 'https://www.shu.edu.cn/xnrc/xsbg.htm' }, +}; + +export const route: Route = { + path: '/xykd/:type?', + categories: ['university'], + example: '/shu/xykd/xsbg', + parameters: { type: '分类,默认为学术公告' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.shu.edu.cn/'], + target: '/xykd', + }, + ], + name: '校园看点', + maintainers: ['GhhG123'], + handler, + url: 'www.shu.edu.cn/', + description: `| 文化信息 | 学术报告 | +| -------- | --------- | +| whxx | xsbg |`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type') ?? 'xsbg'; + const rootUrl = 'https://www.shu.edu.cn'; + + // 发起 HTTP GET 请求 + const response = await got({ + method: 'get', + + /* headers: { + 'user-agent': UA, + cookie: await getCookie(ctx), + }, */ + url: noticeType[type].url, + }); + + const $ = load(response.data); + + const list = $('div.xsbg_list ul li') // 定位到HTML结构中的li元素 + .toArray() + .map((el) => { + const item = $(el); // 使用Cheerio包装每个li元素 + const rawLink = item.find('a').attr('href'); + const dateParts = item + .find('div.sj p') + .toArray() + .map((p) => $(p).text().trim()); // 提取日期部分 + + return { + title: item.find('p.bt').text().trim(), // 获取标题 + link: rawLink ? new URL(rawLink, rootUrl).href : rootUrl, // 生成完整链接 + pubDate: timezone(parseDate(`${dateParts[1]}-${dateParts[0]}`, 'MM-DD'), +8), // 拼接并解析日期 + description: item.find('div.zy').text().trim(), // 提取简要描述 + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got({ + method: 'get', + url: item.link, + }); // 获取详情页内容 + const content = load(detailResponse.data); // 使用cheerio解析内容 + + item.description = content('#vsb_content_500 .v_news_content').html() || item.description; // 提取内容区详情 + + return item; // 返回完整的item + }) + ) + ); + + return { + title: noticeType[type].title, + description: noticeType[type].title, + link: noticeType[type].url, + image: 'https://www.shu.edu.cn/__local/0/08/C6/1EABE492B0CF228A5564D6E6ABE_779D1EE3_5BF7.png', + item: items, + }; +} diff --git a/lib/routes/shuiguopai/namespace.ts b/lib/routes/shuiguopai/namespace.ts index c6bedb1456192e..04c343f0601070 100644 --- a/lib/routes/shuiguopai/namespace.ts +++ b/lib/routes/shuiguopai/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '水果派', url: 'shuiguopai.com', + lang: 'zh-CN', }; diff --git a/lib/routes/sicau/dky.ts b/lib/routes/sicau/dky.ts index 8e6d7e739e4b7f..9c9f7bf02d7b43 100644 --- a/lib/routes/sicau/dky.ts +++ b/lib/routes/sicau/dky.ts @@ -28,8 +28,8 @@ export const route: Route = { handler, url: 'dky.sicau.edu.cn/', description: `| 通知公告 | 学院动态 | 教学管理 | 动科大讲堂 | 就业信息 | - | -------- | -------- | -------- | ---------- | -------- | - | tzgg | xydt | jxgl | dkdjt | zpxx |`, +| -------- | -------- | -------- | ---------- | -------- | +| tzgg | xydt | jxgl | dkdjt | zpxx |`, }; async function handler(ctx) { diff --git a/lib/routes/sicau/jiaowu.ts b/lib/routes/sicau/jiaowu.ts new file mode 100644 index 00000000000000..565aabbe30100e --- /dev/null +++ b/lib/routes/sicau/jiaowu.ts @@ -0,0 +1,77 @@ +import { DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const $get = async (url: string, encoding = 'gb2312') => new TextDecoder(encoding).decode(await ofetch(url, { responseType: 'arrayBuffer' })); + +export const route: Route = { + path: '/jiaowu/jxtz', + categories: ['university'], + example: '/sicau/jiaowu/jxtz', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['jiaowu.sicau.edu.cn/web/web/web/index.asp'], + target: '/jiaowu/jxtz', + }, + ], + name: '教务处', + maintainers: ['hualiong'], + url: 'jiaowu.sicau.edu.cn/', + handler: async () => { + const baseUrl = 'https://jiaowu.sicau.edu.cn/web/web/web'; + + const response = await $get(`${baseUrl}/index.asp`); + const $ = load(response); + + const list = $('ul.notice1:nth-child(1) a') + .toArray() + .map((item) => { + const a = $(item); + const href = a.attr('href')!; + return { + link: `${baseUrl}/${href.substring(href.lastIndexOf('/') + 1)}`, + } as DataItem; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link!, async () => { + const response = await $get(item.link!); + const $ = load(response); + + item.title = $('body > .page-title-2').text(); + + const date = $('body > p.page-title-3').text(); + item.pubDate = timezone(parseDate(date.match(/(\d{4}(?:-\d{1,2}){2})/)![0], 'YYYY-M-D'), +8); + + const str = $('.text1[valign="bottom"]').text(); + const match = str.match(/起草:(.+?)\[(.+?)]/)!; + item.author = match[1]; + item.category = [match[2]]; + + item.description = $('.text1[width="95%"]').html()!; + + return item; + }) + ) + ); + + return { + title: '教学通知 - 川农教务处', + link: 'https://jiaowu.sicau.edu.cn/web/web/web/index.asp', + language: 'zh-cn', + item: items as DataItem[], + }; + }, +}; diff --git a/lib/routes/sicau/jk.ts b/lib/routes/sicau/jk.ts new file mode 100644 index 00000000000000..21262bb1276705 --- /dev/null +++ b/lib/routes/sicau/jk.ts @@ -0,0 +1,210 @@ +import { DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +export const route: Route = { + path: '/jk/:gid/:typeId/:sortType/:token', + categories: ['university'], + example: '/sicau/jk/0/0/2/8d95466cf63e537292b303cb92b5958c', + parameters: { + gid: '活动所属组织ID,见下表', + typeId: '活动类别ID,见下表', + sortType: '排序方式,见下表', + token: '访问令牌,可通过示例中的令牌直接访问(会过期)', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '二课活动', + maintainers: ['hualiong'], + url: 'jk.sicau.edu.cn', + description: ` + +::: tip +**本校学生**可以直接 POST \`https://jk.sicau.edu.cn/user/login/v1.0.0/snoLogin\` 从返回结果中的 \`token\` 字段拿到个人令牌,记得在url后添加以下**查询参数**: + +- sid: \`f1c97a0e81c24e98adb1ebdadca0699b\` +- loginName: \`你的学号\` +- password: \`你的i川农密码\` + +::: + +::: warning +由于i川农后台有请求限制,为避免一次性大量请求而被限流,每次只请求结果的第一页数据,即前20条 +::: + +**活动所属组织ID:** + +| ID | 组织 | ID | 组织 | ID | 组织 | ID | 组织 | +| ---- | -------------------- | ---- | ------------------ | ---- | ---------------------- | ---- | ---------------------- | +| 0 | 全部组织 | 14 | 校纪委 | 28 | 食品学院 | 42 | 经济学院 | +| 1 | 管理学院 | 15 | 生命科学学院 | 29 | 环境学院 | 43 | 机电学院 | +| 2 | 学生心理健康服务中心 | 16 | 水利水电学院 | 30 | 国家重点实验室 | 44 | 都江堰校区综合办后管科 | +| 3 | 档案馆 | 17 | 国合处 | 31 | 党委统战部 | 45 | 园艺学院 | +| 4 | 马克思主义学院 | 18 | 商旅学院 | 32 | 草业科技学院 | 46 | 资源学院 | +| 5 | 都江堰校区党政办 | 19 | 风景园林学院 | 33 | 商学院 | 47 | 学生处 | +| 6 | 土木工程学院 | 20 | 建筑与城乡规划学院 | 34 | 党委组织部 | 48 | 农学院 | +| 7 | 林学院 | 21 | 体育学院 | 35 | 校团委 | 49 | 公共管理学院 | +| 8 | 动物医学院 | 22 | 校体委 | 36 | 法学院 | 50 | 图书馆 | +| 9 | 保卫处 | 23 | 校区团委 | 37 | 水稻研究所 | 51 | 校学生会 | +| 10 | 理学院 | 24 | 后勤管理处 | 38 | 研究生院 | 52 | 动物科技学院 | +| 11 | 艺术与传媒学院 | 25 | 教务处 | 39 | 后勤服务总公司 | 53 | 信息工程学院 | +| 12 | 大学生艺术团 | 26 | 人文学院 | 40 | 招生就业处 | | | +| 13 | 都江堰校区基础教学部 | 27 | 党委宣传部 | 41 | 学生社团管理与服务中心 | | | + +**活动类别ID:** + +| ID | 组织 | ID | 组织 | ID | 组织 | +| ---- | ---------------- | ---- | -------------------- | ---- | -------------- | +| 0 | 所有类别 | 5 | 校本文化(校规校纪) | 10 | 体质测试 | +| 1 | 党团学习 | 6 | 德育—社会实践 | 11 | 文化艺术活动 | +| 2 | 学生干部社会工作 | 7 | 创新创业类 | 12 | 文艺演出或讲座 | +| 3 | 校院班任务 | 8 | 科技学术讲座 | 13 | 劳动教育 | +| 4 | 德育(志愿公益) | 9 | 体育活动(新) | | | + +**排序方式:** + +| 即将开始 | 最新活动 | 可参与 | +| ------- | -------- | -------- | +| 1 | 2 | 4 | +`, + handler: async (ctx) => { + const { gid, typeId, sortType, token } = ctx.req.param(); + const $post = ofetch.create({ + baseURL: 'https://jk.sicau.edu.cn/act/actInfo/v1.0.0', + headers: { 'x-access-token': token }, + method: 'post', + }); + const query = async (page: number) => + await $post(`/getUserSchoolActList`, { + query: { + gid: gidDict[gid], + typeId: typeDict[typeId], + sortType, + page, + }, + }); + + const res = await Promise.all([query(1), query(2)]); + + for (const each of res) { + if (each.code !== '0') { + throw new Error(each.message); + } + } + + const list: DataItem[] = [...res[0].content, ...res[1].content] + .filter((e) => e.statusName !== '待发学时') + .map((each) => ({ + id: each.id, + guid: each.id, + title: each.title, + image: each.logo, + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(String(item.id), async () => { + const { code, message, content } = await $post(`/getActDetail?actId=${item.id}`); + if (code === '0') { + item.author = content.groupName; + item.pubDate = timezone(parseDate(content.startDate, 'YYYY-MM-DD HH:mm:ss'), +8); + item.category = [content.typeName, content.levelName]; + item.description = `<img src="${item.image}" alt="${item.title}" /><p style='white-space: pre-wrap'>${content.description}</p>`; + + return item; + } + throw new Error(message); + }) + ) + ); + + return { + title: '二课活动 - 四川农业大学', + link: 'https://jk.sicau.edu.cn/act/actInfo/v1.0.0/getUserSchoolActList', + language: 'zh-cn', + item: items as DataItem[], + }; + }, +}; + +const typeDict = { + '0': '', + '1': '17a3b11f2d254518b13406ccd18a85b5', + '2': '000392a845ff47d09978c6ddd6dda2d4', + '3': '9ead58d01d3d424ea70b194910893660', + '4': 'e233038099914313950ad9058e4c7176', + '5': 'ccdec1bf8a32497998a4d2b3285e8fa0', + '6': '1db34972f7b247a6a60aac15285870d5', + '7': '3a230729aac44b3aa0127c8cdcf15555', + '8': '0157365febe548ee86848dd58c7b8b4b', + '9': 'fe94059a28e5440fb1097679b4744981', + '10': '5b91902f42854301ae60d30018f8786c', + '11': '1cee1aea9994489286fe2a7d42b6e21e', + '12': '4f7a37aa8ca2452dbf5f44fc7cfa2679', + '13': '7969c78baee34a6f90e029d95db18592', +}; + +const gidDict = { + '0': '', + '1': '075c882582bf4392b5f858e18169c6fa', + '2': '0b6869479c1d4e69b01afa73534ddf7e', + '3': '0b9b91adfcc2494190a06b6ceb33136c', + '4': '0d0ad5dca6f24f3290c5dc62c9e534bf', + '5': '0e0d4803c89a419fa2c87ea415cf0efc', + '6': '11944f4920c645e3936a429f4da48165', + '7': '13968d883e2146febd41fea97b8e935c', + '8': '16e9229a200e4644ac9d283f44dfbd8c', + '9': '1838c6d0dd394ffebe5305a8efaabd24', + '10': '1bb0a81892a74ca792dbf726521284bc', + '11': '1cad939489de4868bcbba1366dc454e8', + '12': '22ddaea411e44df68bdb614d2e97cea1', + '13': '23fd994f03ca4174b364b371a67dcacd', + '14': '2a1fb8567c0e4c1b93d82259b8f784f6', + '15': '2a5f3f75505140cc897c2187b3dcf91a', + '16': '5016d53f98164d9c81484acbbc6b761d', + '17': '56e1f5bea3ab4921b2e3bb37bfbdf629', + '18': '597d5baff6ab469992d4ecb54f7b4c38', + '19': '607a4eefcbd549018c2e253c60b227de', + '20': '61e0696b35da46a69cbd57e2a415e919', + '21': '67c4d8cdaf04456eae344f01e67e0ca0', + '22': '70f29b197d8945be8c6f9714a1923057', + '23': '78d0b179286845e9b589cb27c5b2b3b4', + '24': '7cf23874959a4827b8c15c9f7b99eb64', + '25': '82d71d8ada514858ab30e5f58e64706e', + '26': '89c9be371ab8498499f0b3aff520f4c4', + '27': '8b459d361d4b493b918656710104a4a7', + '28': '8cd045b1390146fd8d5ad155db8b59e1', + '29': '8f231145f3664726beb551378e1f5d99', + '30': '97062a27a31e40a7a5be49475e3df099', + '31': 'ae9e85188f084cb3be8a0fa3ebf6c7b5', + '32': 'b88245cc3c9c47f8ab121ad5c2fa3282', + '33': 'ba368a3503274da781c1960ba084793f', + '34': 'be4149fac2394326b19270e3b70fd704', + '35': 'be68e601768b4e57830bbceb829a2942', + '36': 'c514507d08d3415e965abf84d9dfd31b', + '37': 'c7d5a7b282854f14aafb22ed69abef7a', + '38': 'ca1423660dd940439af1921a5c48e521', + '39': 'd09f1391b0de4667aaf8ed6313582667', + '40': 'd393b583269e420f93cf7cf07ef7b694', + '41': 'd79d6ef35687400da1c3106080ad294a', + '42': 'd850db1a4811420f934ef7c783ba72b1', + '43': 'd8f1ecac920346b79650fbd2783c9a86', + '44': 'dba67112679e4df2892a8896ac2cb898', + '45': 'dd7345a743c6464fafdded750c08a4d8', + '46': 'dfa126fc8e494259bf3d88d61afca53e', + '47': 'e1f80975b5e940f3a3e416ac45e79ce8', + '48': 'ea117c42071a40b5b08423b392bc5722', + '49': 'f4017020fe9a456599754c88c1d9a341', + '50': 'f548b78687094c428085dfb0b064ed32', + '51': 'fa5098c59a9c4db6b287744049053762', + '52': 'fccf2f15e15a479ba9b5564efee436c7', + '53': 'fcf30bbdd8004026ae9b447f2722aecf', +}; diff --git a/lib/routes/sicau/namespace.ts b/lib/routes/sicau/namespace.ts index ad63336b5caf51..df3561850f824d 100644 --- a/lib/routes/sicau/namespace.ts +++ b/lib/routes/sicau/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '四川农业大学', - url: 'dky.sicau.edu.cn', + url: 'www.sicau.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sicau/yan.ts b/lib/routes/sicau/yan.ts index 4b9f75a04e598f..07fd45b48e63d7 100644 --- a/lib/routes/sicau/yan.ts +++ b/lib/routes/sicau/yan.ts @@ -28,8 +28,8 @@ export const route: Route = { handler, url: 'yan.sicau.edu.cn/', description: `| 新闻公告 | 学术报告 | - | -------- | -------- | - | xwgg | xsbg |`, +| -------- | -------- | +| xwgg | xsbg |`, }; async function handler(ctx) { diff --git a/lib/routes/sicau/zsjy.ts b/lib/routes/sicau/zsjy.ts index 131cf6d4c9602d..1e9db002c835dc 100644 --- a/lib/routes/sicau/zsjy.ts +++ b/lib/routes/sicau/zsjy.ts @@ -27,8 +27,8 @@ export const route: Route = { handler, url: 'dky.sicau.edu.cn/', description: `| 本科生招生 | 研究生招生 | 毕业生选录指南 | - | ---------- | ---------- | -------------- | - | bkszs | yjszs | bysxlzn |`, +| ---------- | ---------- | -------------- | +| bkszs | yjszs | bysxlzn |`, }; async function handler(ctx) { diff --git a/lib/routes/sigsac/namespace.ts b/lib/routes/sigsac/namespace.ts index 3adbea9e4d8f9a..679607883b20d8 100644 --- a/lib/routes/sigsac/namespace.ts +++ b/lib/routes/sigsac/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ACM Special Interest Group on Security Audit and Control', url: 'sigsac.org', + lang: 'en', }; diff --git a/lib/routes/simpleinfo/index.ts b/lib/routes/simpleinfo/index.ts index 265ad771fefc4e..f091999e41df2e 100644 --- a/lib/routes/simpleinfo/index.ts +++ b/lib/routes/simpleinfo/index.ts @@ -12,7 +12,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/:category?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/simpleinfo', parameters: { category: '分类名' }, features: { @@ -33,16 +33,16 @@ export const route: Route = { maintainers: ['haukeng'], handler, description: `| 夥伴聊聊 | 專案設計 | - | -------- | -------- | - | work | talk | +| -------- | -------- | +| work | talk | - | 國內外新聞 | 政治百分百 | 社會觀察家 | 心理與哲學 | - | ---------- | ---------- | ---------- | --------------------- | - | news | politics | society | psychology-philosophy | +| 國內外新聞 | 政治百分百 | 社會觀察家 | 心理與哲學 | +| ---------- | ---------- | ---------- | --------------------- | +| news | politics | society | psychology-philosophy | - | 科學大探索 | 環境與健康 | ACG 快樂聊 | 好書籍分享 | 其它主題 | - | ---------- | ------------------ | ---------- | ------------ | ------------ | - | science | environment-health | acg | book-sharing | other-topics |`, +| 科學大探索 | 環境與健康 | ACG 快樂聊 | 好書籍分享 | 其它主題 | +| ---------- | ------------------ | ---------- | ------------ | ------------ | +| science | environment-health | acg | book-sharing | other-topics |`, }; async function handler(ctx) { diff --git a/lib/routes/simpleinfo/namespace.ts b/lib/routes/simpleinfo/namespace.ts index aebbcb29a9919a..3a4ebae54134ee 100644 --- a/lib/routes/simpleinfo/namespace.ts +++ b/lib/routes/simpleinfo/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '簡訊設計', url: 'blog.simpleinfo.cc', + lang: 'zh-TW', }; diff --git a/lib/routes/sina/discovery.ts b/lib/routes/sina/discovery.ts index 6af21973ca41ed..aa63a2a98941c0 100644 --- a/lib/routes/sina/discovery.ts +++ b/lib/routes/sina/discovery.ts @@ -31,8 +31,8 @@ export const route: Route = { maintainers: ['LogicJake'], handler, description: `| 最新 | 天文航空 | 动物植物 | 自然地理 | 历史考古 | 生命医学 | 生活百科 | 科技前沿 | - | ---- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | zx | twhk | dwzw | zrdl | lskg | smyx | shbk | kjqy |`, +| ---- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| zx | twhk | dwzw | zrdl | lskg | smyx | shbk | kjqy |`, }; async function handler(ctx) { diff --git a/lib/routes/sina/finance/china.ts b/lib/routes/sina/finance/china.ts index 975fb13793b124..6b88d0433df480 100644 --- a/lib/routes/sina/finance/china.ts +++ b/lib/routes/sina/finance/china.ts @@ -26,8 +26,8 @@ export const route: Route = { handler, url: 'finance.sina.com.cn/china', description: `| 国内滚动 | 宏观经济 | 金融新闻 | 地方经济 | 部委动态 | 今日财经 TOP10 | - | -------- | -------- | -------- | -------- | -------- | -------------- | - | 1686 | 1687 | 1690 | 1688 | 1689 | 3231 |`, +| -------- | -------- | -------- | -------- | -------- | -------------- | +| 1686 | 1687 | 1690 | 1688 | 1689 | 3231 |`, }; async function handler(ctx) { diff --git a/lib/routes/sina/finance/stock/usstock.ts b/lib/routes/sina/finance/stock/usstock.ts index ddeed513b00b5a..251b107ad23f70 100644 --- a/lib/routes/sina/finance/stock/usstock.ts +++ b/lib/routes/sina/finance/stock/usstock.ts @@ -28,8 +28,8 @@ export const route: Route = { handler, url: 'finance.sina.com.cn/stock/usstock', description: `| 最新报道 | 中概股 | 国际财经 | 互联网 | - | -------- | ------ | -------- | ------ | - | 57045 | 57046 | 56409 | 40811 |`, +| -------- | ------ | -------- | ------ | +| 57045 | 57046 | 56409 | 40811 |`, }; async function handler(ctx) { diff --git a/lib/routes/sina/namespace.ts b/lib/routes/sina/namespace.ts index 963869786e1c06..7871d29039ce40 100644 --- a/lib/routes/sina/namespace.ts +++ b/lib/routes/sina/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '新浪', url: 'finance.sina.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sina/rollnews.ts b/lib/routes/sina/rollnews.ts index c0a89efb2ea1eb..e6af3fa09c59fc 100644 --- a/lib/routes/sina/rollnews.ts +++ b/lib/routes/sina/rollnews.ts @@ -19,8 +19,8 @@ export const route: Route = { maintainers: ['xyqfer'], handler, description: `| 全部 | 国内 | 国际 | 社会 | 体育 | 娱乐 | 军事 | 科技 | 财经 | 股市 | 美股 | - | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | - | 2509 | 2510 | 2511 | 2669 | 2512 | 2513 | 2514 | 2515 | 2516 | 2517 | 2518 |`, +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| 2509 | 2510 | 2511 | 2669 | 2512 | 2513 | 2514 | 2515 | 2516 | 2517 | 2518 |`, }; async function handler(ctx) { diff --git a/lib/routes/sina/sports.ts b/lib/routes/sina/sports.ts index 637c8418b71cf5..d8915d40050b54 100644 --- a/lib/routes/sina/sports.ts +++ b/lib/routes/sina/sports.ts @@ -6,7 +6,10 @@ import { parseArticle } from './utils'; export const route: Route = { path: '/sports/:type?', - name: 'Unknown', + name: '新浪体育', + categories: ['new-media'], + example: '/sports', + parameters: { type: '类别' }, maintainers: ['nczitzk'], handler, }; diff --git a/lib/routes/sinchew/namespace.ts b/lib/routes/sinchew/namespace.ts index f68110e7e5413a..c95824834a25d9 100644 --- a/lib/routes/sinchew/namespace.ts +++ b/lib/routes/sinchew/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '星洲网', url: 'sinchew.com.my', + lang: 'zh-CN', }; diff --git a/lib/routes/sis001/author.ts b/lib/routes/sis001/author.ts new file mode 100644 index 00000000000000..eca6cf227707b5 --- /dev/null +++ b/lib/routes/sis001/author.ts @@ -0,0 +1,55 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { config } from '@/config'; +import { load } from 'cheerio'; +import type { Context } from 'hono'; +import { getCookie, getThread } from './common'; + +export const route: Route = { + path: '/author/:id?', + categories: ['bbs'], + example: '/sis001/author/13131575', + parameters: { id: '作者 ID,可以在作者的个人空间地址找到' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '作者', + maintainers: ['keocheung'], + handler, +}; + +async function handler(ctx: Context) { + const { id = '13131575' } = ctx.req.param(); + const url = `${config.sis001.baseUrl}/forum/space.php?uid=${id}`; + + const cookie = await getCookie(url); + const response = await got(url, { headers: { cookie } }); + const $ = load(response.data); + + const username = $('div.bg div.title').text().replace('的个人空间', ''); + + let items = $('div.center_subject ul li a[href^=thread]') + .toArray() + .map((item) => { + item = $(item); + return { + title: item.text(), + link: `${config.sis001.baseUrl}/forum/${item.attr('href')}`, + author: username, + }; + }); + + items = await Promise.all(items.map((item) => cache.tryGet(item.link, async () => await getThread(cookie, item)))); + + return { + title: `${username}的主题`, + link: url, + item: items, + }; +} diff --git a/lib/routes/sis001/common.ts b/lib/routes/sis001/common.ts new file mode 100644 index 00000000000000..92ee06aea8b23a --- /dev/null +++ b/lib/routes/sis001/common.ts @@ -0,0 +1,73 @@ +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import { DataItem } from '@/types'; +import CryptoJS from 'crypto-js'; +import cache from '@/utils/cache'; +import { config } from '@/config'; + +function getCookie(url: string): Promise<string> { + return cache.tryGet( + 'sis001:cookie', + async () => { + const response = await got(url); + const rsp = response.data; + + const regex = /toNumbers\("([a-fA-F0-9]+)"\)/g; + const matches: string[] = []; + let match: RegExpExecArray | null; + + while ((match = regex.exec(rsp)) !== null) { + matches.push(match[1]); + } + + if (matches.length !== 3) { + return ''; + } + + const key = CryptoJS.enc.Hex.parse(matches[0]); + const iv = CryptoJS.enc.Hex.parse(matches[1]); + const encrypted = CryptoJS.enc.Hex.parse(matches[2]); + + const decrypted = CryptoJS.AES.decrypt({ ciphertext: encrypted }, key, { iv, padding: CryptoJS.pad.NoPadding }); + + return 'CeRaHigh1=' + decrypted.toString(CryptoJS.enc.Hex); + }, + config.cache.routeExpire, + false + ); +} + +async function getThread(cookie: string, item: DataItem) { + const response = await got(item.link, { headers: { cookie } }); + const $ = load(response.data); + + item.guid = item.link?.replace(/^https?:\/\/.+?\//, 'https://www.sis001.com/'); + item.category = $('.posttags a') + .toArray() + .map((a) => $(a).text()); + item.pubDate = timezone( + parseDate( + $('.postinfo') + .eq(0) + .text() + .match(/发表于 (.*)\s*只看该作者/)[1], + 'YYYY-M-D HH:mm' + ), + 8 + ); + $('div[id^=postmessage_] table, fieldset, .posttags, strong font, span:empty').remove(); + item.description = + $('div[id^=postmessage_]') + .eq(0) + .html() + ?.replaceAll('\n', '') + .replaceAll(/\u3000{2}.+?(((?:<br>){2})|( ))/g, (str) => `<p>${str.replaceAll('<br>', '')}</p>`) + .replaceAll(/<p>\u3000{6,}(.+?)<\/p>/g, '<center><p style="text-align:center;">$1</p></center>') + .replaceAll(' ', '') + .replace(/<br><br> +<br><br>/, '') + ($('.defaultpost .postattachlist').html() ?? ''); + return item; +} + +export { getCookie, getThread }; diff --git a/lib/routes/sis001/forum.ts b/lib/routes/sis001/forum.ts index 784db054eddeb5..5775f3adb0b26b 100644 --- a/lib/routes/sis001/forum.ts +++ b/lib/routes/sis001/forum.ts @@ -1,10 +1,10 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; +import { config } from '@/config'; import { load } from 'cheerio'; -import { parseDate } from '@/utils/parse-date'; -import timezone from '@/utils/timezone'; -const baseUrl = 'https://www.sis001.com'; +import type { Context } from 'hono'; +import { getCookie, getThread } from './common'; export const route: Route = { path: '/forum/:id?', @@ -20,16 +20,16 @@ export const route: Route = { supportScihub: false, }, name: '子版块', - maintainers: [], + maintainers: ['TonyRL'], handler, }; -async function handler(ctx) { +async function handler(ctx: Context) { const { id = 76 } = ctx.req.param(); - const url = `${baseUrl}/forum/forum-${id}-1.html`; - - const response = await got(url); + const url = `${config.sis001.baseUrl}/forum/forum-${id}-1.html`; + const cookie = await getCookie(url); + const response = await got(url, { headers: { cookie } }); const $ = load(response.data); let items = $('form table') @@ -41,37 +41,12 @@ async function handler(ctx) { item = $(item); return { title: item.find('th em').text() + ' ' + item.find('span a').eq(0).text(), - link: new URL(item.find('span a').eq(0).attr('href'), `${baseUrl}/forum/`).href, + link: new URL(item.find('span a').eq(0).attr('href'), `${config.sis001.baseUrl}/forum/`).href, author: item.find('.author a').text(), - pubDate: parseDate(item.find('.author em').text(), 'YYYY-M-D'), }; }); - items = await Promise.all( - items.map((item) => - cache.tryGet(item.link, async () => { - const response = await got(item.link); - const $ = load(response.data); - - item.category = $('.posttags a') - .toArray() - .map((a) => $(a).text()); - item.pubDate = timezone( - parseDate( - $('.postinfo') - .eq(0) - .text() - .match(/发表于 (.*)\s*只看该作者/)[1], - 'YYYY-M-D HH:mm' - ), - 8 - ); - $('div[id^=postmessage_] table, fieldset, .posttags').remove(); - item.description = $('div[id^=postmessage_]').eq(0).html() + ($('.defaultpost .postattachlist').html() ?? ''); - return item; - }) - ) - ); + items = await Promise.all(items.map((item) => cache.tryGet(item.link, async () => await getThread(cookie, item)))); return { title: $('head title').text(), diff --git a/lib/routes/sis001/namespace.ts b/lib/routes/sis001/namespace.ts index 42ca5d651b0b04..fccfaef5776b54 100644 --- a/lib/routes/sis001/namespace.ts +++ b/lib/routes/sis001/namespace.ts @@ -3,4 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '第一会所', url: 'sis001.com', + description: `::: tip + 第一会所有多个备用网址,本路由默认使用\`https://sis001.com\`,若该网址无法访问,可以在部署实例的时候通过\`SIS001_BASE_URL\`环境变量配置要使用的地址,如\`https://www.sis001.com\`等 +:::`, + lang: 'zh-CN', }; diff --git a/lib/routes/sjtu/gs.ts b/lib/routes/sjtu/gs.ts index bf6c9390375e0b..4030d255209651 100644 --- a/lib/routes/sjtu/gs.ts +++ b/lib/routes/sjtu/gs.ts @@ -29,20 +29,20 @@ export const route: Route = { maintainers: ['dzx-dzx'], handler, description: `| 工作信息 | 招生信息 | 培养信息 | 学位学科 | 国际交流 | 创新工程 | - | -------- | -------- | -------- | -------- | -------- | -------- | - | work | enroll | train | degree | exchange | xsjy | +| -------- | -------- | -------- | -------- | -------- | -------- | +| work | enroll | train | degree | exchange | xsjy | 当\`type\`为\`enroll\`, \`num\`可选字段: - | 58 | 59 | 60 | 61 | 62 | - | -------- | -------- | ---------- | -------- | -------- | - | 博士招生 | 硕士招生 | 港澳台招生 | 考点信息 | 院系动态 | +| 58 | 59 | 60 | 61 | 62 | +| -------- | -------- | ---------- | -------- | -------- | +| 博士招生 | 硕士招生 | 港澳台招生 | 考点信息 | 院系动态 | 当\`type\`为\`exchange\`, \`num\`可选字段: - | 67 | 68 | 69 | 70 | 71 | - | -------------- | -------------- | -------------- | -------------- | -------------- | - | 国家公派研究生 | 国际化培养资助 | 校际交换与联培 | 交流与合作项目 | 项目招募与宣讲 |`, +| 67 | 68 | 69 | 70 | 71 | +| -------------- | -------------- | -------------- | -------------- | -------------- | +| 国家公派研究生 | 国际化培养资助 | 校际交换与联培 | 交流与合作项目 | 项目招募与宣讲 |`, }; async function handler(ctx) { diff --git a/lib/routes/sjtu/jwc.ts b/lib/routes/sjtu/jwc.ts index 0b726132a88769..b320980eb2d1bd 100644 --- a/lib/routes/sjtu/jwc.ts +++ b/lib/routes/sjtu/jwc.ts @@ -4,10 +4,13 @@ import got from '@/utils/got'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; -const urlRoot = 'https://jwc.sjtu.edu.cn/xwtg'; +const urlRoot = 'https://jwc.sjtu.edu.cn'; async function getFullArticle(link) { - const response = await got(link); + const response = await got(link).catch(() => null); + if (!response) { + return null; + } const $ = load(response.body); const content = $('.content-con'); if (content.length === 0) { @@ -43,9 +46,9 @@ export const route: Route = { name: '教务处通知公告', maintainers: ['SeanChao'], handler, - description: `| 新闻中心 | 通知通告 | 教学运行 | 注册学务 | 研究办 | 教改办 | 综合办 | 语言文字 | 工会与支部 | 通识教育 | - | -------- | -------- | --------- | -------- | ------ | ------ | ------ | -------- | ---------- | -------- | - | news | notice | operation | affairs | yjb | jgb | zhb | language | party | ge |`, + description: `| 新闻中心 | 通知通告 | 教学运行 | 注册学务 | 研究办 | 教改办 | 综合办 | 语言文字 | 工会与支部 | 通识教育 | 面向学生的通知 | +| -------- | -------- | --------- | -------- | ------ | ------ | ------ | -------- | ---------- | -------- | +| news | notice | operation | affairs | yjb | jgb | zhb | language | party | ge | students |`, }; async function handler(ctx) { @@ -53,48 +56,52 @@ async function handler(ctx) { const config = { all: { section: '通知通告', - link: '/tztg.htm', + link: '/xwtg/tztg.htm', }, news: { - link: '/xwzx.htm', + link: '/xwtg/xwzx.htm', section: '新闻中心', }, notice: { - link: '/tztg.htm', + link: '/xwtg/tztg.htm', section: '通知通告', }, operation: { - link: '/jxyx.htm', + link: '/xwtg/jxyx.htm', section: '教学运行', }, affairs: { - link: '/zcxw.htm', + link: '/xwtg/zcxw.htm', section: '注册学务', }, yjb: { - link: '/yjb.htm', + link: '/xwtg/yjb.htm', section: '研究办', }, jgb: { - link: '/jgb.htm', + link: '/xwtg/jgb.htm', section: '教改办', }, zhb: { - link: '/zhb.htm', + link: '/xwtg/zhb.htm', section: '综合办', }, language: { - link: '/yywz.htm', + link: '/xwtg/yywz.htm', section: '语言文字', }, party: { - link: '/ghyzb.htm', + link: '/xwtg/ghyzb.htm', section: '工会与支部', }, ge: { - link: '/tsjy.htm', + link: '/xwtg/tsjy.htm', section: '通识教育', }, + students: { + link: '/index/mxxsdtz.htm', + section: '面向学生的通知', + }, }; const sectionLink = urlRoot + config[type].link; diff --git a/lib/routes/sjtu/namespace.ts b/lib/routes/sjtu/namespace.ts index eaa03d1557e3fa..31b6974f8f556c 100644 --- a/lib/routes/sjtu/namespace.ts +++ b/lib/routes/sjtu/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海交通大学', - url: 'bjwb.seiee.sjtu.edu.cn', + url: 'www.sjtu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sjtu/seiee/index.ts b/lib/routes/sjtu/seiee/index.ts new file mode 100644 index 00000000000000..d7ec9548dcceb2 --- /dev/null +++ b/lib/routes/sjtu/seiee/index.ts @@ -0,0 +1,97 @@ +import { Route } 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: '/seiee/:path/:catID?/:searchCatCode?', + categories: ['university'], + example: '/sjtu/seiee/xzzx_notice_bks', + parameters: { path: "不含'.html'的最后一部分路径", catID: "'本科生人才培养'与'研究生人才培养'的类别ID", searchCatCode: "'本科生人才培养'与'研究生人才培养'下类别名" }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.seiee.sjtu.edu.cn/:path.html'], + target: '/seiee/:path', + }, + ], + name: '电子信息与电气工程学院', + maintainers: ['dzx-dzx'], + handler, +}; + +async function handler(ctx) { + const { path, catID = '', searchCatCode = '' } = ctx.req.param(); + + const rootUrl = 'https://www.seiee.sjtu.edu.cn'; + const currentUrl = `${rootUrl}/${path}.html`; + const ajaxUrl = `${rootUrl}/active/ajax_article_list.html`; + const response = catID + ? ( + await ofetch(ajaxUrl, { + method: 'POST', + body: new URLSearchParams({ + page: '1', + cat_id: catID, + search_cat_code: searchCatCode, + search_cat_title: '', + template: 'v_ajax_normal_list1', + }), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + parseResponse: JSON.parse, + }) + ).content + : await ofetch(currentUrl); + + const $ = load(response); + + const list = $(catID ? 'li' : '.u10 li') + .toArray() + .map((item) => { + item = $(item); + + return { + title: item.find('.name').text().trim(), + link: item.find('a').attr('href'), + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await ofetch(item.link); + const content = load(detailResponse); + + item.description = content('.nr').html(); + item.pubDate = timezone( + parseDate( + content('.jj') + .text() + .trim() + .match(/日期:([\d-]+) /)[1] + ), + +8 + ); + + return item; + }) + ) + ); + + return { + title: $('title').text() || load(await ofetch(currentUrl))('title').text(), + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/sjtu/tongqu/activity.ts b/lib/routes/sjtu/tongqu/activity.ts index e730610683ccab..890ffbac93d84a 100644 --- a/lib/routes/sjtu/tongqu/activity.ts +++ b/lib/routes/sjtu/tongqu/activity.ts @@ -25,8 +25,8 @@ export const route: Route = { maintainers: ['SeanChao'], handler, description: `| 全部 | 最新 | 招新 | 讲座 | 户外 | 招聘 | 游学 | 比赛 | 公益 | 主题党日 | 学生事务 | 广告 | 其他 | - | ---- | ------ | ----------- | ------- | --------- | ---- | ---------- | ------------ | -------------- | -------- | -------------- | ---- | ------ | - | all | newest | recruitment | lecture | outdoords | jobs | studyTours | competitions | publicWarefare | partyDay | studentAffairs | ads | others |`, +| ---- | ------ | ----------- | ------- | --------- | ---- | ---------- | ------------ | -------------- | -------- | -------------- | ---- | ------ | +| all | newest | recruitment | lecture | outdoords | jobs | studyTours | competitions | publicWarefare | partyDay | studentAffairs | ads | others |`, }; async function handler(ctx) { diff --git a/lib/routes/sjtu/yzb/zkxx.ts b/lib/routes/sjtu/yzb/zkxx.ts index 69ee5f8f943e30..ab2ce285e1d455 100644 --- a/lib/routes/sjtu/yzb/zkxx.ts +++ b/lib/routes/sjtu/yzb/zkxx.ts @@ -1,7 +1,9 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import cache from '@/utils/cache'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; +import { fetchArticle } from '@/utils/wechat-mp'; +import ofetch from '@/utils/ofetch'; const baseTitle = '上海交通大学研究生招生网招考信息'; const baseUrl = 'https://yzb.sjtu.edu.cn/index/zkxx/'; @@ -23,32 +25,50 @@ export const route: Route = { maintainers: ['stdrc'], handler, description: `| 博士招生 | 硕士招生 | 港澳台招生 | 考点信息 | 院系动态 | - | -------- | -------- | ---------- | -------- | -------- | - | bszs | sszs | gatzs | kdxx | yxdt |`, +| -------- | -------- | ---------- | -------- | -------- | +| bszs | sszs | gatzs | kdxx | yxdt |`, }; async function handler(ctx) { const pageUrl = `${baseUrl}${ctx.req.param('type')}.htm`; - const response = await got({ - method: 'get', - url: pageUrl, - headers: { - Referer: pageUrl, - }, - }); + const response = await ofetch(pageUrl); - const $ = load(response.data); + const $ = load(response); + + const list = $('li[id^="line"] a') + .toArray() + .map((elem) => ({ + link: new URL(elem.attribs.href, pageUrl).href, + title: $(elem).text(), + pubDate: parseDate($(elem.next?.next).text().trim()), + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + if (new URL(item.link).hostname === 'mp.weixin.qq.com') { + return await fetchArticle(item.link); + } else if (new URL(item.link).hostname === 'www.shmeea.edu.cn') { + const detailResponse = await ofetch(item.link.replace('http://', 'https://')); + const content = load(detailResponse); + item.description = content('.Article_content').html(); + return item; + } else if (new URL(item.link).hostname === 'yzb.sjtu.edu.cn') { + const detailResponse = await ofetch(item.link); + const content = load(detailResponse); + item.description = content('[id^=vsb_content]').html(); + return item; + } else { + return item; + } + }) + ) + ); return { link: pageUrl, title: `${baseTitle} -- ${$('title').text().split('-')[0]}`, - item: $('li[id^="line"] a') - .toArray() - .map((elem) => ({ - link: new URL(elem.attribs.href, pageUrl).href, - title: $(elem).text(), - pubDate: parseDate($(elem.next?.next).text().trim()), - })), + item: items, }; } diff --git a/lib/routes/skeb/following-creators.ts b/lib/routes/skeb/following-creators.ts new file mode 100644 index 00000000000000..3c149f2868cbd9 --- /dev/null +++ b/lib/routes/skeb/following-creators.ts @@ -0,0 +1,52 @@ +import { Data, DataItem, Route } from '@/types'; +import { config } from '@/config'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import { getFollowingsItems } from './utils'; + +export const route: Route = { + path: '/following_creators/:username', + categories: ['picture'], + example: '/skeb/following_creators/@brm2_1925', + parameters: { username: 'Skeb Username with @' }, + features: { + requireConfig: [ + { + name: 'SKEB_BEARER_TOKEN', + optional: false, + description: '在瀏覽器開發者工具(F12)的主控台中輸入 `localStorage.getItem("token")` 獲取', + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Following Creators', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'Following Creators', + source: ['skeb.jp/:username'], + target: '/following_creators/:username', + }, + ], + description: 'Get the list of creators the specified user is following on Skeb.', +}; + +async function handler(ctx): Promise<Data> { + const username = ctx.req.param('username'); + + if (!config.skeb || !config.skeb.bearerToken) { + throw new ConfigNotFoundError('Skeb followings RSS is disabled due to the lack of relevant config'); + } + + const items = await getFollowingsItems(username, 'following_creators'); + + return { + title: `Skeb - ${username} - フォロー中のクリエイター`, + link: `https://skeb.jp/${username}`, + item: items as DataItem[], + }; +} diff --git a/lib/routes/skeb/following-works.ts b/lib/routes/skeb/following-works.ts new file mode 100644 index 00000000000000..0b3bf0cd1e1d8b --- /dev/null +++ b/lib/routes/skeb/following-works.ts @@ -0,0 +1,52 @@ +import { Data, DataItem, Route } from '@/types'; +import { config } from '@/config'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import { getFollowingsItems } from './utils'; + +export const route: Route = { + path: '/following_works/:username', + categories: ['picture'], + example: '/skeb/following_works/@brm2_1925', + parameters: { username: 'Skeb Username with @' }, + features: { + requireConfig: [ + { + name: 'SKEB_BEARER_TOKEN', + optional: false, + description: '在瀏覽器開發者工具(F12)的主控台中輸入 `localStorage.getItem("token")` 獲取', + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Following Works', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'Following Works', + source: ['skeb.jp/:username'], + target: '/following_works/:username', + }, + ], + description: "Get the latest works for the specified user's followings on Skeb.", +}; + +async function handler(ctx): Promise<Data> { + const username = ctx.req.param('username'); + + if (!config.skeb || !config.skeb.bearerToken) { + throw new ConfigNotFoundError('Skeb followings RSS is disabled due to the lack of relevant config'); + } + + const items = await getFollowingsItems(username, 'following_works'); + + return { + title: `Skeb - ${username} - フォロー中のクリエイターの新着作品`, + link: `https://skeb.jp/${username}`, + item: items as DataItem[], + }; +} diff --git a/lib/routes/skeb/friend-works.ts b/lib/routes/skeb/friend-works.ts new file mode 100644 index 00000000000000..ed01a49f547551 --- /dev/null +++ b/lib/routes/skeb/friend-works.ts @@ -0,0 +1,52 @@ +import { Data, DataItem, Route } from '@/types'; +import { config } from '@/config'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import { getFollowingsItems } from './utils'; + +export const route: Route = { + path: '/friend_works/:username', + categories: ['picture'], + example: '/skeb/friend_works/@brm2_1925', + parameters: { username: 'Skeb Username with @' }, + features: { + requireConfig: [ + { + name: 'SKEB_BEARER_TOKEN', + optional: false, + description: '在瀏覽器開發者工具(F12)的主控台中輸入 `localStorage.getItem("token")` 獲取', + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Friend Works', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'Friend Works', + source: ['skeb.jp/:username'], + target: '/friend_works/:username', + }, + ], + description: "Get the latest requests for the specified user's followings on Skeb.", +}; + +async function handler(ctx): Promise<Data> { + const username = ctx.req.param('username'); + + if (!config.skeb || !config.skeb.bearerToken) { + throw new ConfigNotFoundError('Skeb followings RSS is disabled due to the lack of relevant config'); + } + + const items = await getFollowingsItems(username, 'friend_works'); + + return { + title: `Skeb - ${username} - フォロー中のクライアントの新着リクエスト`, + link: `https://skeb.jp/${username}`, + item: items as DataItem[], + }; +} diff --git a/lib/routes/skeb/index.ts b/lib/routes/skeb/index.ts new file mode 100644 index 00000000000000..7d3503f0978d18 --- /dev/null +++ b/lib/routes/skeb/index.ts @@ -0,0 +1,131 @@ +import { Route, Data, DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { baseUrl, processWork, processCreator } from './utils'; +import { config } from '@/config'; + +const categoryMap = { + // Works categories + new_art_works: '新着作品 (Illust)', + new_voice_works: '新着作品 (Voice)', + new_novel_works: '新着作品 (Novel)', + new_video_works: '新着作品 (Video)', + new_music_works: '新着作品 (Music)', + new_correction_works: '新着作品 (Advice)', + new_comic_works: '新着作品 (Comic)', + popular_works: '人気の作品 (Popular)', + // Creators categories + popular_creators: '人気クリエイター', + new_creators: '新着クリエイター', +}; + +const workCategories = new Set(['new_art_works', 'new_voice_works', 'new_novel_works', 'new_video_works', 'new_music_works', 'new_correction_works', 'new_comic_works', 'popular_works']); + +export const route: Route = { + path: '/:category', + categories: ['picture'], + example: '/skeb/new_art_works', + parameters: { category: 'Category, the div id of the section title on the homepage.' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Skeb', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: '新着作品 (Illust)', + source: ['skeb.jp'], + target: '/new_art_works', + }, + { + title: '新着作品 (Voice)', + source: ['skeb.jp'], + target: '/new_voice_works', + }, + { + title: '新着作品 (Novel)', + source: ['skeb.jp'], + target: '/new_novel_works', + }, + { + title: '新着作品 (Video)', + source: ['skeb.jp'], + target: '/new_video_works', + }, + { + title: '新着作品 (Music)', + source: ['skeb.jp'], + target: '/new_music_works', + }, + { + title: '新着作品 (Advice)', + source: ['skeb.jp'], + target: '/new_correction_works', + }, + { + title: '新着作品 (Comic)', + source: ['skeb.jp'], + target: '/new_comic_works', + }, + { + title: '人気の作品 (Popular)', + source: ['skeb.jp'], + target: '/popular_works', + }, + { + title: '人気クリエイター', + source: ['skeb.jp'], + target: '/popular_creators', + }, + { + title: '新着クリエイター', + source: ['skeb.jp'], + target: '/new_creators', + }, + ], +}; + +async function handler(ctx): Promise<Data> { + const category = ctx.req.param('category') || 'new_art_works'; + + if (!(category in categoryMap)) { + throw new Error('Invalid category'); + } + + const url = `${baseUrl}/api`; + + const apiData = await cache.tryGet( + url, + async () => { + const data = await ofetch(url); + return data; + }, + config.cache.routeExpire, + false + ); + + if (!apiData || typeof apiData !== 'object') { + throw new Error('Invalid data received from API'); + } + + const items = await cache.tryGet(category, async () => { + if (!(category in apiData) || !Array.isArray(apiData[category])) { + return []; + } + + const processItem = workCategories.has(category) ? processWork : processCreator; + return (await Promise.all(apiData[category].map(async (item) => await processItem(item)).filter(Boolean))) as DataItem[]; + }); + + return { + title: `Skeb - ${categoryMap[category]}`, + link: `${baseUrl}/#${category}`, + item: items as DataItem[], + }; +} diff --git a/lib/routes/skeb/namespace.ts b/lib/routes/skeb/namespace.ts new file mode 100644 index 00000000000000..5a4962169a1da2 --- /dev/null +++ b/lib/routes/skeb/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Skeb', + url: 'skeb.jp', + lang: 'ja', +}; diff --git a/lib/routes/skeb/search.ts b/lib/routes/skeb/search.ts new file mode 100644 index 00000000000000..682a23eb813fe7 --- /dev/null +++ b/lib/routes/skeb/search.ts @@ -0,0 +1,77 @@ +import { Data, DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { processWork, baseUrl } from './utils'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +export const route: Route = { + path: '/search/:keyword', + categories: ['picture'], + example: '/skeb/search/初音ミク', + parameters: { keyword: 'Search keyword' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Search Results', + maintainers: ['SnowAgar25'], + handler, + description: 'Get the search results for works on Skeb', +}; + +async function handler(ctx): Promise<Data> { + const keyword = ctx.req.param('keyword'); + + if (!keyword) { + throw new InvalidParameterError('Invalid search keyword'); + } + + const url = 'https://hb1jt3kre9-dsn.algolia.net/1/indexes/*/queries'; + + const items = await cache.tryGet(`skeb:search:${keyword}`, async () => { + const data = await ofetch(url, { + method: 'POST', + headers: { + 'x-algolia-application-id': 'HB1JT3KRE9', + 'x-algolia-api-key': '9a4ce7d609e71bf29e977925e4c6740c', + }, + body: { + requests: [ + { + indexName: 'User', + query: keyword, + params: 'hitsPerPage=40', + filters: 'genres:art OR genres:comic OR genres:voice OR genres:novel OR genres:video OR genres:music OR genres:correction', + }, + { + indexName: 'Request', + query: keyword, + params: 'hitsPerPage=40&filters=genre%3Aart%20OR%20genre%3Acomic%20OR%20genre%3Avoice%20OR%20genre%3Anovel%20OR%20genre%3Avideo%20OR%20genre%3Amusic%20OR%20genre%3Acorrection', + }, + ], + }, + }); + + if (!data || !data.results || !Array.isArray(data.results) || data.results.length < 2) { + throw new Error('Invalid data received from API'); + } + + const works = data.results[1].hits; + + if (!Array.isArray(works)) { + throw new TypeError('Invalid hits data received from API'); + } + + return works.map((item) => processWork(item)).filter(Boolean); + }); + + return { + title: `Skeb - Search Results for "${keyword}"`, + link: `${baseUrl}/search?q=${encodeURIComponent(keyword)}`, + item: items as DataItem[], + }; +} diff --git a/lib/routes/skeb/templates/creator.art b/lib/routes/skeb/templates/creator.art new file mode 100644 index 00000000000000..bb64c9a1b962c6 --- /dev/null +++ b/lib/routes/skeb/templates/creator.art @@ -0,0 +1,8 @@ +{{ if avatarUrl }} + <img src="{{ avatarUrl }}" /> +{{ /if }} +<p>委託狀況(Accepting Commissions):{{ acceptingCommissions }}</p> +<p>NSFW:{{ nsfwAcceptable }}</p> +{{ if skills }} + <p>類型(Genre):{{ skills }}</p> +{{ /if }} diff --git a/lib/routes/skeb/templates/work.art b/lib/routes/skeb/templates/work.art new file mode 100644 index 00000000000000..6c0903453d7584 --- /dev/null +++ b/lib/routes/skeb/templates/work.art @@ -0,0 +1,10 @@ +{{ if imageUrl }} + <img src="{{ imageUrl }}" /><br> +{{ /if }} +{{ if audioUrl }} + <audio controls> + <source src="{{ audioUrl }}" type="audio/mp3"> + Your browser does not support the audio element. + </audio><br> +{{ /if }} +{{ body }} diff --git a/lib/routes/skeb/utils.ts b/lib/routes/skeb/utils.ts new file mode 100644 index 00000000000000..bb8275ed2c6565 --- /dev/null +++ b/lib/routes/skeb/utils.ts @@ -0,0 +1,155 @@ +import { config } from '@/config'; +import { DataItem } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +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://skeb.jp'; + +interface Work { + path: string; + private_thumbnail_image_urls: null | string; + private: boolean; + genre: string; + tipped: boolean; + creator_id: number; + client_id: number; + vtt_url: null | string; + thumbnail_image_urls: { + src: string; + srcset: string; + }; + preview_url: null | string; + duration: null | number; + nsfw: boolean; + hardcore: boolean; + consored_thumbnail_image_urls: { + src: string; + srcset: string; + }; + body: string; + nc: number; + word_count: number; + transcoder: string; + creator_acceptable_same_genre: boolean; +} + +interface Creator { + id: number; + creator: boolean; + nsfw_acceptable: boolean; + acceptable: boolean; + name: string; + screen_name: string; + avatar_url: string; + header_url: string | null; + appeal_receivable: boolean; + popular_creator_rank: number | null; + request_master_rank: number | null; + first_requester_rank: number | null; + deleted_at: string | null; + tip_acceptable_by: string; + accept_expiration_days: number; + skills: { genre: string }[]; + nc: number; +} + +export function processWork(work: Work): DataItem | null { + if (!work || typeof work !== 'object' || work.private === true) { + return null; + } + + const imageUrl = work.thumbnail_image_urls?.srcset?.split(',').pop()?.trim().split(' ')[0] || ''; + const body = work.body || ''; + + const audioUrl = work.genre === 'music' || work.genre === 'voice' ? work.preview_url : null; + + const renderedHtml = art(path.join(__dirname, 'templates/work.art'), { + imageUrl, + body, + audioUrl, + }); + + return { + title: work.path || '', + link: `${baseUrl}${work.path || ''}`, + description: renderedHtml, + }; +} + +const skillMap = { + art: 'Illust', + voice: 'Voice', + novel: 'Novel', + video: 'Video', + music: 'Music', + correction: 'Advice', + comic: 'Comic', +}; + +export function processCreator(creator: Creator): DataItem | null { + if (!creator || typeof creator !== 'object') { + return null; + } + + const avatarUrl = creator.avatar_url || ''; + + let renderedHtml; + + if (creator.creator) { + const acceptingCommissions = creator.acceptable ? 'Yes' : 'No'; + const nsfwAcceptable = creator.nsfw_acceptable ? 'Yes' : 'No'; + + let skills = ''; + if (Array.isArray(creator.skills) && creator.skills.length > 0) { + skills = creator.skills + .map((skill) => skillMap[skill.genre] || skill.genre) + .filter(Boolean) + .join(', '); + } + + renderedHtml = art(path.join(__dirname, 'templates/creator.art'), { + avatarUrl, + acceptingCommissions, + nsfwAcceptable, + skills, + }); + } + + return { + title: creator.name || '', + link: `${baseUrl}/@${creator.screen_name || ''}`, + description: renderedHtml, + }; +} + +export async function getFollowingsItems(username: string, path: 'friend_works' | 'following_works' | 'following_creators'): Promise<DataItem[]> { + const url = `${baseUrl}/api/users/${username.replace('@', '')}/followings`; + + const followings_data = await cache.tryGet( + `skeb:followings_data:${username}`, + async () => { + const data = await ofetch(url, { + headers: { + Authorization: `Bearer ${config.skeb.bearerToken}`, + }, + }); + return data; + }, + config.cache.routeExpire, + false + ); + + if (!followings_data || typeof followings_data !== 'object') { + throw new Error('Failed to fetch followings data'); + } + + if (path === 'following_creators') { + return followings_data[path].map((item) => processCreator(item)).filter(Boolean) as DataItem[]; + } + return followings_data[path].map((item) => processWork(item)).filter(Boolean) as DataItem[]; +} diff --git a/lib/routes/skeb/works.ts b/lib/routes/skeb/works.ts new file mode 100644 index 00000000000000..ff599f456f17fd --- /dev/null +++ b/lib/routes/skeb/works.ts @@ -0,0 +1,88 @@ +import { Data, DataItem, Route } from '@/types'; +import { config } from '@/config'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import { baseUrl, processWork } from './utils'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/works/:username', + categories: ['picture'], + example: '/skeb/works/@brm2_1925', + parameters: { username: 'Skeb Username with @' }, + features: { + requireConfig: [ + { + name: 'SKEB_BEARER_TOKEN', + optional: false, + description: '在瀏覽器開發者工具(F12)的主控台中輸入 `localStorage.getItem("token")` 獲取', + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Creator Works', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'Creator Works', + source: ['skeb.jp/:username'], + target: '/works/:username', + }, + ], + description: 'Get the latest works of a specific creator on Skeb', +}; + +async function handler(ctx): Promise<Data> { + const username = ctx.req.param('username'); + + if (!config.skeb || !config.skeb.bearerToken) { + throw new ConfigNotFoundError('Skeb works RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>'); + } + + const url = `${baseUrl}/api/users/${username.replace('@', '')}/works`; + + const items = await cache.tryGet(url, async () => { + const fetchData = async (retryCount = 0, maxRetries = 3) => { + const data = await ofetch(url, { + retry: 0, + method: 'GET', + query: { role: 'creator', sort: 'date', offset: '0' }, + headers: { + 'User-Agent': config.ua, + Cookie: `request_key=${cache.get('skeb:request_key')}`, + Authorization: `Bearer ${config.skeb.bearerToken}`, + }, + }).catch((error) => { + if (retryCount >= maxRetries) { + throw new Error('Max retries reached'); + } + const newRequestKey = error.response?._data?.match(/request_key=(.*?);/)?.[1]; + if (newRequestKey) { + cache.set('skeb:request_key', newRequestKey); + return fetchData(retryCount + 1, maxRetries); + } + throw error; + }); + return data; + }; + + const data = await fetchData(); + + if (!data || !Array.isArray(data)) { + throw new Error('Invalid data received from API'); + } + + return data.map((item) => processWork(item)).filter(Boolean); + }); + + return { + title: `Skeb - ${username}'s Works`, + link: `${baseUrl}/${username}`, + item: items as DataItem[], + }; +} diff --git a/lib/routes/skebetter/illust.ts b/lib/routes/skebetter/illust.ts new file mode 100644 index 00000000000000..c5d0fa943a96ee --- /dev/null +++ b/lib/routes/skebetter/illust.ts @@ -0,0 +1,83 @@ +import { Data, DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import { processItems, fetchData } from './utils'; +import { config } from '@/config'; + +export const route: Route = { + path: '/illust/:type', + categories: ['anime'], + example: '/skebetter/illust/hot', + parameters: { type: 'Type, see below' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Illust', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'Illust - Hot', + source: ['skebetter.com/illust'], + target: '/illust/hot', + }, + { + title: 'Illust - Week', + source: ['skebetter.com/illust'], + target: '/illust/week', + }, + { + title: 'Illust - Month', + source: ['skebetter.com/illust'], + target: '/illust/month', + }, + { + title: 'Illust - Latest', + source: ['skebetter.com/illust'], + target: '/illust/latest', + }, + ], + description: ` +| 急上昇 | 週間 | 月間 | 新着 | +| ----- | ---- | ---- | ---- | +| hot | week | month| latest |`, +}; + +async function handler(ctx): Promise<Data> { + const type = ctx.req.param('type'); + const baseUrl = 'https://api.twieromanga.com/api/illust/hot'; + const typeMap = { + hot: '急上昇', + week: '週間', + month: '月間', + latest: '新着', + }; + const linkMap = { + hot: '', + week: '?term=week', + month: '?term=month', + latest: '?term=latest', + }; + + const url = `${baseUrl}?type=${type}`; + + const items = await cache.tryGet( + url, + async () => { + const data = await fetchData(url); + return processItems(data, 'illust'); + }, + config.cache.routeExpire, + false + ); + + return { + title: `Skebetter Illust - ${typeMap[type]}`, + link: `https://skebetter.com/illust${linkMap[type]}`, + item: items as DataItem[], + }; +} diff --git a/lib/routes/skebetter/index.ts b/lib/routes/skebetter/index.ts new file mode 100644 index 00000000000000..3ccd93e0bd73c3 --- /dev/null +++ b/lib/routes/skebetter/index.ts @@ -0,0 +1,83 @@ +import { Data, DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import { processItems, fetchData } from './utils'; +import { config } from '@/config'; + +export const route: Route = { + path: '/:type', + categories: ['anime'], + example: '/skebetter/hot', + parameters: { type: 'Type, see below' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Hot', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'Skebetter - Hot', + source: ['skebetter.com'], + target: '/hot', + }, + { + title: 'Skebetter - Week', + source: ['skebetter.com'], + target: '/week', + }, + { + title: 'Skebetter - Month', + source: ['skebetter.com'], + target: '/month', + }, + { + title: 'Skebetter - Latest', + source: ['skebetter.com'], + target: '/latest', + }, + ], + description: ` +| 急上昇 | 週間 | 月間 | 新着 | +| ----- | ---- | ---- | ---- | +| hot | week | month| latest |`, +}; + +async function handler(ctx): Promise<Data> { + const type = ctx.req.param('type'); + const baseUrl = 'https://api.twieromanga.com/api/hotv2'; + const typeMap = { + hot: '急上昇', + week: '週間', + month: '月間', + latest: '新着', + }; + const linkMap = { + hot: '', + week: '?term=week', + month: '?term=month', + latest: '?term=latest', + }; + + const url = `${baseUrl}?type=${type}`; + + const items = await cache.tryGet( + url, + async () => { + const data = await fetchData(url); + return processItems(data, 'index'); + }, + config.cache.routeExpire, + false + ); + + return { + title: `Skebetter - ${typeMap[type]}`, + link: `https://skebetter.com/${linkMap[type]}`, + item: items as DataItem[], + }; +} diff --git a/lib/routes/skebetter/manga.ts b/lib/routes/skebetter/manga.ts new file mode 100644 index 00000000000000..4b63cf301597c0 --- /dev/null +++ b/lib/routes/skebetter/manga.ts @@ -0,0 +1,65 @@ +import { Data, DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import { processItems, fetchData } from './utils'; +import { config } from '@/config'; + +export const route: Route = { + path: '/manga/:order', + categories: ['anime'], + example: '/skebetter/manga/1', + parameters: { order: 'Order, see below.' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Manga', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'Manga - Latest', + source: ['skebetter.com/series'], + target: '/manga/1', + }, + { + title: 'Manga - Hot', + source: ['skebetter.com/series'], + target: '/manga/2', + }, + ], + description: ` +| 新着 (Latest) | 人気 (Hot) | +| ---- | ---- | +| 1 | 2 |`, +}; + +async function handler(ctx): Promise<Data> { + const order = ctx.req.param('order') ?? '1'; + const baseUrl = 'https://api.twieromanga.com/api/mangaseries'; + const orderMap = { + '1': '新着', + '2': '人気', + }; + + const url = `${baseUrl}?order=${order}`; + + const items = await cache.tryGet( + url, + async () => { + const data = await fetchData(url, true); + return processItems(data, 'manga'); + }, + config.cache.routeExpire, + false + ); + + return { + title: `Skebetter Manga - ${orderMap[order]}`, + link: `https://skebetter.com/series?order=${order}`, + item: items as DataItem[], + }; +} diff --git a/lib/routes/skebetter/namespace.ts b/lib/routes/skebetter/namespace.ts new file mode 100644 index 00000000000000..fdc4859f678523 --- /dev/null +++ b/lib/routes/skebetter/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Skebetter', + url: 'skebetter.com', + lang: 'en', +}; diff --git a/lib/routes/skebetter/utils.ts b/lib/routes/skebetter/utils.ts new file mode 100644 index 00000000000000..271fe55398d918 --- /dev/null +++ b/lib/routes/skebetter/utils.ts @@ -0,0 +1,72 @@ +import { DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; + +export interface MediaUrl { + h: number; + w: number; + sh: number; + sw: number; + type: string; + media_id: string | null; + media_uri: string; + media_index: number; +} + +export interface Tweet { + id: string; + screen_name: string; + series_id?: number; + text: string; + date: string; + img: string; + user_name: string; + fav: string; + retweet: string; + user_id: string; + media_url: string; + media_count: number; + hashtags?: Record<string, unknown>; + media_urls: MediaUrl[]; + score: string; + rank: number; +} + +export const processItems = (data: Tweet[], type: 'index' | 'illust' | 'manga'): DataItem[] => + data.map((item) => { + const baseAuthorUrl = `https://skebetter.com/author/${item.user_id}`; + const description = ` + <p>❤${item.fav} 🔁${item.retweet}</p> + ${item.media_urls.map((media) => `<img src="${media.media_uri}" />`).join('')} + `; + + if (type === 'manga') { + return { + title: item.text, + description, + author: item.user_name, + link: `https://skebetter.com/series/${item.series_id}`, + }; + } + + if (type === 'illust') { + return { + title: item.text, + description, + author: item.user_name, + link: `${baseAuthorUrl}/illust/${item.id}`, + }; + } + + // type === 'index' + return { + title: item.text, + description, + author: item.user_name, + link: item.series_id ? `https://skebetter.com/series/${item.series_id}` : `${baseAuthorUrl}/manga/${item.id}`, + }; + }); + +export const fetchData = async (url: string, isManga: boolean = false) => { + const response = await ofetch(url); + return isManga ? (response as Tweet[]) : (response.tweet as Tweet[]); +}; diff --git a/lib/routes/sketis/isabelle-dev/blog/index.ts b/lib/routes/sketis/isabelle-dev/blog/index.ts new file mode 100644 index 00000000000000..f90577f5586ab6 --- /dev/null +++ b/lib/routes/sketis/isabelle-dev/blog/index.ts @@ -0,0 +1,77 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +const source = [ + 'isabelle-dev.sketis.net/phame/', + 'isabelle-dev.sketis.net/phame/blog/', + 'isabelle-dev.sketis.net/phame/blog/view/:blog/', + 'isabelle-dev.sketis.net/phame/post/', + 'isabelle-dev.sketis.net/phame/post/view/:post_id/:post_title/', +]; + +export const route: Route = { + path: '/isabelle-dev/blog/:blog', + categories: ['programming'], + example: '/sketis/isabelle-dev/blog/1', + parameters: { blog: 'name of blog (1 for NEWS; 2 for Release)' }, + description: ` +- Isabelle News: \`https://isabelle-dev.sketis.net/phame/blog/view/1/\` +- Isabelle Release: \`https://isabelle-dev.sketis.net/phame/blog/view/2/\` +`, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source, + target: '/isabelle-dev/blog/1', + }, + { + source, + target: '/isabelle-dev/blog/2', + }, + ], + name: 'Isabelle Development Blogs', + url: 'isabelle-dev.sketis.net', + maintainers: ['Ritsuka314'], + handler: async (ctx) => { + const baseUrl = 'https://isabelle-dev.sketis.net'; + const { blog } = ctx.req.param(); + const blogName = blog === '1' ? 'News' : (blog === '2' ? 'Release' : 'UNKNOWN'); + const url = `${baseUrl}/phame/blog/view/${blog}/`; + const response = await ofetch(url); + const $ = load(response); + + const items = $('.phui-document-summary-view') + .toArray() + .map((item_) => { + const item = $(item_); + const title = item.find('.remarkup-header').first(); + const subtitle = item.find('.phui-document-summary-subtitle').first(); + const date = subtitle.find('strong').first()[0].nextSibling.data.slice(4); // parse starts after ' on ' + return { + title: title.text(), + // We need an absolute URL for `link`, but `a.attr('href')` returns a relative URL. + link: `${baseUrl}${title.find('a').attr('href')}`, + description: item.find('.phui-document-summary-body').html(), + pubDate: parseDate(date), + author: subtitle.find('strong').text(), + }; + }); + + return { + title: `Isabelle ${blogName}`, + // channel link + link: url, + // each feed item + item: items, + }; + }, +}; diff --git a/lib/routes/sketis/namespace.ts b/lib/routes/sketis/namespace.ts new file mode 100644 index 00000000000000..aeb569174cc286 --- /dev/null +++ b/lib/routes/sketis/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Sketis | Website of Dr. Makarius Wenzel', + url: 'sketis.net', + lang: 'en', +}; diff --git a/lib/routes/skysports/namespace.ts b/lib/routes/skysports/namespace.ts index df4e6542f487bf..f8a8a21667c649 100644 --- a/lib/routes/skysports/namespace.ts +++ b/lib/routes/skysports/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Sky Sports', url: 'skysports.com', + lang: 'en', }; diff --git a/lib/routes/slowmist/namespace.ts b/lib/routes/slowmist/namespace.ts index e955bf77ee23c9..e547fa8fb1c1cf 100644 --- a/lib/routes/slowmist/namespace.ts +++ b/lib/routes/slowmist/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '慢雾科技', url: 'slowmist.com', + lang: 'zh-CN', }; diff --git a/lib/routes/slowmist/slowmist.ts b/lib/routes/slowmist/slowmist.ts index 3ca828b792de05..eccfb4babe9c80 100644 --- a/lib/routes/slowmist/slowmist.ts +++ b/lib/routes/slowmist/slowmist.ts @@ -6,7 +6,7 @@ import { finishArticleItem } from '@/utils/wechat-mp'; export const route: Route = { path: '/:type?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/slowmist/research', parameters: { type: '分类,见下表,默认为公司新闻' }, features: { @@ -27,8 +27,8 @@ export const route: Route = { handler, url: 'slowmist.com/zh/news.html', description: `| 公司新闻 | 漏洞披露 | 技术研究 | - | -------- | -------- | -------- | - | news | vul | research |`, +| -------- | -------- | -------- | +| news | vul | research |`, }; async function handler(ctx) { diff --git a/lib/routes/smashingmagazine/category.ts b/lib/routes/smashingmagazine/category.ts index 3137dc8ddbf926..ebf06eb14f6603 100644 --- a/lib/routes/smashingmagazine/category.ts +++ b/lib/routes/smashingmagazine/category.ts @@ -28,40 +28,40 @@ export const route: Route = { handler, url: 'smashingmagazine.com/articles/', description: `| **Category** | | - | ------------------ | ------------------ | - | Accessibility | accessibility | - | Best practices | best-practices | - | Business | business | - | Career | career | - | Checklists | checklists | - | CSS | css | - | Data Visualization | data-visualization | - | Design | design | - | Design Patterns | design-patterns | - | Design Systems | design-systems | - | E-Commerce | e-commerce | - | Figma | figma | - | Freebies | freebies | - | HTML | html | - | Illustrator | illustrator | - | Inspiration | inspiration | - | JavaScript | javascript | - | Mobile | mobile | - | Performance | performance | - | Privacy | privacy | - | React | react | - | Responsive Design | responsive-design | - | Round-Ups | round-ups | - | SEO | seo | - | Typography | typography | - | Tools | tools | - | UI | ui | - | Usability | usability | - | UX | ux | - | Vue | vue | - | Wallpapers | wallpapers | - | Web Design | web-design | - | Workflow | workflow |`, +| ------------------ | ------------------ | +| Accessibility | accessibility | +| Best practices | best-practices | +| Business | business | +| Career | career | +| Checklists | checklists | +| CSS | css | +| Data Visualization | data-visualization | +| Design | design | +| Design Patterns | design-patterns | +| Design Systems | design-systems | +| E-Commerce | e-commerce | +| Figma | figma | +| Freebies | freebies | +| HTML | html | +| Illustrator | illustrator | +| Inspiration | inspiration | +| JavaScript | javascript | +| Mobile | mobile | +| Performance | performance | +| Privacy | privacy | +| React | react | +| Responsive Design | responsive-design | +| Round-Ups | round-ups | +| SEO | seo | +| Typography | typography | +| Tools | tools | +| UI | ui | +| Usability | usability | +| UX | ux | +| Vue | vue | +| Wallpapers | wallpapers | +| Web Design | web-design | +| Workflow | workflow |`, }; async function handler(ctx) { diff --git a/lib/routes/smashingmagazine/namespace.ts b/lib/routes/smashingmagazine/namespace.ts index 60a7f1d150a91d..ad0adea53f2c05 100644 --- a/lib/routes/smashingmagazine/namespace.ts +++ b/lib/routes/smashingmagazine/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Smashing Magazine', url: 'smashingmagazine.com', + lang: 'en', }; diff --git a/lib/routes/smzdm/haowen-fenlei.ts b/lib/routes/smzdm/haowen-fenlei.ts index bfd0a5f7f01b78..3d7c9dc45e6662 100644 --- a/lib/routes/smzdm/haowen-fenlei.ts +++ b/lib/routes/smzdm/haowen-fenlei.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['LogicJake'], handler, description: `| 最新 | 周排行 | 月排行 | - | ---- | ------ | ------ | - | 0 | 7 | 30 |`, +| ---- | ------ | ------ | +| 0 | 7 | 30 |`, }; async function handler(ctx) { diff --git a/lib/routes/smzdm/haowen.ts b/lib/routes/smzdm/haowen.ts index a7732cc650cfae..ce710e7b8a94f6 100644 --- a/lib/routes/smzdm/haowen.ts +++ b/lib/routes/smzdm/haowen.ts @@ -1,6 +1,6 @@ -import { Route } from '@/types'; +import { DataItem, 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'; import timezone from '@/utils/timezone'; @@ -9,7 +9,17 @@ export const route: Route = { path: '/haowen/:day?', categories: ['shopping'], example: '/smzdm/haowen/1', - parameters: { day: '以天为时间跨度,默认为 `all`,其余可以选择 `1`,`7`,`30`,`365`' }, + parameters: { + day: { + description: '以天为时间跨度,默认为 `1`', + options: [ + { value: '1', label: '今日热门' }, + { value: '7', label: '周热门' }, + { value: '30', label: '月热门' }, + ], + default: '1', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -19,43 +29,49 @@ export const route: Route = { supportScihub: false, }, name: '好文', - maintainers: ['LogicJake'], + maintainers: ['LogicJake', 'pseudoyu'], handler, }; async function handler(ctx) { - const day = ctx.req.param('day') ?? 'all'; + const day = ctx.req.param('day') ?? '1'; const link = `https://post.smzdm.com/hot_${day}/`; - const response = await got(link); - const $ = load(response.data); + const response = await ofetch(link); + const $ = load(response); const title = $('li.filter-tab.active').text(); const list = $('li.feed-row-wide') .toArray() .map((item) => { - item = $(item); + const $item = $(item); return { - title: item.find('h5.z-feed-title a').text(), - link: item.find('h5.z-feed-title a').attr('href'), - pubDate: timezone(parseDate(item.find('span.z-publish-time').text()), 8), + title: $item.find('h5.z-feed-title a').text(), + link: $item.find('h5.z-feed-title a').attr('href'), + pubDate: timezone(parseDate($item.find('span.z-publish-time').text()), 8), }; }); const out = await Promise.all( list.map((item) => - cache.tryGet(item.link, async () => { - const response = await got(item.link); - const $ = load(response.data); + cache.tryGet(item.link ?? '', async () => { + const response = await ofetch(item.link ?? ''); + const $ = load(response); const content = $('#articleId'); content.find('.item-name').remove(); content.find('.recommend-tab').remove(); - item.description = content.html(); - item.pubDate = timezone(parseDate($('meta[property="og:release_date"]').attr('content')), 8); - item.author = $('meta[property="og:author"]').attr('content'); + const releaseDate = $('meta[property="og:release_date"]').attr('content'); + + const outItem: DataItem = { + title: item.title, + link: item.link, + description: content.html() || '', + pubDate: releaseDate ? timezone(parseDate(releaseDate), 8) : item.pubDate, + author: $('meta[property="og:author"]').attr('content') || '', + }; - return item; + return outItem; }) ) ); @@ -63,6 +79,6 @@ async function handler(ctx) { return { title: `${title}-什么值得买好文`, link, - item: out, + item: out.filter((item): item is DataItem => item !== null), }; } diff --git a/lib/routes/smzdm/keyword.ts b/lib/routes/smzdm/keyword.ts index d5d4799a2b093e..1161ebcf6c94f5 100644 --- a/lib/routes/smzdm/keyword.ts +++ b/lib/routes/smzdm/keyword.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; @@ -6,7 +6,8 @@ import timezone from '@/utils/timezone'; export const route: Route = { path: '/keyword/:keyword', - categories: ['shopping'], + categories: ['shopping', 'popular'], + view: ViewType.Notifications, example: '/smzdm/keyword/女装', parameters: { keyword: '你想订阅的关键词' }, features: { diff --git a/lib/routes/smzdm/namespace.ts b/lib/routes/smzdm/namespace.ts index 85fc6c216f537e..2162099df72d93 100644 --- a/lib/routes/smzdm/namespace.ts +++ b/lib/routes/smzdm/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '什么值得买', url: 'post.smzdm.com', - description: `:::tip + description: `::: tip 网站也提供了部分 RSS: [https://www.smzdm.com/dingyue](https://www.smzdm.com/dingyue) :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/smzdm/product.ts b/lib/routes/smzdm/product.ts new file mode 100644 index 00000000000000..14e9ca19a74cb0 --- /dev/null +++ b/lib/routes/smzdm/product.ts @@ -0,0 +1,85 @@ +import { Route, DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/product/:id', + categories: ['shopping'], + example: '/smzdm/product/zm5vzpe', + parameters: { id: '商品 id,网址上直接可以看到' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['wiki.smzdm.com/p/:id'], + target: '/product/:id', + }, + ], + name: '商品', + maintainers: ['chesha1'], + handler, +}; + +async function handler(ctx) { + const link = `https://wiki.smzdm.com/p/${ctx.req.param('id')}`; + + const response = await ofetch(link); + const $ = load(response); + const title = $('title').text(); + + // get simple info from list + const items: DataItem[] = $('ul#feed-main-list li') + .toArray() + .map((elem) => { + const altText = $(elem).find('img').attr('alt'); + const link = $(elem).find('h5.feed-block-title a').attr('href'); + const price = $(elem).find('.z-highlight').text(); + const title = altText + ' ' + price; + const description = $(elem).find('.feed-block-descripe').text().replaceAll(/\s+/g, ''); + + return { + title, + link, + description, + }; + }); + + // get detail info from each item + const out = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + + // filter outdated articles + if ($('span.old').length > 0) { + return null; + } else { + const pubDate = $('meta[name="weibo:webpage:create_at"]').attr('content'); + item.pubDate = pubDate; + + if (item.description === '阅读全文') { + item.description = $('p[itemprop="description"]').first().html() as string; + } + + return item; + } + }) + ) + ); + + const filteredOut = out.filter((result) => result !== null); + + return { + title, + link, + item: filteredOut, + }; +} diff --git a/lib/routes/smzdm/ranking.ts b/lib/routes/smzdm/ranking.ts index c12be8399ee14b..608273fdc153dc 100644 --- a/lib/routes/smzdm/ranking.ts +++ b/lib/routes/smzdm/ranking.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import timezone from '@/utils/timezone'; @@ -15,9 +15,186 @@ const getTrueHour = (rank_type, rank_id, hour) => { export const route: Route = { path: '/ranking/:rank_type/:rank_id/:hour', - categories: ['shopping'], + categories: ['shopping', 'popular'], + view: ViewType.Notifications, example: '/smzdm/ranking/pinlei/11/3', - parameters: { rank_type: '榜单类型', rank_id: '榜单ID', hour: '时间跨度' }, + parameters: { + rank_type: { + description: '榜单类型', + options: [ + { + value: 'pinlei', + label: '好价品类榜', + }, + { + value: 'dianshang', + label: '好价电商榜', + }, + { + value: 'haitao', + label: '海淘 TOP 榜', + }, + { + value: 'haowen', + label: '好文排行榜', + }, + { + value: 'haowu', + label: '好物排行榜', + }, + ], + }, + rank_id: { + description: '榜单ID', + options: [ + { + label: '好价品类榜 - 全部', + value: '11', + }, + { + label: '好价品类榜 - 食品生鲜', + value: '12', + }, + { + label: '好价品类榜 - 电脑数码', + value: '13', + }, + { + label: '好价品类榜 - 运动户外', + value: '14', + }, + { + label: '好价品类榜 - 家用电器', + value: '15', + }, + { + label: '好价品类榜 - 白菜', + value: '17', + }, + { + label: '好价品类榜 - 服饰鞋包', + value: '74', + }, + { + label: '好价品类榜 - 日用百货', + value: '75', + }, + { + label: '好价电商榜 - 券活动', + value: '24', + }, + { + label: '好价电商榜 - 京东', + value: '23', + }, + { + label: '好价电商榜 - 天猫', + value: '25', + }, + { + label: '好价电商榜 - 亚马逊中国', + value: '26', + }, + { + label: '好价电商榜 - 国美在线', + value: '27', + }, + { + label: '好价电商榜 - 苏宁易购', + value: '28', + }, + { + label: '好价电商榜 - 网易', + value: '29', + }, + { + label: '好价电商榜 - 西集网', + value: '30', + }, + { + label: '好价电商榜 - 美国亚马逊', + value: '31', + }, + { + label: '好价电商榜 - 日本亚马逊', + value: '32', + }, + { + label: '好价电商榜 - ebay', + value: '33', + }, + { + label: '海淘 TOP 榜 - 全部', + value: '39', + }, + { + label: '海淘 TOP 榜 - 海外直邮', + value: '34', + }, + { + label: '海淘 TOP 榜 - 美国榜', + value: '35', + }, + { + label: '海淘 TOP 榜 - 欧洲榜', + value: '36', + }, + { + label: '海淘 TOP 榜 - 澳新榜', + value: '37', + }, + { + label: '海淘 TOP 榜 - 亚洲榜', + value: '38', + }, + { + label: '海淘 TOP 榜 - 晒物榜', + value: 'hsw', + }, + { + label: '好文排行榜 - 原创', + value: 'yc', + }, + { + label: '好文排行榜 - 资讯', + value: 'zx', + }, + { + label: '好物排行榜 - 新晋榜', + value: 'hwall', + }, + { + label: '好物排行榜 - 消费众测', + value: 'zc', + }, + { + label: '好物排行榜 - 新锐品牌', + value: 'nb', + }, + { + label: '好物排行榜 - 好物榜单', + value: 'hw', + }, + ], + }, + hour: { + description: '时间跨度', + options: [ + { + value: '3', + label: '3 小时', + }, + { + value: '12', + label: '12 小时', + }, + { + value: '24', + label: '24 小时', + }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -29,49 +206,6 @@ export const route: Route = { name: '排行榜', maintainers: ['DIYgod'], handler, - description: `- 榜单类型 - - | 好价品类榜 | 好价电商榜 | 海淘 TOP 榜 | 好文排行榜 | 好物排行榜 | - | ---------- | ---------- | ----------- | ---------- | ---------- | - | pinlei | dianshang | haitao | haowen | haowu | - - - 榜单 ID - - 好价品类榜 - - | 全部 | 食品生鲜 | 电脑数码 | 运动户外 | 家用电器 | 白菜 | 服饰鞋包 | 日用百货 | - | ---- | -------- | -------- | -------- | -------- | ---- | -------- | -------- | - | 11 | 12 | 13 | 14 | 15 | 17 | 74 | 75 | - - 好价电商榜 - - | 券活动 | 京东 | 天猫 | 亚马逊中国 | 国美在线 | 苏宁易购 | 网易 | 西集网 | 美国亚马逊 | 日本亚马逊 | ebay | - | ------ | ---- | ---- | ---------- | -------- | -------- | ---- | ------ | ---------- | ---------- | ---- | - | 24 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | - - 海淘 TOP 榜 - - | 全部 | 海外直邮 | 美国榜 | 欧洲榜 | 澳新榜 | 亚洲榜 | 晒物榜 | - | ---- | -------- | ------ | ------ | ------ | ------ | ------ | - | 39 | 34 | 35 | 36 | 37 | 38 | hsw | - - 好文排行榜 - - | 原创 | 资讯 | - | ---- | ---- | - | yc | zx | - - 好物排行榜 - - | 新晋榜 | 消费众测 | 新锐品牌 | 好物榜单 | - | ------ | -------- | -------- | -------- | - | hwall | zc | nb | hw | - - - 时间跨度 - - | 3 小时 | 12 小时 | 24 小时 | - | ------ | ------- | ------- | - | 3 | 12 | 24 |`, }; async function handler(ctx) { diff --git a/lib/routes/snowpeak/namespace.ts b/lib/routes/snowpeak/namespace.ts index 883689e7f11e0f..b8bb0efef1c85a 100644 --- a/lib/routes/snowpeak/namespace.ts +++ b/lib/routes/snowpeak/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Snow Peak', url: 'snowpeak.com', + lang: 'en', }; diff --git a/lib/routes/sobooks/index.ts b/lib/routes/sobooks/index.ts index 4e724d49208b35..3d53bbdfab49c0 100644 --- a/lib/routes/sobooks/index.ts +++ b/lib/routes/sobooks/index.ts @@ -24,15 +24,15 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 分类 | 分类名 | - | -------- | ---------------- | - | 小说文学 | xiaoshuowenxue | - | 历史传记 | lishizhuanji | - | 人文社科 | renwensheke | - | 励志成功 | lizhichenggong | - | 经济管理 | jingjiguanli | - | 学习教育 | xuexijiaoyu | - | 生活时尚 | shenghuoshishang | - | 英文原版 | yingwenyuanban |`, +| -------- | ---------------- | +| 小说文学 | xiaoshuowenxue | +| 历史传记 | lishizhuanji | +| 人文社科 | renwensheke | +| 励志成功 | lizhichenggong | +| 经济管理 | jingjiguanli | +| 学习教育 | xuexijiaoyu | +| 生活时尚 | shenghuoshishang | +| 英文原版 | yingwenyuanban |`, }; async function handler(ctx) { diff --git a/lib/routes/sobooks/namespace.ts b/lib/routes/sobooks/namespace.ts index 1e9c392dc9cec7..a182127167d55f 100644 --- a/lib/routes/sobooks/namespace.ts +++ b/lib/routes/sobooks/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'SoBooks', url: 'sobooks.net', + lang: 'en', }; diff --git a/lib/routes/sobooks/tag.ts b/lib/routes/sobooks/tag.ts index 9f542e93692ebe..9fe0ddb3195e1c 100644 --- a/lib/routes/sobooks/tag.ts +++ b/lib/routes/sobooks/tag.ts @@ -25,13 +25,13 @@ export const route: Route = { handler, description: `热门标签 - | 小说 | 文学 | 历史 | 日本 | 科普 | 管理 | 推理 | 社会 | 经济 | - | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | - | 传记 | 美国 | 悬疑 | 哲学 | 心理 | 商业 | 金融 | 思维 | 经典 | - | 随笔 | 投资 | 文化 | 励志 | 科幻 | 成长 | 中国 | 英国 | 政治 | - | 漫画 | 纪实 | 艺术 | 科学 | 生活 | 职场 | 散文 | 法国 | 互联网 | - | 营销 | 奇幻 | 二战 | 股票 | 女性 | 德国 | 学习 | 战争 | 创业 | - | 绘本 | 名著 | 爱情 | 军事 | 理财 | 教育 | 世界 | 人物 | 沟通 |`, +| 小说 | 文学 | 历史 | 日本 | 科普 | 管理 | 推理 | 社会 | 经济 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | +| 传记 | 美国 | 悬疑 | 哲学 | 心理 | 商业 | 金融 | 思维 | 经典 | +| 随笔 | 投资 | 文化 | 励志 | 科幻 | 成长 | 中国 | 英国 | 政治 | +| 漫画 | 纪实 | 艺术 | 科学 | 生活 | 职场 | 散文 | 法国 | 互联网 | +| 营销 | 奇幻 | 二战 | 股票 | 女性 | 德国 | 学习 | 战争 | 创业 | +| 绘本 | 名著 | 爱情 | 军事 | 理财 | 教育 | 世界 | 人物 | 沟通 |`, }; async function handler(ctx) { diff --git a/lib/routes/sogou/namespace.ts b/lib/routes/sogou/namespace.ts index dab4d6b925b6cd..8c2707210f6970 100644 --- a/lib/routes/sogou/namespace.ts +++ b/lib/routes/sogou/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '搜狗', url: 'www.sogou.com', + lang: 'zh-CN', }; diff --git a/lib/routes/sogou/search.ts b/lib/routes/sogou/search.ts index 96ff94c63b5df3..5df85d561205c6 100644 --- a/lib/routes/sogou/search.ts +++ b/lib/routes/sogou/search.ts @@ -42,12 +42,13 @@ async function handler(ctx) { const result = $('#main'); return result .find('.vrwrap') - .map((i, el) => { + .toArray() + .map((el) => { const element = $(el); const imgs = element .find('img') - .map((j, el2) => $(el2).attr('src')) - .toArray(); + .toArray() + .map((el2) => $(el2).attr('src')); const link = element.find('h3 a').first().attr('href'); const title = element.find('h3').first().text(); const description = element.find('.text-layout').first().text() || element.find('.space-txt').first().text() || element.find('[class^="translate"]').first().text(); @@ -61,7 +62,6 @@ async function handler(ctx) { pubDate, }; }) - .toArray() .filter((e) => e?.link); }, config.cache.routeExpire, diff --git a/lib/routes/sohu/mp.ts b/lib/routes/sohu/mp.ts index b822b5f7f6eff3..865c0f653cbf42 100644 --- a/lib/routes/sohu/mp.ts +++ b/lib/routes/sohu/mp.ts @@ -8,10 +8,11 @@ import * as cheerio from 'cheerio'; import { parseDate } from '@/utils/parse-date'; import path from 'node:path'; import { art } from '@/utils/render'; +import CryptoJS from 'crypto-js'; export const route: Route = { path: '/mp/:xpt', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sohu/mp/c29odXptdGhnbjZ3NEBzb2h1LmNvbQ==', parameters: { xpt: '搜狐号 xpt ,可在URL中找到或搜狐号 ID' }, radar: [ @@ -39,6 +40,15 @@ function randomString(length = 32) { } const defaultSUV = '1612268936507kas0gk'; +function decryptImageUrl(cipherText) { + const key = CryptoJS.enc.Utf8.parse('www.sohu.com6666'); + const cipher = CryptoJS.AES.decrypt(cipherText, key, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }); + return cipher.toString(CryptoJS.enc.Utf8); +} + function fetchArticle(item) { return cache.tryGet(item.link, async () => { const response = await ofetch(item.link); @@ -64,6 +74,13 @@ function fetchArticle(item) { article.find('#backsohucom, p[data-role="editor-name"]').each((i, e) => { $(e).remove(); }); + article.find('img').each((_, e) => { + const $e = $(e); + if ($e.attr('data-src') && !$e.attr('src')) { + $e.attr('src', decryptImageUrl($e.attr('data-src'))); + $e.removeAttr('data-src'); + } + }); item.description = article.html(); } @@ -107,11 +124,12 @@ async function handler(ctx) { ) .sort((a: any, b: any) => b.length - a.length)[0] || '{}' ); - const renderData = JSON.parse( + const blockRenderData = JSON.parse( $('script:contains("column_2_text")') .text() - .match(/renderData:\s(.*)/)?.[1] || '{}' + .match(/({.*})/)?.[1] ); + const renderData = blockRenderData[Object.keys(blockRenderData).find((e) => e.startsWith('FeedSlideloadAuthor'))]; const globalConst = JSON.parse( $('script:contains("globalConst")') .text() diff --git a/lib/routes/sohu/namespace.ts b/lib/routes/sohu/namespace.ts index b857e377dc3069..10eb070274906f 100644 --- a/lib/routes/sohu/namespace.ts +++ b/lib/routes/sohu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '搜狐号', url: 'sohu.com', + lang: 'zh-CN', }; diff --git a/lib/routes/solidot/main.ts b/lib/routes/solidot/main.ts index 18f1d5bcba03bb..6770d9f06b486c 100644 --- a/lib/routes/solidot/main.ts +++ b/lib/routes/solidot/main.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; // Warning: The author still knows nothing about javascript! @@ -13,9 +13,34 @@ import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { path: '/:type?', - categories: ['traditional-media'], + categories: ['traditional-media', 'popular'], + view: ViewType.Articles, example: '/solidot/linux', - parameters: { type: '消息类型。默认为 www. 在网站上方选择后复制子域名即可' }, + parameters: { + type: { + description: '消息类型,在网站上方选择后复制子域名或参见 [https://www.solidot.org/index.rss](https://www.solidot.org/index.rss) 即可', + options: [ + { value: 'www', label: '全部' }, + { value: 'startup', label: '创业' }, + { value: 'linux', label: 'Linux' }, + { value: 'science', label: '科学' }, + { value: 'technology', label: '科技' }, + { value: 'mobile', label: '移动' }, + { value: 'apple', label: '苹果' }, + { value: 'hardware', label: '硬件' }, + { value: 'software', label: '软件' }, + { value: 'security', label: '安全' }, + { value: 'games', label: '游戏' }, + { value: 'books', label: '书籍' }, + { value: 'ask', label: 'ask' }, + { value: 'idle', label: 'idle' }, + { value: 'blog', label: '博客' }, + { value: 'cloud', label: '云计算' }, + { value: 'story', label: '奇客故事' }, + ], + default: 'www', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -27,15 +52,6 @@ export const route: Route = { name: '最新消息', maintainers: ['sgqy', 'hang333', 'TonyRL'], handler, - description: `:::tip - Solidot 提供的 feed: - - - [https://www.solidot.org/index.rss](https://www.solidot.org/index.rss) - ::: - - | 全部 | 创业 | Linux | 科学 | 科技 | 移动 | 苹果 | 硬件 | 软件 | 安全 | 游戏 | 书籍 | ask | idle | 博客 | 云计算 | 奇客故事 | - | ---- | ------- | ----- | ------- | ---------- | ------ | ----- | -------- | -------- | -------- | ----- | ----- | --- | ---- | ---- | ------ | -------- | - | www | startup | linux | science | technology | mobile | apple | hardware | software | security | games | books | ask | idle | blog | cloud | story |`, }; async function handler(ctx) { diff --git a/lib/routes/solidot/namespace.ts b/lib/routes/solidot/namespace.ts index da8e7dc28d9bd8..1fd7f60b59be1e 100644 --- a/lib/routes/solidot/namespace.ts +++ b/lib/routes/solidot/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Solidot', url: 'www.solidot.org', + lang: 'zh-CN', }; diff --git a/lib/routes/sony/downloads.ts b/lib/routes/sony/downloads.ts index 66c4217989301a..ea889c7e4aa690 100644 --- a/lib/routes/sony/downloads.ts +++ b/lib/routes/sony/downloads.ts @@ -23,9 +23,9 @@ export const route: Route = { name: 'Software Downloads', maintainers: ['EthanWng97'], handler, - description: `:::tip + description: `::: tip Open \`https://www.sony.com/electronics/support\` and search for the corresponding product, such as \`Sony A7M4\`, the website corresponding to which is \`https://www.sony.com/electronics/support/e-mount-body-ilce-7-series/ilce-7m4/downloads\`, where \`productType\` is \`e-mount-body-ilce-7-series\` and \`productId\` is \`ilce-7m4\`. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/sony/namespace.ts b/lib/routes/sony/namespace.ts index 7a09fd38687568..d21b73709fe659 100644 --- a/lib/routes/sony/namespace.ts +++ b/lib/routes/sony/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Sony', url: 'sony.com', + lang: 'en', }; diff --git a/lib/routes/sorrycc/index.ts b/lib/routes/sorrycc/index.ts new file mode 100644 index 00000000000000..6d96a4db939a01 --- /dev/null +++ b/lib/routes/sorrycc/index.ts @@ -0,0 +1,85 @@ +import { type DataItem, ViewType, type Data, type Route } from '@/types'; +import type { Context } from 'hono'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import type { Post } from './types'; +import { config } from '@/config'; +import { parseDate } from '@/utils/parse-date'; + +const WORDPRESS_HASH = 'f05fca638390aed897fbe3c2fff03000'; + +export const route: Route = { + name: '文章', + categories: ['blog'], + path: '/', + example: '/sorrycc', + radar: [ + { + source: ['sorrycc.com'], + }, + ], + handler, + maintainers: ['KarasuShin'], + view: ViewType.Articles, + features: { + supportRadar: true, + requireConfig: [ + { + name: 'SORRYCC_COOKIES', + description: `登录用户的Cookie,获取方式:\n1. 登录sorrycc.com\n2. 打开浏览器开发者工具,切换到 Application 面板\n3. 点击侧边栏中的Storage -> Cookies -> https://sorrycc.com\n4. 复制 Cookie 中的 wordpress_logged_in_${WORDPRESS_HASH} 值`, + optional: true, + }, + ], + }, + description: '云谦的博客,部分内容存在权限校验,访问完整内容请部署RSSHub私有实例并配置授权信息', +}; + +async function handler(ctx: Context): Promise<Data> { + const host = 'https://sorrycc.com'; + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')!, 10) : 100; + const cookie = config.sorrycc.cookie; + + const data = await ofetch<Post[]>(`${host}/wp-json/wp/v2/posts?per_page=${limit}`); + + const items: DataItem[] = await Promise.all( + data.map(async (item) => { + const title = item.title.rendered; + const link = item.link; + const pubDate = parseDate(item.date_gmt); + const updated = parseDate(item.modified_gmt); + if (item.categories.includes(7) && cookie) { + return (await cache.tryGet(link, async () => { + const article = await ofetch(link, { + headers: { + Cookie: `wordpress_logged_in_${WORDPRESS_HASH}=${cookie}`, + }, + }); + const $article = load(article); + const description = $article('.content').html(); + return { + title, + description, + link, + pubDate, + updated, + }; + })) as unknown as DataItem; + } + return { + title, + description: item.content.rendered, + link, + pubDate, + updated, + } as DataItem; + }) + ); + + return { + title: '文章', + item: items, + link: host, + image: `${host}/wp-content/uploads/2024/01/cropped-CC-1-32x32.png`, + }; +} diff --git a/lib/routes/sorrycc/namespace.ts b/lib/routes/sorrycc/namespace.ts new file mode 100644 index 00000000000000..5bd8a46dacb79c --- /dev/null +++ b/lib/routes/sorrycc/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '云谦的博客', + url: 'sorrycc.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/sorrycc/types.ts b/lib/routes/sorrycc/types.ts new file mode 100644 index 00000000000000..ff84da8ae61683 --- /dev/null +++ b/lib/routes/sorrycc/types.ts @@ -0,0 +1,13 @@ +export interface Post { + id: number; + content: { + rendered: string; + }; + date_gmt: string; + modified_gmt: string; + link: string; + categories: number[]; + title: { + rendered: string; + }; +} diff --git a/lib/routes/soundofhope/namespace.ts b/lib/routes/soundofhope/namespace.ts index 9eaf84054fab30..df15f89bd376f7 100644 --- a/lib/routes/soundofhope/namespace.ts +++ b/lib/routes/soundofhope/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '希望之声', url: 'soundofhope.org', + lang: 'en', }; diff --git a/lib/routes/soundon/namespace.ts b/lib/routes/soundon/namespace.ts new file mode 100644 index 00000000000000..037feabba13a6f --- /dev/null +++ b/lib/routes/soundon/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'SoundOn', + url: 'player.soundon.fm', + lang: 'en', +}; diff --git a/lib/routes/soundon/podcast.ts b/lib/routes/soundon/podcast.ts new file mode 100644 index 00000000000000..d8163dfc29e531 --- /dev/null +++ b/lib/routes/soundon/podcast.ts @@ -0,0 +1,80 @@ +import { Route, ViewType } from '@/types'; +import { config } from '@/config'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { Podcast, PodcastInfo } from './types'; + +const handler = async (ctx) => { + const { id } = ctx.req.param(); + + const apiEndpoint = 'https://api.soundon.fm/v2/client'; + const apiToken = 'KilpEMLQeNzxmNBL55u5'; + + const podcastInfo = (await cache.tryGet(`soundon:${id}`, async () => { + const response = await ofetch(`${apiEndpoint}/podcasts/${id}`, { + headers: { + 'api-token': apiToken, + }, + }); + return response.data.data; + })) as PodcastInfo; + + const episodes = (await cache.tryGet( + `soundon:${id}:episodes`, + async () => { + const response = await ofetch(`${apiEndpoint}/podcasts/${id}/episodes`, { + headers: { + 'api-token': apiToken, + }, + }); + return response.data; + }, + config.cache.routeExpire, + false + )) as Podcast[]; + + const items = episodes.map(({ data: item }) => ({ + title: item.title, + description: item.contentEncoded, + link: item.url, + author: item.artistName, + pubDate: parseDate(item.publishDate), + itunes_item_image: item.cover, + enclosure_url: item.audioUrl, + enclosure_type: item.audioType, + itunes_duration: item.duration, + category: item.itunesKeywords, + })); + + return { + title: podcastInfo.title, + description: podcastInfo.description, + itunes_author: podcastInfo.artistName, + itunes_category: podcastInfo.itunesCategories.join(', '), + itunes_explicit: podcastInfo.explicit, + image: podcastInfo.cover, + language: podcastInfo.language, + link: podcastInfo.url, + item: items, + }; +}; + +export const route: Route = { + path: '/p/:id', + categories: ['multimedia'], + example: '/soundon/p/33a68cdc-18ad-4192-84cc-22bd7fdc6a31', + parameters: { id: 'Podcast ID' }, + features: { + supportPodcast: true, + }, + radar: [ + { + source: ['player.soundon.fm/p/:id'], + }, + ], + name: 'Podcast', + maintainers: ['TonyRL'], + view: ViewType.Audios, + handler, +}; diff --git a/lib/routes/soundon/types.ts b/lib/routes/soundon/types.ts new file mode 100644 index 00000000000000..94cf36daf39dd9 --- /dev/null +++ b/lib/routes/soundon/types.ts @@ -0,0 +1,67 @@ +export interface PodcastInfo { + id: string; + title: string; + channels: string[]; + feedUrl: string; + explicit: boolean; + description: string; + itunesCategories: string[]; + cover: string; + complete: boolean; + blocked: boolean; + lastIndexedAt: string; + publishDate: string; + copyright: string; + url: string; + tsv: string; + ownerEmail: string; + ownerName: string; + artistName: string; + language: string; + subtitle: string; + enableProductPage: boolean; + itunesType: string; + contentEncoded: string; + createdAt: string; + updatedAt: string; + donationUrl: string; + weight: number; + activated: boolean; + guid: string; + soundonId: string; +} + +export interface Podcast { + id: string; + updatedAt: string; + createdAt: string; + data: PodcastData; +} + +interface PodcastData { + id: string; + guid: string; + hash: string; + title: string; + audioUrl: string; + explicit: boolean; + description: string; + complete: boolean; + publishDate: string; + itunesKeywords: string[]; + audioType: string; + duration: number; + artistName: string; + url: string; + cover: string; + contentEncoded: string; + podcastId: string; + summary: string; + episodeType: string; + exclusiveType: string; + createdAt: string; + updatedAt: string; + weight: number; + keywords: string[]; + activated: boolean; +} diff --git a/lib/routes/sourceforge/namespace.ts b/lib/routes/sourceforge/namespace.ts index 1ca5a9ac55bec6..fc2ba18622a6c8 100644 --- a/lib/routes/sourceforge/namespace.ts +++ b/lib/routes/sourceforge/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'SourceForge', url: 'www.sourceforge.net', + lang: 'en', }; diff --git a/lib/routes/southcn/namespace.ts b/lib/routes/southcn/namespace.ts index 962c514bc0cebd..2fa6f804b834c1 100644 --- a/lib/routes/southcn/namespace.ts +++ b/lib/routes/southcn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南方网', url: 'nfapp.southcn.com', + lang: 'zh-CN', }; diff --git a/lib/routes/southcn/nfapp/column.ts b/lib/routes/southcn/nfapp/column.ts index 0833e8f960d81c..10f4d8cbb7b32b 100644 --- a/lib/routes/southcn/nfapp/column.ts +++ b/lib/routes/southcn/nfapp/column.ts @@ -26,9 +26,9 @@ export const route: Route = { name: '南方 +(按栏目 ID)', maintainers: ['TimWu007'], handler, - description: `:::tip + description: `::: tip 若此处输入的是栏目 ID(而非南方号 ID),则该接口会返回与输入栏目相关联栏目的文章。例如,输入栏目 ID \`38\`(广州),则返回的结果还会包含 ID 为 \`3547\`(市长报道集)的文章。 - ::: +::: 1. \`pc.nfapp.southcn.com\` 下的文章页面,可通过 url 查看,例:\`http://pc.nfapp.southcn.com/13707/7491109.html\` 的栏目 ID 为 \`13707\`。 2. \`static.nfapp.southcn.com\` 下的文章页面,可查看网页源代码,搜索 \`columnid\`。 diff --git a/lib/routes/spankbang/namespace.ts b/lib/routes/spankbang/namespace.ts new file mode 100644 index 00000000000000..ddcf2d246d0143 --- /dev/null +++ b/lib/routes/spankbang/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'SpankBang', + url: 'spankbang.com', + lang: 'en', +}; diff --git a/lib/routes/spankbang/new-videos.ts b/lib/routes/spankbang/new-videos.ts new file mode 100644 index 00000000000000..182de368a2a0ef --- /dev/null +++ b/lib/routes/spankbang/new-videos.ts @@ -0,0 +1,90 @@ +import { Data, Route } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; + +import puppeteer from '@/utils/puppeteer'; +import * as cheerio from 'cheerio'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { config } from '@/config'; +import logger from '@/utils/logger'; +import cache from '@/utils/cache'; + +const __dirname = getCurrentPath(import.meta.url); +const render = (data) => art(path.join(__dirname, 'templates/video.art'), data); + +const handler = async () => { + const baseUrl = 'https://spankbang.com'; + const link = `${baseUrl}/new_videos/`; + + const browser = await puppeteer(); + + const data = await cache.tryGet( + link, + async () => { + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (request) => { + request.resourceType() === 'document' ? request.continue() : request.abort(); + }); + logger.http(`Requesting ${link}`); + await page.goto(link, { + waitUntil: 'domcontentloaded', + }); + + const response = await page.content(); + const $ = cheerio.load(response); + + const items = $('.video-item') + .toArray() + .map((item) => { + const $item = $(item); + const thumb = $item.find('.thumb'); + const cover = $item.find('img.cover'); + + return { + title: thumb.attr('title'), + link: new URL(thumb.attr('href')!, baseUrl).href, + description: render({ + cover: cover.data('src'), + preview: cover.data('preview'), + }), + }; + }); + + return { + title: $('head title').text(), + description: $('head meta[name="description"]').attr('content'), + item: items, + }; + }, + config.cache.routeExpire, + false + ); + + await browser.close(); + + return { + title: data.title, + description: data.description, + link, + item: data.item, + } as unknown as Promise<Data>; +}; + +export const route: Route = { + path: '/new_videos', + categories: ['multimedia'], + example: '/spankbang/new_videos', + name: 'New Porn Videos', + maintainers: ['TonyRL'], + features: { + antiCrawler: true, + requirePuppeteer: true, + }, + radar: [ + { + source: ['spankbang.com/new_videos/', 'spankbang.com/'], + }, + ], + handler, +}; diff --git a/lib/routes/spankbang/templates/video.art b/lib/routes/spankbang/templates/video.art new file mode 100644 index 00000000000000..c30942421bef40 --- /dev/null +++ b/lib/routes/spankbang/templates/video.art @@ -0,0 +1,7 @@ +{{ if preview }} + <video controls preload="metadata" + {{ if cover }} poster="{{ cover }}" {{ /if }} + > + <source src="{{ preview }}" type="video/mp4"> + </video> +{{ /if }} diff --git a/lib/routes/spglobal/namespace.ts b/lib/routes/spglobal/namespace.ts new file mode 100644 index 00000000000000..cbdfcf54bf062b --- /dev/null +++ b/lib/routes/spglobal/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'S&P Global', + url: 'www.spglobal.com', + lang: 'en', +}; diff --git a/lib/routes/spglobal/ratings.ts b/lib/routes/spglobal/ratings.ts new file mode 100644 index 00000000000000..b6ba748231a850 --- /dev/null +++ b/lib/routes/spglobal/ratings.ts @@ -0,0 +1,64 @@ +import { Route, ViewType } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import got from '@/utils/got'; + +export const route: Route = { + path: '/ratings/:language?', + categories: ['finance'], + view: ViewType.Notifications, + example: '/spglobal/ratings/en', + parameters: { + language: { + description: '语言', + options: [ + { value: 'zh', label: '中文' }, + { value: 'en', label: 'English' }, + { value: 'es', label: 'Español' }, + { value: 'pt', label: 'Português' }, + { value: 'jp', label: '日本語' }, + { value: 'ru', label: 'Русский' }, + { value: 'ar', label: 'العربية' }, + ], + }, + }, + radar: [ + { + source: ['www.spglobal.com/ratings/:language'], + }, + ], + name: 'Ratings', + description: ` +| language | Description | +| --- | --- | +| zh | 中文 | +| en | English | +| es | Español | +| pt | Português | +| jp | 日本語 | +| ru | Русский | +| ar | العربية | + `, + maintainers: ['FYLSen'], + handler, +}; + +async function handler(ctx) { + const language = ctx.req.param('language'); + + const responseData = await got( + `https://www.spglobal.com/crownpeaksearchproxy.aspx?q=https%3A%2F%2Fsearchg2-restricted.crownpeak.net%2Fsandpglobal-spglobal-live%2Fselect%3Fq%3D*%253A*%26echoParams%3Dexplicit%26fl%3Dtitle%2Ccustom_i_article_id%2Ccustom_ss_theme%2Ccustom_ss_theme_full%2Ccustom_dt_meta_publish_date%2Ccustom_s_meta_location%20%2Ccustom_s_local_url%2Ccustom_s_tile_image%2Ccustom_s_cshtml_path%2Ccustom_s_sub_type%2Ccustom_s_meta_type%2Cscore%2Ccustom_s_division%2Ccustom_ss_contenttype%2Ccustom_ss_location%2Ccustom_ss_region%2Ccustom_ss_theme%2Ccustom_ss_author_thumbnails%2Ccustom_ss_authors%2Ccustom_ss_author_titles%2Ccustom_s_meta_videoid%2Ctaxonomy_tag_freeform%2Ccustom_ss_tags%2Ccustom_ss_freeform%26defType%3Dedismax%26wt%3Djson%26start%3D0%26rows%3D10%26fq%3Dcustom_s_type%3Aarticle%26fq%3Dcustom_s_sub_type%3A(%22blog%22%2C%20%22news%22%2C%20%22research%22%2C%20%22podcast%22%2C%20%22video%22%2C%20%22article%22%2C%20%22pdf%20details%22)%26fq%3Dcustom_s_division%3A%22Ratings%22%26fq%3Dcustom_s_region%3A%22${language}%22%26facet%3Dtrue%26facet.mincount%3D1%26facet.field%3Dcustom_ss_theme_full%26facet.limit%3D15%26sort%3Dcustom_dt_meta_publish_date%20desc%26f.custom_ss_theme_full.facet.sort%3Dindex` + ); + + const items = responseData?.data?.response?.docs || []; + + return { + title: `S&P Global Ratings(${language})`, + link: `https://www.spglobal.com/ratings/${language}/`, + allowEmpty: true, + item: items.map((x) => ({ + title: x.title, + pubDate: parseDate(x.custom_dt_meta_publish_date), + link: `https://www.spglobal.com${x.custom_s_local_url}`, + })), + }; +} diff --git a/lib/routes/spotify/artist.ts b/lib/routes/spotify/artist.ts index e25f7884206091..9cc9028c4a326e 100644 --- a/lib/routes/spotify/artist.ts +++ b/lib/routes/spotify/artist.ts @@ -1,11 +1,12 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import utils from './utils'; -import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; export const route: Route = { path: '/artist/:id', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Audios, example: '/spotify/artist/6k9TBCxyr4bXwZ8Y21Kwn1', parameters: { id: 'Artist ID' }, features: { @@ -38,21 +39,18 @@ export const route: Route = { async function handler(ctx) { const token = await utils.getPublicToken(); const id = ctx.req.param('id'); - const meta = await got - .get(`https://api.spotify.com/v1/artists/${id}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .json(); - - const itemsResponse = await got - .get(`https://api.spotify.com/v1/artists/${id}/albums`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .json(); + const meta = await ofetch(`https://api.spotify.com/v1/artists/${id}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const itemsResponse = await ofetch(`https://api.spotify.com/v1/artists/${id}/albums`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); const albums = itemsResponse.items; return { diff --git a/lib/routes/spotify/artists-top.ts b/lib/routes/spotify/artists-top.ts index 0a8497d713a319..eff23670538158 100644 --- a/lib/routes/spotify/artists-top.ts +++ b/lib/routes/spotify/artists-top.ts @@ -1,6 +1,6 @@ import { Route } from '@/types'; import utils from './utils'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; export const route: Route = { path: '/top/artists', @@ -41,13 +41,12 @@ export const route: Route = { async function handler() { const token = await utils.getPrivateToken(); - const itemsResponse = await got - .get(`https://api.spotify.com/v1/me/top/artists`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .json(); + const itemsResponse = await ofetch(`https://api.spotify.com/v1/me/top/artists`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); const items = itemsResponse.items; return { diff --git a/lib/routes/spotify/namespace.ts b/lib/routes/spotify/namespace.ts index 20b9e3dca70c00..a1f911214f8a74 100644 --- a/lib/routes/spotify/namespace.ts +++ b/lib/routes/spotify/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Spotify', url: 'open.spotify.com', + lang: 'en', }; diff --git a/lib/routes/spotify/playlist.ts b/lib/routes/spotify/playlist.ts index b1888db12c6a6e..62c9dcdc0e686a 100644 --- a/lib/routes/spotify/playlist.ts +++ b/lib/routes/spotify/playlist.ts @@ -1,11 +1,12 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import utils from './utils'; -import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; export const route: Route = { path: '/playlist/:id', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Audios, example: '/spotify/playlist/4UBVy1LttvodwivPUuwJk2', parameters: { id: 'Playlist ID' }, features: { @@ -25,6 +26,9 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, + description: `::: warning +Due to [limitations by Spotify](https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api), this endpoint is unable to access "Algorithmic and Spotify-owned editorial playlists". +:::`, radar: [ { source: ['open.spotify.com/playlist/:id'], @@ -38,13 +42,12 @@ export const route: Route = { async function handler(ctx) { const token = await utils.getPublicToken(); const id = ctx.req.param('id'); - const meta = await got - .get(`https://api.spotify.com/v1/playlists/${id}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .json(); + const meta = await ofetch(`https://api.spotify.com/v1/playlists/${id}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); const tracks = meta.tracks.items; return { diff --git a/lib/routes/spotify/saved.ts b/lib/routes/spotify/saved.ts index 7aeafbc7998081..a288c36ef0cec1 100644 --- a/lib/routes/spotify/saved.ts +++ b/lib/routes/spotify/saved.ts @@ -1,7 +1,7 @@ import { Route } from '@/types'; import utils from './utils'; -import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; export const route: Route = { path: '/saved/:limit?', @@ -47,13 +47,12 @@ async function handler(ctx) { const limit = ctx.req.param('limit'); const pageSize = isNaN(Number.parseInt(limit)) ? 50 : Number.parseInt(limit); - const itemsResponse = await got - .get(`https://api.spotify.com/v1/me/tracks?limit=${pageSize}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .json(); + const itemsResponse = await ofetch(`https://api.spotify.com/v1/me/tracks?limit=${pageSize}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); const tracks = itemsResponse.items; return { diff --git a/lib/routes/spotify/show.ts b/lib/routes/spotify/show.ts index 8302fa48460357..352b058c21f867 100644 --- a/lib/routes/spotify/show.ts +++ b/lib/routes/spotify/show.ts @@ -1,11 +1,12 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import utils from './utils'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/show/:id', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Audios, example: '/spotify/show/5CfCWKI5pZ28U0uOzXkDHe', parameters: { id: 'Show ID' }, features: { @@ -30,8 +31,8 @@ export const route: Route = { source: ['open.spotify.com/show/:id'], }, ], - name: 'Show', - maintainers: ['caiohsramos'], + name: 'Show/Podcasts', + maintainers: ['caiohsramos', 'pseudoyu'], handler, }; @@ -39,13 +40,12 @@ async function handler(ctx) { const token = await utils.getPublicToken(); const id = ctx.req.param('id'); - const meta = await got - .get(`https://api.spotify.com/v1/shows/${id}?market=US`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .json(); + const meta = await ofetch(`https://api.spotify.com/v1/shows/${id}?market=US`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); const episodes = meta.episodes.items; @@ -58,13 +58,13 @@ async function handler(ctx) { itunes_category: meta.type, itunes_explicit: meta.explicit, allowEmpty: true, - item: episodes.map((x) => ({ + item: episodes.filter(Boolean).map((x) => ({ title: x.name, description: x.html_description, pubDate: parseDate(x.release_date), link: x.external_urls.spotify, itunes_item_image: x.images[0].url, - itunes_duration: x.duration_ms * 1000, + itunes_duration: x.duration_ms / 1000, enclosure_url: x.audio_preview_url, enclosure_type: 'audio/mpeg', })), diff --git a/lib/routes/spotify/tracks-top.ts b/lib/routes/spotify/tracks-top.ts index 3ab80540b1178b..c9b732bba43f2b 100644 --- a/lib/routes/spotify/tracks-top.ts +++ b/lib/routes/spotify/tracks-top.ts @@ -1,6 +1,6 @@ import { Route } from '@/types'; import utils from './utils'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; export const route: Route = { path: '/top/tracks', @@ -41,13 +41,12 @@ export const route: Route = { async function handler() { const token = await utils.getPrivateToken(); - const itemsResponse = await got - .get(`https://api.spotify.com/v1/me/top/tracks`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .json(); + const itemsResponse = await ofetch(`https://api.spotify.com/v1/me/top/tracks`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); const items = itemsResponse.items; return { diff --git a/lib/routes/spotify/utils.ts b/lib/routes/spotify/utils.ts index ea460fba9fce45..d2777414fb2959 100644 --- a/lib/routes/spotify/utils.ts +++ b/lib/routes/spotify/utils.ts @@ -1,6 +1,6 @@ import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; // Token used to retrieve public information. async function getPublicToken() { @@ -10,16 +10,16 @@ async function getPublicToken() { const { clientId, clientSecret } = config.spotify; - const tokenResponse = await got - .post('https://accounts.spotify.com/api/token', { - headers: { - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, - }, - form: { - grant_type: 'client_credentials', - }, - }) - .json(); + const tokenResponse = await ofetch('https://accounts.spotify.com/api/token', { + method: 'POST', + headers: { + Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'client_credentials', + }).toString(), + }); return tokenResponse.access_token; } @@ -32,17 +32,17 @@ async function getPrivateToken() { const { clientId, clientSecret, refreshToken } = config.spotify; - const tokenResponse = await got - .post('https://accounts.spotify.com/api/token', { - headers: { - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, - }, - form: { - grant_type: 'refresh_token', - refresh_token: refreshToken, - }, - }) - .json(); + const tokenResponse = await ofetch('https://accounts.spotify.com/api/token', { + method: 'POST', + headers: { + Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }).toString(), + }); return tokenResponse.access_token; } diff --git a/lib/routes/springer/journal.ts b/lib/routes/springer/journal.ts index 6a6a24284438cc..e70cf16db584eb 100644 --- a/lib/routes/springer/journal.ts +++ b/lib/routes/springer/journal.ts @@ -30,35 +30,34 @@ export const route: Route = { }, ], name: 'Journal', - maintainers: ['Derekmini', 'TonyRL'], + maintainers: ['Derekmini', 'TonyRL', 'xiahaoyun'], handler, }; async function handler(ctx) { - const host = 'https://www.springer.com'; + const host = 'https://link.springer.com'; const journal = ctx.req.param('journal'); - const jrnlUrl = `${host}/journal/${journal}`; + const jrnlUrl = `${host}/journal/${journal}/volumes-and-issues`; const response = await got(jrnlUrl, { cookieJar, }); const $ = load(response.data); - const jrnlName = $('h1#journalTitle').text().trim(); - const issueUrl = $('p.c-card__title.u-mb-16.u-flex-grow').find('a').attr('href'); + const jrnlName = $('span.app-journal-masthead__title').text().trim(); + const issueUrl = `${host}${$('li.c-list-group__item:first-of-type').find('a').attr('href')}`; const response2 = await got(issueUrl, { cookieJar, }); const $2 = load(response2.data); - const issue = $2('.app-volumes-and-issues__info').find('h1').text(); - const list = $2('article.c-card') + const issue = $2('h2.app-journal-latest-issue__heading').text(); + const list = $2('ol.u-list-reset > li') .map((_, item) => { - const title = $(item).find('.c-card__title').text().trim(); - const link = $(item).find('a').attr('href'); + const title = $(item).find('h3.app-card-open__heading').find('a').text().trim(); + const link = $(item).find('h3.app-card-open__heading').find('a').attr('href'); const doi = link.replace('https://link.springer.com/article/', ''); const img = $(item).find('img').attr('src'); const authors = $(item) - .find('.c-author-list') .find('li') .map((_, item) => $(item).text().trim()) .get() @@ -85,8 +84,7 @@ async function handler(ctx) { cookieJar, }); const $3 = load(response3.data); - $3('.c-article__sub-heading').remove(); - item.abstract = $3('div#Abs1-content').text(); + item.abstract = $3('div#Abs1-content > p:first-of-type').text(); item.description = renderDesc(item); return item; }) diff --git a/lib/routes/springer/namespace.ts b/lib/routes/springer/namespace.ts index 311d8908d3599c..8595db42d8813b 100644 --- a/lib/routes/springer/namespace.ts +++ b/lib/routes/springer/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Springer', url: 'www.springer.com', + lang: 'en', }; diff --git a/lib/routes/sputniknews/index.ts b/lib/routes/sputniknews/index.ts index 0e0f1e41d9f169..af31a4996220d5 100644 --- a/lib/routes/sputniknews/index.ts +++ b/lib/routes/sputniknews/index.ts @@ -55,50 +55,50 @@ export const route: Route = { handler, description: `Categories for International site: - | WORLD | COVID-19 | BUSINESS | SPORT | TECH | OPINION | - | ----- | -------- | -------- | ----- | ---- | ------- | - | world | covid-19 | business | sport | tech | opinion | +| WORLD | COVID-19 | BUSINESS | SPORT | TECH | OPINION | +| ----- | -------- | -------- | ----- | ---- | ------- | +| world | covid-19 | business | sport | tech | opinion | Categories for Chinese site: - | 新闻 | 中国 | 俄罗斯 | 国际 | 俄中关系 | 评论 | - | ---- | ----- | ------ | --------------- | ------------------------ | ------- | - | news | china | russia | category\_guoji | russia\_china\_relations | opinion | +| 新闻 | 中国 | 俄罗斯 | 国际 | 俄中关系 | 评论 | +| ---- | ----- | ------ | --------------- | ------------------------ | ------- | +| news | china | russia | category\_guoji | russia\_china\_relations | opinion | Language - | Language | Id | - | ----------- | ----------- | - | English | english | - | Spanish | spanish | - | German | german | - | French | french | - | Greek | greek | - | Italian | italian | - | Czech | czech | - | Polish | polish | - | Serbian | serbian | - | Latvian | latvian | - | Lithuanian | lithuanian | - | Moldavian | moldavian | - | Belarusian | belarusian | - | Armenian | armenian | - | Abkhaz | abkhaz | - | Ssetian | ssetian | - | Georgian | georgian | - | Azerbaijani | azerbaijani | - | Arabic | arabic | - | Turkish | turkish | - | Persian | persian | - | Dari | dari | - | Kazakh | kazakh | - | Kyrgyz | kyrgyz | - | Uzbek | uzbek | - | Tajik | tajik | - | Vietnamese | vietnamese | - | Japanese | japanese | - | Chinese | chinese | - | Portuguese | portuguese |`, +| Language | Id | +| ----------- | ----------- | +| English | english | +| Spanish | spanish | +| German | german | +| French | french | +| Greek | greek | +| Italian | italian | +| Czech | czech | +| Polish | polish | +| Serbian | serbian | +| Latvian | latvian | +| Lithuanian | lithuanian | +| Moldavian | moldavian | +| Belarusian | belarusian | +| Armenian | armenian | +| Abkhaz | abkhaz | +| Ssetian | ssetian | +| Georgian | georgian | +| Azerbaijani | azerbaijani | +| Arabic | arabic | +| Turkish | turkish | +| Persian | persian | +| Dari | dari | +| Kazakh | kazakh | +| Kyrgyz | kyrgyz | +| Uzbek | uzbek | +| Tajik | tajik | +| Vietnamese | vietnamese | +| Japanese | japanese | +| Chinese | chinese | +| Portuguese | portuguese |`, }; async function handler(ctx) { diff --git a/lib/routes/sputniknews/namespace.ts b/lib/routes/sputniknews/namespace.ts index 762bce7a97f343..b2d149812ab3c0 100644 --- a/lib/routes/sputniknews/namespace.ts +++ b/lib/routes/sputniknews/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Sputnik News 俄罗斯卫星通讯社', url: 'sputniknews.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sqmc/namespace.ts b/lib/routes/sqmc/namespace.ts index a68bc4c612cd99..6af5b8a56f620a 100644 --- a/lib/routes/sqmc/namespace.ts +++ b/lib/routes/sqmc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '新乡医学院三全学院', url: 'sqmc.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sqmc/www.ts b/lib/routes/sqmc/www.ts index 5d695ebdac3bd0..26cc58539fd47c 100644 --- a/lib/routes/sqmc/www.ts +++ b/lib/routes/sqmc/www.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['nyaShine'], handler, description: `| 学校要闻 | 通知 | 学术讲座 | 基层风采书院 | 基层风采院系 | 外媒报道 | 三全学院报 | - | -------- | ---- | -------- | ------------ | ------------ | -------- | ---------- | - | 3157 | 3187 | 3188 | 3185 | 3186 | 3199 | 3200 |`, +| -------- | ---- | -------- | ------------ | ------------ | -------- | ---------- | +| 3157 | 3187 | 3188 | 3185 | 3186 | 3199 | 3200 |`, }; async function handler(ctx) { diff --git a/lib/routes/sse/lawandrules.ts b/lib/routes/sse/lawandrules.ts deleted file mode 100644 index 72f17adc5bc310..00000000000000 --- a/lib/routes/sse/lawandrules.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: '/lawandrules/:slug?', - categories: ['finance'], - example: '/sse/lawandrules', - parameters: { slug: '见下文,默认为 `latest`' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - name: '本所业务指南与流程', - maintainers: ['nczitzk'], - handler, - description: `将目标栏目的网址拆解为 \`https://www.sse.com.cn/lawandrules/guide/\` 和后面的字段,把后面的字段中的 \`/\` 替换为 \`-\`,即为该路由的 slug - - 如:(最新指南与流程)\`https://www.sse.com.cn/lawandrules/guide/latest\` 的网址在 \`https://www.sse.com.cn/lawandrules/guide/\` 后的字段是 \`latest\`,则对应的 slug 为 \`latest\`,对应的路由即为 \`/sse/lawandrules/latest\` - - 又如:(主板业务指南与流程 - 发行承销业务指南)\`https://www.sse.com.cn/lawandrules/guide/zbywznylc/fxcxywzn\` 的网址在 \`https://www.sse.com.cn/lawandrules/guide/\` 后的字段是 \`zbywznylc/fxcxywzn\`,则对应的 slug 为 \`zbywznylc-fxcxywzn\`,对应的路由即为 \`/sse/lawandrules/zbywznylc-fxcxywzn\``, -}; - -async function handler(ctx) { - const slug = ctx.req.param('slug') ?? 'latest'; - - const rootUrl = 'https://www.sse.com.cn'; - const currentUrl = `${rootUrl}/lawandrules/guide/${slug.replaceAll('-', '/')}`; - const response = await got(currentUrl); - - const $ = load(response.data); - - const list = $('.sse_list_1 dl dd') - .toArray() - .map((item) => { - item = $(item); - return { - title: item.find('a').attr('title'), - link: `${rootUrl}${item.find('a').attr('href')}`, - pubDate: parseDate(item.find('span').text().trim()), - }; - }); - - const items = await Promise.all( - list.map((item) => - cache.tryGet(item.link, async () => { - const detailResponse = await got(item.link); - const content = load(detailResponse.data); - - item.description = content('.allZoom').html(); - - return item; - }) - ) - ); - - return { - title: $('title').text(), - link: currentUrl, - item: items, - }; -} diff --git a/lib/routes/sse/namespace.ts b/lib/routes/sse/namespace.ts index 3bff470ebbca55..2067936b1895da 100644 --- a/lib/routes/sse/namespace.ts +++ b/lib/routes/sse/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海证券交易所', url: 'bond.sse.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sse/sselawsrules.ts b/lib/routes/sse/sselawsrules.ts new file mode 100644 index 00000000000000..a33e3989eed725 --- /dev/null +++ b/lib/routes/sse/sselawsrules.ts @@ -0,0 +1,370 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; + +export const handler = async (ctx) => { + const { category = 'latest' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + + const rootUrl = 'https://www.sse.com.cn'; + const currentUrl = new URL(`lawandrules/sselawsrules/${category}`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + let items = $('div#sse_list_1 dl dd') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + return { + title: item.find('a').text().trim(), + pubDate: parseDate(item.find('span').text().trim()), + link: new URL(item.find('a').prop('href'), rootUrl).href, + }; + }); + + 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.article-infor h2').text().trim(); + const description = $$('div.allZoom').html(); + + item.title = title; + item.description = description; + item.pubDate = parseDate($$('div.article_opt i').text().trim()); + item.author = $$('meta[name="author"]').prop('content'); + item.content = { + html: description, + text: $$('div.allZoom').text(), + }; + item.updated = $$('meta[name="others"]').prop('content') + ? timezone( + parseDate( + $$('meta[name="others"]') + .prop('content') + .split(/时间\s/) + .pop() + ), + +8 + ) + : undefined; + + return item; + }) + ) + ); + + const image = new URL($('img.sse_logo').prop('content'), rootUrl).href; + + return { + title: $('title').text(), + description: $('meta[name="description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $('meta[name="author"]').prop('content'), + }; +}; + +export const route: Route = { + path: '/sselawsrules/:category{.+}?', + name: '本所业务规则', + url: 'www.sse.com.cn', + maintainers: ['nczitzk'], + handler, + example: '/sse/sselawsrules/latest', + parameters: { category: '分类,默认为最新规则,即 `latest`,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [最新规则](https://www.sse.com.cn/lawandrules/sselawsrules/latest/),网址为 \`https://www.sse.com.cn/lawandrules/sselawsrules/latest/\`。截取 \`https://www.sse.com.cn/lawandrules/sselawsrules/\` 到末尾 \`/\` 的部分 \`latest\` 作为参数填入,此时路由为 [\`/sse/sselawsrules/latest\`](https://rsshub.app/sse/sselawsrules/latest)。 +::: + +| [最新规则](https://www.sse.com.cn/lawandrules/sselawsrules/latest/) | [章程](https://www.sse.com.cn/lawandrules/sselawsrules/article/) | [首发](https://www.sse.com.cn/lawandrules/sselawsrules/stocks/review/firstepisode/) | [再融资](https://www.sse.com.cn/lawandrules/sselawsrules/stocks/review/refinancing/) | [重组](https://www.sse.com.cn/lawandrules/sselawsrules/stocks/review/recombination/) | +| ------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| [latest](https://rsshub.app/sse/sselawsrules/latest) | [article](https://rsshub.app/sse/sselawsrules/article) | [stocks/review/firstepisode](https://rsshub.app/sse/sselawsrules/stocks/review/firstepisode) | [stocks/review/refinancing](https://rsshub.app/sse/sselawsrules/stocks/review/refinancing) | [stocks/review/recombination](https://rsshub.app/sse/sselawsrules/stocks/review/recombination) | + +| [转板](https://www.sse.com.cn/lawandrules/sselawsrules/stocks/review/flap/) | [发行承销](https://www.sse.com.cn/lawandrules/sselawsrules/stocks/issue/) | [主板上市(挂牌)](https://www.sse.com.cn/lawandrules/sselawsrules/stocks/mainipo/) | [科创板上市(挂牌)](https://www.sse.com.cn/lawandrules/sselawsrules/stocks/staripo/) | [股票交易](https://www.sse.com.cn/lawandrules/sselawsrules/stocks/exchange/) | +| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| [stocks/review/flap](https://rsshub.app/sse/sselawsrules/stocks/review/flap) | [stocks/issue](https://rsshub.app/sse/sselawsrules/stocks/issue) | [stocks/mainipo](https://rsshub.app/sse/sselawsrules/stocks/mainipo) | [stocks/staripo](https://rsshub.app/sse/sselawsrules/stocks/staripo) | [stocks/exchange](https://rsshub.app/sse/sselawsrules/stocks/exchange) | + +| [试点创新企业](https://www.sse.com.cn/lawandrules/sselawsrules/stocks/innovative/) | [股权分置改革](https://www.sse.com.cn/lawandrules/sselawsrules/stocks/reform/) | [发行上市审核](https://www.sse.com.cn/lawandrules/sselawsrules/bond/review/) | [发行承销](https://www.sse.com.cn/lawandrules/sselawsrules/bond/issue/) | [公司债券上市(挂牌)](https://www.sse.com.cn/lawandrules/sselawsrules/bond/listing/corporatebond/) | +| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| [stocks/innovative](https://rsshub.app/sse/sselawsrules/stocks/innovative) | [stocks/reform](https://rsshub.app/sse/sselawsrules/stocks/reform) | [bond/review](https://rsshub.app/sse/sselawsrules/bond/review) | [bond/issue](https://rsshub.app/sse/sselawsrules/bond/issue) | [bond/listing/corporatebond](https://rsshub.app/sse/sselawsrules/bond/listing/corporatebond) | + +| [资产支持证券上市(挂牌)](https://www.sse.com.cn/lawandrules/sselawsrules/bond/listing/assets/) | [债券交易通用](https://www.sse.com.cn/lawandrules/sselawsrules/bond/trading/currency/) | [国债预发行](https://www.sse.com.cn/lawandrules/sselawsrules/bond/trading/tbondp/) | [债券质押式三方回购](https://www.sse.com.cn/lawandrules/sselawsrules/bond/trading/tripartyrepo/) | [债券质押式协议回购](https://www.sse.com.cn/lawandrules/sselawsrules/bond/trading/repurchase/) | +| ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| [bond/listing/assets](https://rsshub.app/sse/sselawsrules/bond/listing/assets) | [bond/trading/currency](https://rsshub.app/sse/sselawsrules/bond/trading/currency) | [bond/trading/tbondp](https://rsshub.app/sse/sselawsrules/bond/trading/tbondp) | [bond/trading/tripartyrepo](https://rsshub.app/sse/sselawsrules/bond/trading/tripartyrepo) | [bond/trading/repurchase](https://rsshub.app/sse/sselawsrules/bond/trading/repurchase) | + +| [国债买断式回购交易](https://www.sse.com.cn/lawandrules/sselawsrules/bond/trading/outrightrepo/) | [信用保护工具](https://www.sse.com.cn/lawandrules/sselawsrules/bond/trading/cdx/) | [上市公司可转债](https://www.sse.com.cn/lawandrules/sselawsrules/bond/convertible/) | [基金上市](https://www.sse.com.cn/lawandrules/sselawsrules/fund/listing/) | [基金交易](https://www.sse.com.cn/lawandrules/sselawsrules/fund/trading/) | +| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| [bond/trading/outrightrepo](https://rsshub.app/sse/sselawsrules/bond/trading/outrightrepo) | [bond/trading/cdx](https://rsshub.app/sse/sselawsrules/bond/trading/cdx) | [bond/convertible](https://rsshub.app/sse/sselawsrules/bond/convertible) | [fund/listing](https://rsshub.app/sse/sselawsrules/fund/listing) | [fund/trading](https://rsshub.app/sse/sselawsrules/fund/trading) | + +| [基础设施公募REITs](https://www.sse.com.cn/lawandrules/sselawsrules/reits/) | [期权](https://www.sse.com.cn/lawandrules/sselawsrules/option/) | [通用类](https://www.sse.com.cn/lawandrules/sselawsrules/trade/universal/) | [融资融券](https://www.sse.com.cn/lawandrules/sselawsrules/trade/specific/margin/) | [转融通](https://www.sse.com.cn/lawandrules/sselawsrules/trade/specific/refinancing/) | +| --------------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| [reits](https://rsshub.app/sse/sselawsrules/reits) | [option](https://rsshub.app/sse/sselawsrules/option) | [trade/universal](https://rsshub.app/sse/sselawsrules/trade/universal) | [trade/specific/margin](https://rsshub.app/sse/sselawsrules/trade/specific/margin) | [trade/specific/refinancing](https://rsshub.app/sse/sselawsrules/trade/specific/refinancing) | + +| [质押式回购](https://www.sse.com.cn/lawandrules/sselawsrules/trade/specific/repo/) | [质押式报价回购](https://www.sse.com.cn/lawandrules/sselawsrules/trade/specific/pricerepo/) | [约定购回](https://www.sse.com.cn/lawandrules/sselawsrules/trade/specific/promise/) | [协议转让](https://www.sse.com.cn/lawandrules/sselawsrules/trade/specific/xyzr/) | [其他](https://www.sse.com.cn/lawandrules/sselawsrules/trade/specific/others/) | +| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| [trade/specific/repo](https://rsshub.app/sse/sselawsrules/trade/specific/repo) | [trade/specific/pricerepo](https://rsshub.app/sse/sselawsrules/trade/specific/pricerepo) | [trade/specific/promise](https://rsshub.app/sse/sselawsrules/trade/specific/promise) | [trade/specific/xyzr](https://rsshub.app/sse/sselawsrules/trade/specific/xyzr) | [trade/specific/others](https://rsshub.app/sse/sselawsrules/trade/specific/others) | + +| [沪港通](https://www.sse.com.cn/lawandrules/sselawsrules/global/hkexsc/) | [互联互通存托凭证](https://www.sse.com.cn/lawandrules/sselawsrules/global/slsc/) | [会员管理](https://www.sse.com.cn/lawandrules/sselawsrules/member/personnel/) | [适当性管理](https://www.sse.com.cn/lawandrules/sselawsrules/member/adequacy/) | [纪律处分与复核](https://www.sse.com.cn/lawandrules/sselawsrules/disciplinary/) | +| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | +| [global/hkexsc](https://rsshub.app/sse/sselawsrules/global/hkexsc) | [global/slsc](https://rsshub.app/sse/sselawsrules/global/slsc) | [member/personnel](https://rsshub.app/sse/sselawsrules/member/personnel) | [member/adequacy](https://rsshub.app/sse/sselawsrules/member/adequacy) | [disciplinary](https://rsshub.app/sse/sselawsrules/disciplinary) | + +| [交易收费](https://www.sse.com.cn/lawandrules/sselawsrules/charge/) | [其他业务规则](https://www.sse.com.cn/lawandrules/sselawsrules/other/) | [业务规则废止公告](https://www.sse.com.cn/lawandrules/sserules/repeal/announcement/) | [已废止规则文本](https://www.sse.com.cn/lawandrules/sselawsrules/repeal/rules/) | +| ------------------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| [charge](https://rsshub.app/sse/sselawsrules/charge) | [other](https://rsshub.app/sse/sselawsrules/other) | [/lawandrules/sserules/repeal/announcement](https://rsshub.app/sse/sselawsrules//lawandrules/sserules/repeal/announcement) | [repeal/rules](https://rsshub.app/sse/sselawsrules/repeal/rules) | + `, + categories: ['finance'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.sse.com.cn/lawandrules/sselawsrules/:category'], + target: (params) => { + const category = params.category; + + return `/sse/sselawsrules${category ? `/${category}` : ''}`; + }, + }, + { + title: '最新规则', + source: ['www.sse.com.cn/lawandrules/sselawsrules/latest/'], + target: '/sselawsrules/latest', + }, + { + title: '章程', + source: ['www.sse.com.cn/lawandrules/sselawsrules/article/'], + target: '/sselawsrules/article', + }, + { + title: '首发', + source: ['www.sse.com.cn/lawandrules/sselawsrules/stocks/review/firstepisode/'], + target: '/sselawsrules/stocks/review/firstepisode', + }, + { + title: '再融资', + source: ['www.sse.com.cn/lawandrules/sselawsrules/stocks/review/refinancing/'], + target: '/sselawsrules/stocks/review/refinancing', + }, + { + title: '重组', + source: ['www.sse.com.cn/lawandrules/sselawsrules/stocks/review/recombination/'], + target: '/sselawsrules/stocks/review/recombination', + }, + { + title: '转板', + source: ['www.sse.com.cn/lawandrules/sselawsrules/stocks/review/flap/'], + target: '/sselawsrules/stocks/review/flap', + }, + { + title: '发行承销', + source: ['www.sse.com.cn/lawandrules/sselawsrules/stocks/issue/'], + target: '/sselawsrules/stocks/issue', + }, + { + title: '主板上市(挂牌)', + source: ['www.sse.com.cn/lawandrules/sselawsrules/stocks/mainipo/'], + target: '/sselawsrules/stocks/mainipo', + }, + { + title: '科创板上市(挂牌)', + source: ['www.sse.com.cn/lawandrules/sselawsrules/stocks/staripo/'], + target: '/sselawsrules/stocks/staripo', + }, + { + title: '股票交易', + source: ['www.sse.com.cn/lawandrules/sselawsrules/stocks/exchange/'], + target: '/sselawsrules/stocks/exchange', + }, + { + title: '试点创新企业', + source: ['www.sse.com.cn/lawandrules/sselawsrules/stocks/innovative/'], + target: '/sselawsrules/stocks/innovative', + }, + { + title: '股权分置改革', + source: ['www.sse.com.cn/lawandrules/sselawsrules/stocks/reform/'], + target: '/sselawsrules/stocks/reform', + }, + { + title: '发行上市审核', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/review/'], + target: '/sselawsrules/bond/review', + }, + { + title: '发行承销', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/issue/'], + target: '/sselawsrules/bond/issue', + }, + { + title: '公司债券上市(挂牌)', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/listing/corporatebond/'], + target: '/sselawsrules/bond/listing/corporatebond', + }, + { + title: '资产支持证券上市(挂牌)', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/listing/assets/'], + target: '/sselawsrules/bond/listing/assets', + }, + { + title: '债券交易通用', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/trading/currency/'], + target: '/sselawsrules/bond/trading/currency', + }, + { + title: '国债预发行', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/trading/tbondp/'], + target: '/sselawsrules/bond/trading/tbondp', + }, + { + title: '债券质押式三方回购', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/trading/tripartyrepo/'], + target: '/sselawsrules/bond/trading/tripartyrepo', + }, + { + title: '债券质押式协议回购', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/trading/repurchase/'], + target: '/sselawsrules/bond/trading/repurchase', + }, + { + title: '国债买断式回购交易', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/trading/outrightrepo/'], + target: '/sselawsrules/bond/trading/outrightrepo', + }, + { + title: '信用保护工具', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/trading/cdx/'], + target: '/sselawsrules/bond/trading/cdx', + }, + { + title: '上市公司可转债', + source: ['www.sse.com.cn/lawandrules/sselawsrules/bond/convertible/'], + target: '/sselawsrules/bond/convertible', + }, + { + title: '基金上市', + source: ['www.sse.com.cn/lawandrules/sselawsrules/fund/listing/'], + target: '/sselawsrules/fund/listing', + }, + { + title: '基金交易', + source: ['www.sse.com.cn/lawandrules/sselawsrules/fund/trading/'], + target: '/sselawsrules/fund/trading', + }, + { + title: '基础设施公募REITs', + source: ['www.sse.com.cn/lawandrules/sselawsrules/reits/'], + target: '/sselawsrules/reits', + }, + { + title: '期权', + source: ['www.sse.com.cn/lawandrules/sselawsrules/option/'], + target: '/sselawsrules/option', + }, + { + title: '通用类', + source: ['www.sse.com.cn/lawandrules/sselawsrules/trade/universal/'], + target: '/sselawsrules/trade/universal', + }, + { + title: '融资融券', + source: ['www.sse.com.cn/lawandrules/sselawsrules/trade/specific/margin/'], + target: '/sselawsrules/trade/specific/margin', + }, + { + title: '转融通', + source: ['www.sse.com.cn/lawandrules/sselawsrules/trade/specific/refinancing/'], + target: '/sselawsrules/trade/specific/refinancing', + }, + { + title: '质押式回购', + source: ['www.sse.com.cn/lawandrules/sselawsrules/trade/specific/repo/'], + target: '/sselawsrules/trade/specific/repo', + }, + { + title: '质押式报价回购', + source: ['www.sse.com.cn/lawandrules/sselawsrules/trade/specific/pricerepo/'], + target: '/sselawsrules/trade/specific/pricerepo', + }, + { + title: '约定购回', + source: ['www.sse.com.cn/lawandrules/sselawsrules/trade/specific/promise/'], + target: '/sselawsrules/trade/specific/promise', + }, + { + title: '协议转让', + source: ['www.sse.com.cn/lawandrules/sselawsrules/trade/specific/xyzr/'], + target: '/sselawsrules/trade/specific/xyzr', + }, + { + title: '其他', + source: ['www.sse.com.cn/lawandrules/sselawsrules/trade/specific/others/'], + target: '/sselawsrules/trade/specific/others', + }, + { + title: '沪港通', + source: ['www.sse.com.cn/lawandrules/sselawsrules/global/hkexsc/'], + target: '/sselawsrules/global/hkexsc', + }, + { + title: '互联互通存托凭证', + source: ['www.sse.com.cn/lawandrules/sselawsrules/global/slsc/'], + target: '/sselawsrules/global/slsc', + }, + { + title: '会员管理', + source: ['www.sse.com.cn/lawandrules/sselawsrules/member/personnel/'], + target: '/sselawsrules/member/personnel', + }, + { + title: '适当性管理', + source: ['www.sse.com.cn/lawandrules/sselawsrules/member/adequacy/'], + target: '/sselawsrules/member/adequacy', + }, + { + title: '纪律处分与复核', + source: ['www.sse.com.cn/lawandrules/sselawsrules/disciplinary/'], + target: '/sselawsrules/disciplinary', + }, + { + title: '交易收费', + source: ['www.sse.com.cn/lawandrules/sselawsrules/charge/'], + target: '/sselawsrules/charge', + }, + { + title: '其他业务规则', + source: ['www.sse.com.cn/lawandrules/sselawsrules/other/'], + target: '/sselawsrules/other', + }, + { + title: '业务规则废止公告', + source: ['www.sse.com.cn/lawandrules/sserules/repeal/announcement/'], + target: '/sselawsrules//lawandrules/sserules/repeal/announcement', + }, + { + title: '已废止规则文本', + source: ['www.sse.com.cn/lawandrules/sselawsrules/repeal/rules/'], + target: '/sselawsrules/repeal/rules', + }, + ], +}; diff --git a/lib/routes/ssm/namespace.ts b/lib/routes/ssm/namespace.ts index 58c02046253795..28ec877bd8c5e5 100644 --- a/lib/routes/ssm/namespace.ts +++ b/lib/routes/ssm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '澳门卫生局', url: 'www.ssm.gov.mo', + lang: 'zh-CN', }; diff --git a/lib/routes/sspai/activity.ts b/lib/routes/sspai/activity.ts index bb1855d5ac4292..8e8e816a092862 100644 --- a/lib/routes/sspai/activity.ts +++ b/lib/routes/sspai/activity.ts @@ -4,7 +4,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/activity/:slug', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/activity/urfp0d9i', parameters: { slug: '作者 slug,可在作者主页URL中找到' }, features: { diff --git a/lib/routes/sspai/author.ts b/lib/routes/sspai/author.ts index fced3b0cfee885..2d24ed2b920a9c 100644 --- a/lib/routes/sspai/author.ts +++ b/lib/routes/sspai/author.ts @@ -22,7 +22,7 @@ async function getUserId(slug) { export const route: Route = { path: '/author/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/author/796518', parameters: { id: '作者 slug 或 id,slug 可在作者主页URL中找到,id 不易查找,仅作兼容' }, features: { @@ -55,13 +55,19 @@ async function handler(ctx) { const author_nickname = data[0].author.nickname; const items = await Promise.all( data.map((item) => { - const link = `https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second`; + const link = `https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second&support_webp=true`; let description = ''; const key = `sspai: ${item.id}`; return cache.tryGet(key, async () => { const response = await got(link); - description = response.data.data.body; + // description = response.data.data.body; + const articleData = response.data.data; + const banner = articleData.promote_image; + if (banner) { + description = `<img src="${banner}" alt="Article Cover Image" style="display: block; margin: 0 auto;"><br>`; + } + description += articleData.body; return { title: item.title.trim(), diff --git a/lib/routes/sspai/bookmarks.ts b/lib/routes/sspai/bookmarks.ts index 5fba4ec04a3344..095d16945a470b 100644 --- a/lib/routes/sspai/bookmarks.ts +++ b/lib/routes/sspai/bookmarks.ts @@ -4,7 +4,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/bookmarks/:slug', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/bookmarks/urfp0d9i', parameters: { slug: '用户 slug,可在个人主页URL中找到' }, features: { diff --git a/lib/routes/sspai/column.ts b/lib/routes/sspai/column.ts index 63e27c34baed8f..97ea30c84f05fd 100644 --- a/lib/routes/sspai/column.ts +++ b/lib/routes/sspai/column.ts @@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/column/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/column/262', parameters: { id: '专栏 id' }, features: { @@ -58,7 +58,7 @@ async function handler(ctx) { list.map((item) => { const title = item.title; const date = item.created_at; - const link = `https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second`; + const link = `https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second&support_webp=true`; const itemUrl = `https://sspai.com/post/${item.id}`; const author = item.author.nickname; diff --git a/lib/routes/sspai/index.ts b/lib/routes/sspai/index.ts index 00eb6070edde45..18813cfd0009f9 100644 --- a/lib/routes/sspai/index.ts +++ b/lib/routes/sspai/index.ts @@ -1,11 +1,12 @@ -import { Route } from '@/types'; +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'], + categories: ['new-media', 'popular'], + view: ViewType.Articles, example: '/sspai/index', parameters: {}, features: { @@ -35,13 +36,18 @@ async function handler() { }); const items = await Promise.all( resp.data.data.map((item) => { - const link = `https://sspai.com/api/v1/${item.slug ? `member/article/single/info/get?slug=${item.slug}` : `article/info/get?id=${item.id}`}&view=second`; + const link = `https://sspai.com/api/v1/${item.slug ? `member/article/single/info/get?slug=${item.slug}` : `article/info/get?id=${item.id}`}&view=second&support_webp=true`; let description = ''; const key = `sspai: ${item.id}`; return cache.tryGet(key, async () => { const response = await got({ method: 'get', url: link }); - description = response.data.data.body; + const articleData = response.data.data; + const banner = articleData.promote_image; + if (banner) { + description = `<img src="${banner}" alt="Article Cover Image" style="display: block; margin: 0 auto;"><br>`; + } + description += articleData.body; return { title: item.title.trim(), @@ -55,9 +61,9 @@ async function handler() { ); return { - title: '少数派 -- 首页', + title: '少数派', link: 'https://sspai.com', - description: '少数派 -- 首页', + description: '少数派首页', item: items, }; } diff --git a/lib/routes/sspai/matrix.ts b/lib/routes/sspai/matrix.ts index 2ade91768fdb70..f6e6451582045c 100644 --- a/lib/routes/sspai/matrix.ts +++ b/lib/routes/sspai/matrix.ts @@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/matrix', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/matrix', parameters: {}, features: { @@ -36,13 +36,19 @@ async function handler() { const data = resp.data.list; const items = await Promise.all( data.map((item) => { - const link = `https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second`; + const link = `https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second&support_webp=true`; let description = ''; const key = `sspai: ${item.id}`; return cache.tryGet(key, async () => { const response = await got(link); - description = response.data.data.body; + // description = response.data.data.body; + const articleData = response.data.data; + const banner = articleData.promote_image; + if (banner) { + description = `<img src="${banner}" alt="Article Cover Image" style="display: block; margin: 0 auto;"><br>`; + } + description += articleData.body; return { title: item.title.trim(), diff --git a/lib/routes/sspai/namespace.ts b/lib/routes/sspai/namespace.ts index f06919315b2e2c..eb2d046a0d97db 100644 --- a/lib/routes/sspai/namespace.ts +++ b/lib/routes/sspai/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '少数派 sspai', url: 'sspai.com', + lang: 'zh-CN', }; diff --git a/lib/routes/sspai/prime-community.ts b/lib/routes/sspai/prime-community.ts new file mode 100644 index 00000000000000..86c332aeb45e8b --- /dev/null +++ b/lib/routes/sspai/prime-community.ts @@ -0,0 +1,83 @@ +import { config } from '@/config'; +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/prime/community', + categories: ['new-media'], + example: '/sspai/prime/community', + features: { + requireConfig: [ + { + name: 'SSPAI_BEARERTOKEN', + optional: false, + description: `少数派会员账号认证 token。获取方式:登陆后打开少数派会员社区界面,打开浏览器开发者工具中 “网络”(Network) 选项卡,筛选 URL 找到任一个地址为 \`sspai.com/api\` 开头的请求,点击检查其 “消息头”,在 “请求头” 中找到Authorization字段,将其值复制填入配置即可。你的配置应该形如 \`SSPAI_BEARERTOKEN: 'Bearer eyJxxxx......xx_U8'\`。`, + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['sspai.com/community'], + }, + ], + name: '会员社区', + maintainers: ['mintyfrankie'], + handler, +}; + +const TOKEN = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjcyNzU3NiIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNzQ4NTI2MDEzfQ.di8RB-lxHI_JMBQHFd2xhcpk6Zd_3bvfQlAti6HAuZA'; + +async function handler() { + let token; + const cacheIn = await cache.get('sspai:token'); + + if (cacheIn) { + token = cacheIn; + } else if (config.sspai.bearertoken) { + token = config.sspai.bearertoken; + cache.set('sspai:token', config.sspai.bearertoken); + } else { + token = TOKEN; + } + + const feedEndpoint = 'https://sspai.com/api/v1/community/page/get'; + const headers = { + Authorization: token, + }; + + const response = await ofetch(feedEndpoint, { headers }); + const list = response.data.map((item) => ({ + title: item.title, + link: `https://sspai.com/t/${item.id_hash}`, + pubDate: new Date(item.created_at * 1000), + author: item.author.nickname, + category: item.channel.title, + id_hash: item.id_hash, + })); + + // FIXME: TypeError: Cannot read properties of null (reading 'body') + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const postEndpoint = `https://sspai.com/api/v1/community/topic/single/info/get?id_hash=${item.id_hash}`; + const response = await ofetch(postEndpoint, { headers }); + item.description = response.data.body || 'No content'; + return item; + }) + ) + ); + + return { + title: '少数派会员社区', + link: 'https://sspai.com/community', + lang: 'zh-CN', + description: '少数派会员社区', + item: items, + }; +} diff --git a/lib/routes/sspai/series-update.ts b/lib/routes/sspai/series-update.ts index a0626a7008826f..448578c0274920 100644 --- a/lib/routes/sspai/series-update.ts +++ b/lib/routes/sspai/series-update.ts @@ -4,7 +4,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/series/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/series/77', parameters: { id: '专栏 id' }, features: { @@ -36,7 +36,7 @@ async function handler(ctx) { response.data.data.map(async (item) => { let description = ''; if (item.probation) { - const res = await got(`https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second`); + const res = await got(`https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second&support_webp=true`); description = res.data.data.body; } else { description = `<img src="https://cdn.sspai.com/${item.banner}">`; diff --git a/lib/routes/sspai/series.ts b/lib/routes/sspai/series.ts index 0c930f77fb11e7..ef1f208a943ed2 100644 --- a/lib/routes/sspai/series.ts +++ b/lib/routes/sspai/series.ts @@ -5,7 +5,7 @@ import { load } from 'cheerio'; export const route: Route = { path: '/series', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/series', parameters: {}, features: { diff --git a/lib/routes/sspai/shortcuts-gallery.ts b/lib/routes/sspai/shortcuts-gallery.ts index 751fde0dee28c1..eafe47a9d58fd9 100644 --- a/lib/routes/sspai/shortcuts-gallery.ts +++ b/lib/routes/sspai/shortcuts-gallery.ts @@ -4,7 +4,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/shortcuts', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/shortcuts', parameters: {}, features: { diff --git a/lib/routes/sspai/tag.ts b/lib/routes/sspai/tag.ts index 5e4c40e7f93932..aff876c94b9226 100644 --- a/lib/routes/sspai/tag.ts +++ b/lib/routes/sspai/tag.ts @@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/tag/:keyword', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/tag/apple', parameters: { keyword: '关键词' }, features: { @@ -41,12 +41,18 @@ async function handler(ctx) { const data = resp.data.list; const items = await Promise.all( data.map((item) => { - const link = `https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second`; + const link = `https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second&support_webp=true`; let description; const key = `sspai: ${item.id}`; return cache.tryGet(key, async () => { const response = await got({ method: 'get', url: link, headers: { Referer: host } }); - description = response.data.data.body; + // description = response.data.data.body; + const articleData = response.data.data; + const banner = articleData.promote_image; + if (banner) { + description = `<img src="${banner}" alt="Article Cover Image" style="display: block; margin: 0 auto;"><br>`; + } + description += articleData.body; return { title: item.title.trim(), diff --git a/lib/routes/sspai/topic.ts b/lib/routes/sspai/topic.ts index 5ca450d5e35aa6..292cd51e51f203 100644 --- a/lib/routes/sspai/topic.ts +++ b/lib/routes/sspai/topic.ts @@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/topic/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/topic/250', parameters: { id: '专题 id,可在专题主页URL中找到' }, features: { @@ -41,7 +41,7 @@ async function handler(ctx) { list.map((item) => { const title = item.title; const date = item.created_at; - const link = `https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second`; + const link = `https://sspai.com/api/v1/article/info/get?id=${item.id}&view=second&support_webp=true`; const itemUrl = `https://sspai.com/post/${item.id}`; const author = item.author.nickname; @@ -52,7 +52,13 @@ async function handler(ctx) { } return cache.tryGet(`sspai: ${item.id}`, async () => { const response = await got(link); - const description = response.data.data.body; + let description = ''; + const articleData = response.data.data; + const banner = articleData.promote_image; + if (banner) { + description = `<img src="${banner}" alt="Article Cover Image" style="display: block; margin: 0 auto;"><br>`; + } + description += articleData.body; const single = { title, diff --git a/lib/routes/sspai/topics.ts b/lib/routes/sspai/topics.ts index c23b96df72588a..5bdd6a9101874d 100644 --- a/lib/routes/sspai/topics.ts +++ b/lib/routes/sspai/topics.ts @@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/topics', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/sspai/topics', parameters: {}, features: { @@ -42,7 +42,7 @@ async function handler() { const key = `sspai:topics:${item.id}`; return cache.tryGet(key, () => { - description = `${item.intro}<br><img src="https://cdn.sspai.com/${item.banner}" /><br>如有兴趣,请复制链接订阅 <br> <h3>https://rsshub.app/sspai/topic/${item.id}</h3>`; + description = `<br><img src="https://cdnfile.sspai.com/${item.banner}" alt="Article Cover Image" style="display: block; margin: 0 auto;"/>${item.intro}<br>如有兴趣,请复制链接订阅 <br> <h3>https://rsshub.app/sspai/topic/${item.id}</h3>`; return { title: item.title.trim(), diff --git a/lib/routes/sspu/namespace.ts b/lib/routes/sspu/namespace.ts index f9e1516ea9f49b..dda02542acea0e 100644 --- a/lib/routes/sspu/namespace.ts +++ b/lib/routes/sspu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '上海第二工业大学', url: 'jwc.sspu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/startuplatte/index.ts b/lib/routes/startuplatte/index.ts index 731db7e491f988..23712e79b18285 100644 --- a/lib/routes/startuplatte/index.ts +++ b/lib/routes/startuplatte/index.ts @@ -6,7 +6,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/:category?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/startuplatte', parameters: { category: '分类,见下表,默认为首頁' }, features: { @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 首頁 | 大師智慧 | 深度分析 | 新知介紹 | - | ---- | -------- | -------- | -------- | - | | quote | analysis | trend |`, +| ---- | -------- | -------- | -------- | +| | quote | analysis | trend |`, }; async function handler(ctx) { diff --git a/lib/routes/startuplatte/namespace.ts b/lib/routes/startuplatte/namespace.ts index 4d961bf5c05bb7..85dabe0c4a6c5a 100644 --- a/lib/routes/startuplatte/namespace.ts +++ b/lib/routes/startuplatte/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '創新拿鐵', url: 'startuplatte.com', + lang: 'zh-TW', }; diff --git a/lib/routes/stbu/jsjxy.ts b/lib/routes/stbu/jsjxy.ts index 2f4a8333f8187d..669432e6249466 100644 --- a/lib/routes/stbu/jsjxy.ts +++ b/lib/routes/stbu/jsjxy.ts @@ -22,14 +22,17 @@ export const route: Route = { }, radar: [ { - source: ['jsjxy.stbu.edu.cn/news', 'jsjxy.stbu.edu.cn', 'stbu.edu.cn'], + source: ['jsjxy.stbu.edu.cn/news', 'jsjxy.stbu.edu.cn'], + }, + { + source: ['stbu.edu.cn'], }, ], name: '计算机学院 - 通知公告', maintainers: ['HyperCherry'], handler, url: 'jsjxy.stbu.edu.cn/news', - description: `:::warning + description: `::: warning 计算机学院通知公告疑似禁止了非大陆 IP 访问,使用路由需要自行 [部署](https://docs.rsshub.app/deploy/)。 :::`, }; diff --git a/lib/routes/stbu/namespace.ts b/lib/routes/stbu/namespace.ts index 5fca5944e89cea..4ba9c5ee6d905a 100644 --- a/lib/routes/stbu/namespace.ts +++ b/lib/routes/stbu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '四川工商学院', url: 'stbu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/stcn/index.ts b/lib/routes/stcn/index.ts index 1622828767c040..86c058bf07f36e 100644 --- a/lib/routes/stcn/index.ts +++ b/lib/routes/stcn/index.ts @@ -1,119 +1,264 @@ -import { Route } from '@/types'; +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import timezone from '@/utils/timezone'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; -export const route: Route = { - path: '/:id?', - categories: ['finance'], - example: '/stcn/yw', - parameters: { id: '栏目 id,见下表,默认为要闻' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - name: '栏目', - maintainers: ['nczitzk'], - handler, - description: `| 快讯 | 要闻 | 股市 | 公司 | 数据 | - | ---- | ---- | ---- | ------- | ---- | - | kx | yw | gs | company | data | - - | 基金 | 金融 | 评论 | 产经 | 创投 | - | ---- | ------- | ------- | ---- | ---- | - | fund | finance | comment | cj | ct | +import { type CheerioAPI, type Cheerio, type Element, load } from 'cheerio'; +import { type Context } from 'hono'; - | 科创板 | 新三板 | 投教 | ESG | 滚动 | - | ------ | ------ | ---- | --- | ---- | - | kcb | xsb | tj | zk | gd | +export const handler = async (ctx: Context): Promise<Data> => { + const { id = 'yw' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); - | 股市一览 | 独家解读 | - | -------- | -------- | - | gsyl | djjd | + const baseUrl: string = 'https://www.stcn.com'; + const targetUrl: string = new URL(`article/list/${id}.html`, baseUrl).href; - | 公司新闻 | 公司动态 | - | -------- | -------- | - | gsxw | gsdt | + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'zh-CN'; - | 独家数据 | 看点数据 | 资金流向 | 科创板 | 行情总貌 | - | -------- | -------- | -------- | ------- | -------- | - | djsj | kd | zj | sj\_kcb | hq | + let items: DataItem[] = []; - | 专栏 | 作者 | - | ---- | ------ | - | zl | author | + items = $('ul.infinite-list li') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio<Element> = $(el); - | 行业 | 汽车 | - | ---- | ---- | - | cjhy | cjqc | + const $aEl: Cheerio<Element> = $el.find('div.tt a'); - | 投教课堂 | 政策知识 | 投教动态 | 专题活动 | - | -------- | -------- | -------- | -------- | - | tjkt | zczs | tjdt | zthd |`, -}; + const title: string = $aEl.text(); + const description: string = $el.find('div.text').html(); + const pubDateStr: string | undefined = $el.find('div.info span').last().text().trim(); + const linkUrl: string | undefined = $aEl.attr('href'); + const categoryEls: Element[] = $el.find('div.tags span').toArray(); + const categories: string[] = [...new Set(categoryEls.map((el) => $(el).text()).filter(Boolean))]; + const authors: DataItem['author'] = $el.find('div.info span').first().text(); + const image: string | undefined = $el.find('div.side a img').attr('src'); + const upDatedStr: string | undefined = pubDateStr; -async function handler(ctx) { - const id = ctx.req.param('id') ?? 'yw'; + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr, ['HH:mm', 'MM-DD HH:mm', 'YYYY-MM-DD HH:mm']), +8) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? timezone(parseDate(upDatedStr, ['HH:mm', 'MM-DD HH:mm', 'YYYY-MM-DD HH:mm']), +8) : undefined, + language, + }; - const rootUrl = 'https://www.stcn.com'; - const currentUrl = `${rootUrl}/article/list/${id}.html`; - const apiUrl = `${rootUrl}/article/list.html?type=${id}`; + return processedItem; + }); - const response = await got({ - method: 'get', - url: apiUrl, - }); + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } - const $ = load(response.data); + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); - let items = $('.t, .tt, .title') - .find('a') - .toArray() - .map((item) => { - item = $(item); + const title: string = $$('div.detail-title').text(); + const description: string = $$('div.detail-content').html() ?? ''; + const pubDateStr: string | undefined = $$('div.detail-info span').last().text().trim(); + const categories: string[] = $$('meta[name="keywords"]').attr('content')?.split(/,/) ?? []; + const authors: DataItem['author'] = $$('div.detail-info span').first().text().split(/:/).pop(); + const upDatedStr: string | undefined = pubDateStr; - const link = item.attr('href'); + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : item.pubDate, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? timezone(parseDate(upDatedStr), +8) : item.updated, + language, + }; - return { - title: item.text().replaceAll(/(^【|】$)/g, ''), - link: link.startsWith('http') ? link : `${rootUrl}${link}`, - }; - }); - - items = await Promise.all( - items.map((item) => - cache.tryGet(item.link, async () => { - if (/\.html$/.test(item.link)) { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - - const content = load(detailResponse.data); - - item.title = content('.detail-title').text(); - item.author = content('.detail-info span').first().text().split(':').pop(); - item.pubDate = timezone(parseDate(content('.detail-info span').last().text()), +8); - item.category = content('.detail-content-tags div') - .toArray() - .map((t) => content(t).text()); - item.description = content('.detail-content').html(); - } - - return item; + return { + ...item, + ...processedItem, + }; + }); }) ) - ); + ).filter((_): _ is DataItem => true); return { - title: `证券时报网 - ${$('.breadcrumb a').last().text()}`, - link: currentUrl, + title: $('title').text(), + description: $('meta[name="description"]').attr('content'), + link: targetUrl, item: items, + allowEmpty: true, + image: $('img.stcn-logo').attr('src'), + author: $('meta[name="keywords"]').attr('content')?.split(/,/)[0], + language, + id: targetUrl, }; -} +}; + +export const route: Route = { + path: '/article/list/:id?', + name: '列表', + url: 'www.stcn.com', + maintainers: ['nczitzk'], + handler, + example: '/stcn/article/list/yw', + parameters: { + category: { + description: '分类,默认为 `yw`,即要闻,可在对应分类页 URL 中找到', + options: [ + { + label: '要闻', + value: 'yw', + }, + { + label: '股市', + value: 'gs', + }, + { + label: '公司', + value: 'company', + }, + { + label: '基金', + value: 'fund', + }, + { + label: '金融', + value: 'finance', + }, + { + label: '评论', + value: 'comment', + }, + { + label: '产经', + value: 'cj', + }, + { + label: '科创板', + value: 'kcb', + }, + { + label: '新三板', + value: 'xsb', + }, + { + label: 'ESG', + value: 'zk', + }, + { + label: '滚动', + value: 'gd', + }, + ], + }, + }, + description: `:::tip +若订阅 [要闻](https://www.stcn.com/article/list/yw.html),网址为 \`https://www.stcn.com/article/list/yw.html\`,请截取 \`https://www.stcn.com/article/list/\` 到末尾 \`.html\` 的部分 \`yw\` 作为 \`id\` 参数填入,此时目标路由为 [\`/stcn/article/list/yw\`](https://rsshub.app/stcn/article/list/yw)。 + +::: + +| 要闻 | 股市 | 公司 | 基金 | 金融 | 评论 | +| ---- | ---- | ------- | ---- | ------- | ------- | +| yw | gs | company | fund | finance | comment | + +| 产经 | 科创板 | 新三板 | ESG | 滚动 | +| ---- | ------ | ------ | --- | ---- | +| cj | kcb | xsb | zk | gd | +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/:id'], + target: (params, url) => { + const urlObj: URL = new URL(url); + const id: string | undefined = urlObj.searchParams.get('type') ?? params.id; + + return `/stcn/article/list${id ? `/${id}` : ''}`; + }, + }, + { + title: '要闻', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/yw.html'], + target: '/article/list/yw', + }, + { + title: '股市', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/gs.html'], + target: '/article/list/gs', + }, + { + title: '公司', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/company.html'], + target: '/article/list/company', + }, + { + title: '基金', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/fund.html'], + target: '/article/list/fund', + }, + { + title: '金融', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/finance.html'], + target: '/article/list/finance', + }, + { + title: '评论', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/comment.html'], + target: '/article/list/comment', + }, + { + title: '产经', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/cj.html'], + target: '/article/list/cj', + }, + { + title: '科创板', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/kcb.html'], + target: '/article/list/kcb', + }, + { + title: '新三板', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/xsb.html'], + target: '/article/list/xsb', + }, + { + title: 'ESG', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/zk.html'], + target: '/article/list/zk', + }, + { + title: '滚动', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/gd.html'], + target: '/article/list/gd', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/stcn/kx.ts b/lib/routes/stcn/kx.ts new file mode 100644 index 00000000000000..2697e4a632db63 --- /dev/null +++ b/lib/routes/stcn/kx.ts @@ -0,0 +1,144 @@ +import { type Data, type DataItem, type 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 { type CheerioAPI, load } from 'cheerio'; +import { type Context } from 'hono'; + +export const handler = async (ctx: Context): Promise<Data> => { + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'https://www.stcn.com'; + const targetUrl: string = new URL('article/list/kx.html', baseUrl).href; + const apiUrl: string = new URL('article/list.html', baseUrl).href; + + const targetResponse = await ofetch(targetUrl); + + const response = await ofetch(apiUrl, { + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + query: { + type: 'kx', + }, + }); + + const $: CheerioAPI = load(targetResponse); + const language = $('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + items = response.data.slice(0, limit).map((item): DataItem => { + const title: string = item.title; + const description: string = item.content; + const pubDate: number | string = item.time; + const linkUrl: string | undefined = item.url; + const categories: string[] = item.tags ? item.tags.map((c) => c.name) : []; + const authors: DataItem['author'] = item.source; + const image: string | undefined = item.share?.image; + const updated: number | string = pubDate; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDate ? parseDate(pubDate, 'X') : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + category: categories, + author: authors, + content: { + html: description, + text: item.content ?? description, + }, + image, + banner: image, + updated: updated ? parseDate(updated, 'X') : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.detail-title').text(); + const description: string = $$('div.detail-content').html() ?? ''; + const pubDateStr: string | undefined = $$('div.detail-info span').last().text().trim(); + const categories: string[] = [...new Set([...(item.category as string[]), ...($$('meta[name="keywords"]').attr('content')?.split(/,/) ?? [])])]; + const authors: DataItem['author'] = $$('div.detail-info span').first().text().split(/:/).pop(); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : item.pubDate, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? timezone(parseDate(upDatedStr), +8) : item.updated, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[name="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('img.stcn-logo').attr('src'), + author: $('meta[name="keywords"]').attr('content')?.split(/,/)[0], + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/article/list/kx', + name: '快讯', + url: 'www.stcn.com', + maintainers: ['nczitzk'], + handler, + example: '/stcn/article/list/kx', + parameters: undefined, + description: undefined, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.stcn.com/article/list/kx.html'], + target: '/article/list/kx', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/stcn/namespace.ts b/lib/routes/stcn/namespace.ts index 92ff6dfbe3665e..11b830a5377c42 100644 --- a/lib/routes/stcn/namespace.ts +++ b/lib/routes/stcn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '证券时报网', url: 'stcn.com', + lang: 'zh-CN', }; diff --git a/lib/routes/stcn/rank.ts b/lib/routes/stcn/rank.ts new file mode 100644 index 00000000000000..b8a5dc8c1c59f9 --- /dev/null +++ b/lib/routes/stcn/rank.ts @@ -0,0 +1,249 @@ +import { type Data, type DataItem, type 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 { type CheerioAPI, load } from 'cheerio'; +import { type Context } from 'hono'; + +export const handler = async (ctx: Context): Promise<Data> => { + const { id = 'yw' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'https://www.stcn.com'; + const targetUrl: string = new URL(`article/list/${id}.html`, baseUrl).href; + const apiUrl: string = new URL(`article/category-news-rank.html`, baseUrl).href; + + const response = await ofetch(apiUrl, { + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + query: { + type: id, + }, + }); + + const targetResponse = await ofetch(targetUrl); + const $: CheerioAPI = load(targetResponse); + const language = $('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + items = response.data.slice(0, limit).map((item): DataItem => { + const title: string = item.title; + const linkUrl: string | undefined = item.url; + + const processedItem: DataItem = { + title, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.detail-title').text(); + const description: string = $$('div.detail-content').html() ?? ''; + const pubDateStr: string | undefined = $$('div.detail-info span').last().text().trim(); + const categories: string[] = $$('meta[name="keywords"]').attr('content')?.split(/,/) ?? []; + const authors: DataItem['author'] = $$('div.detail-info span').first().text().split(/:/).pop(); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : item.pubDate, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + updated: upDatedStr ? timezone(parseDate(upDatedStr), +8) : item.updated, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[name="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('img.stcn-logo').attr('src'), + author: $('meta[name="keywords"]').attr('content')?.split(/,/)[0], + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/article/rank/:id?', + name: '热榜', + url: 'www.stcn.com', + maintainers: ['nczitzk'], + handler, + example: '/stcn/article/rank/yw', + parameters: { + category: { + description: '分类,默认为 `yw`,即要闻,可在对应分类页 URL 中找到', + options: [ + { + label: '要闻', + value: 'yw', + }, + { + label: '股市', + value: 'gs', + }, + { + label: '公司', + value: 'company', + }, + { + label: '基金', + value: 'fund', + }, + { + label: '金融', + value: 'finance', + }, + { + label: '评论', + value: 'comment', + }, + { + label: '产经', + value: 'cj', + }, + { + label: '科创板', + value: 'kcb', + }, + { + label: '新三板', + value: 'xsb', + }, + { + label: 'ESG', + value: 'zk', + }, + { + label: '滚动', + value: 'gd', + }, + ], + }, + }, + description: `:::tip +若订阅 [要闻](https://www.stcn.com/article/list/yw.html),网址为 \`https://www.stcn.com/article/list/yw.html\`,请截取 \`https://www.stcn.com/article/list/\` 到末尾 \`.html\` 的部分 \`yw\` 作为 \`id\` 参数填入,此时目标路由为 [\`/stcn/article/rank/yw\`](https://rsshub.app/stcn/article/rank/yw)。 + +::: + +| 要闻 | 股市 | 公司 | 基金 | 金融 | 评论 | +| ---- | ---- | ------- | ---- | ------- | ------- | +| yw | gs | company | fund | finance | comment | + +| 产经 | 科创板 | 新三板 | ESG | 滚动 | +| ---- | ------ | ------ | --- | ---- | +| cj | kcb | xsb | zk | gd | +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/:id'], + target: (params, url) => { + const urlObj: URL = new URL(url); + const id: string | undefined = urlObj.searchParams.get('type') ?? params.id; + + return `/stcn/article/rank${id ? `/${id}` : ''}`; + }, + }, + { + title: '要闻', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/yw.html'], + target: '/article/rank/yw', + }, + { + title: '股市', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/gs.html'], + target: '/article/rank/gs', + }, + { + title: '公司', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/company.html'], + target: '/article/rank/company', + }, + { + title: '基金', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/fund.html'], + target: '/article/rank/fund', + }, + { + title: '金融', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/finance.html'], + target: '/article/rank/finance', + }, + { + title: '评论', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/comment.html'], + target: '/article/rank/comment', + }, + { + title: '产经', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/cj.html'], + target: '/article/rank/cj', + }, + { + title: '科创板', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/kcb.html'], + target: '/article/rank/kcb', + }, + { + title: '新三板', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/xsb.html'], + target: '/article/rank/xsb', + }, + { + title: 'ESG', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/zk.html'], + target: '/article/rank/zk', + }, + { + title: '滚动', + source: ['www.stcn.com/article/list.html', 'www.stcn.com/article/list/gd.html'], + target: '/article/rank/gd', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/stdaily/namespace.ts b/lib/routes/stdaily/namespace.ts index 6d15d93cac0a3c..1975fcd4c26f4f 100644 --- a/lib/routes/stdaily/namespace.ts +++ b/lib/routes/stdaily/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国科技网', url: 'digitalpaper.stdaily.com', + lang: 'zh-CN', }; diff --git a/lib/routes/steam/appcommunityfeed.ts b/lib/routes/steam/appcommunityfeed.ts index da93fb95f81b74..8c16bf0ad29dbc 100644 --- a/lib/routes/steam/appcommunityfeed.ts +++ b/lib/routes/steam/appcommunityfeed.ts @@ -60,7 +60,7 @@ Example: - \`/appcommunityfeed/730/rgSections[]=6\` for CS2 Workshop contents only. - \`/appcommunityfeed/570/rgSections[]=3&rgSections[]=9\` for Dota2 Video and Guides contents. -:::tip +::: tip It can also access community hub contents that require a logged-in account. ::: `, diff --git a/lib/routes/steam/curator.ts b/lib/routes/steam/curator.ts new file mode 100644 index 00000000000000..a99329bc54edd2 --- /dev/null +++ b/lib/routes/steam/curator.ts @@ -0,0 +1,93 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { getCurrentPath } from '@/utils/helpers'; +import path from 'node:path'; +import { art } from '@/utils/render'; + +const __dirname = getCurrentPath(import.meta.url); + +export const route: Route = { + path: '/curator/:id/:routeParams?', + categories: ['game'], + example: '/steam/curator/34646096-80-Days', + parameters: { + id: "Steam curator id. It usually consists of a series of numbers and the curator's name.", + routeParams: { + description: `Extra parameters to filter the reviews. The following parameters are supported: +| Key | Description | Accepts | Defaults to | +| --------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------ | ----------- | +| \`curations\` | Review type to filter by. \`0\`: Recommended, \`1\`: Not Recommended, \`2\`: Informational | \`0\`/\`1\`/\`2\`/\`0,1\`/\`0,2\`/\`1,2\` | | +| \`tagids\` | Tag to filter by. Details are provided below. | use comma to separate multiple tagid | | + +Note: There is a [‘Popular Tags’](https://store.steampowered.com/tag/browse) page where you can find many but not all of the tags. The tag’s ID is in the \`data-tagid\` attribute of the element.Steam does not currently provide a page that comprehensively lists all tags, and you may need to explore alternative ways to find them. + +Examples: +* \`/steam/curator/34646096-80-Days/curations=&tagids=\` +* \`/steam/curator/34646096-80-Days/curations=0&tagids=19\` +* \`/steam/curator/34646096-80-Days/curations=0,2&tagids=19,21\` +`, + }, + }, + radar: [ + { + title: 'Latest Curator Reviews', + source: ['store.steampowered.com/curator/:id'], + target: '/curator/:id', + }, + ], + description: 'The Latest reviews from a Steam Curator.', + name: 'Latest Curator Reviews', + maintainers: ['naremloa', 'fenxer'], + handler: async (ctx) => { + const { id, routeParams } = ctx.req.param(); + const params = new URLSearchParams(routeParams); + + const url = new URL(`https://store.steampowered.com/curator/${id}/ajaxgetfilteredrecommendations/?query&start=0&count=10&dynamic_data=&sort=recent&app_types=&reset=false&curations=&tagids=`); + for (const [key, value] of params) { + if (['curations', 'tagids'].includes(key)) { + url.searchParams.set(key, value || ''); + } + } + + const response = await ofetch(url.toString()); + const $ = load(response.results_html ?? ''); + + const items = $('.recommendation') + .toArray() + .map((item) => { + const el = $(item); + const appImageEl = el.find('a.store_capsule img'); + const appTitle = appImageEl.attr('alt')!; + const appImage = appImageEl.attr('src') ?? ''; + const appLink = el.find('.recommendation_link').first().attr('href'); + const reviewContent = el.find('.recommendation_desc').text().trim(); + const reviewDateText = el.find('.curator_review_date').text().trim(); + + const notCurrentYearPattern = /,\s\b\d{4}\b$/; + const reviewPubDate = notCurrentYearPattern.test(reviewDateText) ? parseDate(reviewDateText) : parseDate(`${reviewDateText}, ${new Date().getFullYear()}`); + + const description = art(path.join(__dirname, 'templates/curator-description.art'), { image: appImage, description: reviewContent }); + + return { + title: appTitle, + link: appLink, + description, + pubDate: reviewPubDate, + media: { + content: { + url: appImage, + medium: 'image', + }, + }, + }; + }); + + return { + title: `Steam Curator ${id} Reviews`, + link: url.toString(), + item: items, + }; + }, +}; diff --git a/lib/routes/steam/namespace.ts b/lib/routes/steam/namespace.ts index 0ecb6b79f3d741..ac2d91ab614e32 100644 --- a/lib/routes/steam/namespace.ts +++ b/lib/routes/steam/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Steam', url: 'store.steampowered.com', + lang: 'en', }; diff --git a/lib/routes/steam/search.ts b/lib/routes/steam/search.ts index ed23b802877b34..88d20a28412b03 100644 --- a/lib/routes/steam/search.ts +++ b/lib/routes/steam/search.ts @@ -34,6 +34,8 @@ async function handler(ctx) { const isBundle = !!$el.attr('data-ds-bundle-data'); const isDiscounted = $el.find('.discount_original_price').length > 0; const hasReview = $el.find('.search_review_summary').length > 0; + const appID: string | undefined = $el.attr('data-ds-appid'); + let desc = ''; if (isBundle) { const bundle = JSON.parse($el.attr('data-ds-bundle-data')); @@ -57,6 +59,11 @@ async function handler(ctx) { title: $el.find('span.title').text(), link: $el.attr('href'), description: desc.replaceAll('\n', '<br>'), + media: { + thumbnail: { + url: `https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/${appID}/header.jpg`, + }, + }, }; }) .filter((it) => it.title), diff --git a/lib/routes/steam/sharefile-changelog.ts b/lib/routes/steam/sharefile-changelog.ts new file mode 100644 index 00000000000000..8a0fe49ea2fc05 --- /dev/null +++ b/lib/routes/steam/sharefile-changelog.ts @@ -0,0 +1,65 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/sharefile-changelog/:sharefileID/:routeParams?', + categories: ['game'], + example: '/steam/sharefile-changelog/2851063440/l=schinese', + parameters: { + sharefileID: 'Steam community sharefile id. Usually refers to a workshop item.', + routeParams: 'Route parameters.', + }, + radar: [ + { + title: 'Sharefile Changelog', + source: ['steamcommunity.com/sharedfiles/filedetails/changelog/:sharefileID'], + target: '/sharefile-changelog/:sharefileID', + }, + ], + description: `Steam Community Sharefile's Changelog. Primary used for a workshop item. +Helpful route parameters: +- \`l=\` language parameter, change the language of description. +- \`p=\` page parameter, change the results page. p=1 by default. +`, + name: 'Sharefile Changelog', + maintainers: ['NyaaaDoge'], + + handler: async (ctx) => { + const { sharefileID, routeParams } = ctx.req.param(); + + const url = `https://steamcommunity.com/sharedfiles/filedetails/changelog/${sharefileID}${routeParams ? `?${routeParams}` : ''}`; + const response = await ofetch(url); + const $ = load(response); + + const appName = $('div.apphub_AppName').first().text(); + const appIcon = $('div.apphub_AppIcon').children('img').attr('src'); + const itemTitle = $('div.workshopItemTitle').first().text(); + + const items = $('div.clearfix .changeLogCtn') + .toArray() + .map((item) => { + item = $(item); + // changelogHeadline is local time + const changelogHeadline = item.find('.headline').first().text(); + const changelogTimestamp = item.find('p').first().attr('id'); + const changeDetail = item.find('p').first().html(); + + return { + title: changelogHeadline, + link: `https://steamcommunity.com/sharedfiles/filedetails/changelog/${sharefileID}`, + description: changeDetail, + pubDate: parseDate(changelogTimestamp, 'X'), + }; + }); + + return { + title: itemTitle, + link: `https://steamcommunity.com/sharedfiles/filedetails/changelog/${sharefileID}`, + description: `${appName} steam community sharefile changelog`, + item: items, + icon: appIcon, + }; + }, +}; diff --git a/lib/routes/steam/templates/curator-description.art b/lib/routes/steam/templates/curator-description.art new file mode 100644 index 00000000000000..e77f842b8e8fb6 --- /dev/null +++ b/lib/routes/steam/templates/curator-description.art @@ -0,0 +1,4 @@ +{{ if image }} +<img src="{{ image }}"/> +{{ /if }} +<p>{{ description }}</p> diff --git a/lib/routes/steam/templates/workshop-search-description.art b/lib/routes/steam/templates/workshop-search-description.art new file mode 100644 index 00000000000000..364adf5da4cac2 --- /dev/null +++ b/lib/routes/steam/templates/workshop-search-description.art @@ -0,0 +1,5 @@ +{{ if image }} +<img src="{{ image }}"/><br/> +{{ /if }} +{{ if rating }}<img src="{{ rating }}"/>{{ /if }}{{ if checkmark }}{{ each checkmark }} <img src="{{ $value }}"/>{{ /each }}{{ /if }} +<p>{{ description }}</p> diff --git a/lib/routes/steam/workshop-search.ts b/lib/routes/steam/workshop-search.ts new file mode 100644 index 00000000000000..e8dee0b01fbd5d --- /dev/null +++ b/lib/routes/steam/workshop-search.ts @@ -0,0 +1,109 @@ +import { Route } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +export const route: Route = { + path: '/workshopsearch/:appid?/:routeParams?', + categories: ['game'], + example: '/steam/workshopsearch/730', + parameters: { + appid: 'Steam appid, can be found on the community hub page or store page URL, 730 by default.', + routeParams: 'Route parameters, can be found on the search result page URL. Route parameters located after the appid.', + }, + radar: [ + { + title: 'Workshop Search Results', + source: ['steamcommunity.com/app/:appid/workshop/'], + target: '/workshopsearch/:appid', + }, + ], + description: `Steam Community Workshop Search Results. +The parameter 'l=language' changes the language of search results(if possible). +For example, route \`/workshopsearch/730/l=schinese\` will display the simplified Chinese descriptions of the entry. + +Language Parameter: + +| English | 简体中文 | 繁體中文 | 日本語 | 한국어 | ภาษาไทย | български | čeština | dansk | Deutsch | español | latam | ελληνικά | français | italiano | Bahasa Indonesia | magyar | Nederlands | norsk | polski | português | brasileiro | română | русский | suomi | svenska | Türkçe | Tiếng Việt | українська | +| ------- | -------- | -------- | -------- | ------- | ------- | --------- | ------- | ------ | ------- | ------- | ----- | -------- | -------- | -------- | ---------------- | --------- | ---------- | --------- | ------ | ---------- | ---------- | -------- | ------- | ------- | ------- | ------- | ---------- | ---------- | +| english | schinese | tchinese | japanese | koreana | thai | bulgarian | czech | danish | german | spanish | latam | greek | french | italian | indonesian | hungarian | dutch | norwegian | polish | portuguese | brazilian | romanian | russian | finnish | swedish | turkish | vietnamese | ukrainian | + +`, + name: 'Community Workshop Search', + maintainers: ['NyaaaDoge'], + + handler: async (ctx) => { + const { appid = 730, routeParams } = ctx.req.param(); + + const url = `https://steamcommunity.com/workshop/browse/?appid=${appid}${routeParams ? `&${routeParams}` : ''}`; + const response = await ofetch(url); + const $ = load(response); + + const appName = $('div.apphub_AppName').first().text(); + const workshopDescription = $('div.customBrowseText').first().text(); + const appIcon = $('div.apphub_AppIcon').children('img').attr('src'); + + const items = $('div.workshopBrowseItems .workshopItem') + .toArray() + .map((item) => { + item = $(item); + const publishedFileId = item.find('a').first().attr('data-publishedfileid'); + const entryTitle = item.find('.workshopItemTitle').first().text(); + const authorNickName = item.find('.workshop_author_link').first().text(); + const previewImage = item.find('.workshopItemPreviewImage').first().attr('src'); + const ratingImage = item.find('.fileRating').first().attr('src'); + // Some items are flaged as 'accepted for game' and 'incompatible item' + const checkMarkImages: string[] = []; + $(item) + .find('.workshop_checkmark') + .each((index, element) => { + const checkMarkElement = $(element); + const style = checkMarkElement.attr('style'); + // Only add checkmark image if it is not set to 'display: none' + if (!style || !style.includes('display: none;')) { + checkMarkImages.push(checkMarkElement.attr('src') || ''); + } + }); + // const script_tag = item.next('script'); + // console.log(`script_tag:${script_tag.text()}`); + const hoverContent = item.next('script').text(); + const regex = /SharedFileBindMouseHover\(\s*"sharedfile_\d+",\s*(?:true|false),\s*({.*?})\s*\);/; + const match = hoverContent.match(regex); + + let entryDescription = ''; + + if (match) { + const jsonString = match[1]; + // console.log(jsonString); + const data = JSON.parse(jsonString); + if (data.id === publishedFileId) { + entryDescription = data.description; + } + } + + return { + title: entryTitle, + link: `https://steamcommunity.com/sharedfiles/filedetails/?id=${publishedFileId}`, + description: art(path.join(__dirname, 'templates/workshop-search-description.art'), { + image: previewImage, + rating: ratingImage, + checkmark: checkMarkImages, + description: entryDescription, + }), + author: authorNickName, + }; + }); + + return { + title: `${appName} Steam Workshop Content`, + link: `https://steamcommunity.com/workshop/browse/?appid=${appid}${routeParams ? `&${routeParams}` : ''}`, + item: items, + icon: appIcon, + description: workshopDescription, + }; + }, +}; diff --git a/lib/routes/stheadline/namespace.ts b/lib/routes/stheadline/namespace.ts index 3aae5c26d49487..1f3a8db1c31cc2 100644 --- a/lib/routes/stheadline/namespace.ts +++ b/lib/routes/stheadline/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '星島日報', url: 'std.stheadline.com', + lang: 'zh-TW', }; diff --git a/lib/routes/stockedge/daily-news.ts b/lib/routes/stockedge/daily-news.ts index 9664a566ff414f..5702c4617ef818 100644 --- a/lib/routes/stockedge/daily-news.ts +++ b/lib/routes/stockedge/daily-news.ts @@ -1,10 +1,11 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import { getData, getList } from './utils'; export const route: Route = { path: '/daily-updates/news', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Notifications, example: '/stockedge/daily-updates/news', parameters: {}, features: { @@ -36,6 +37,9 @@ async function handler() { const items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { + if (!item.securityID) { + return item; + } const info = await getData(`${apiInfo}/${item.securityID}`); item.description = item.description + '<br><br>' + info?.AboutCompanyText; return item; diff --git a/lib/routes/stockedge/namespace.ts b/lib/routes/stockedge/namespace.ts index 1550910c8218f2..d70291c7ba26f1 100644 --- a/lib/routes/stockedge/namespace.ts +++ b/lib/routes/stockedge/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Stock Edge', url: 'web.stockedge.com', + lang: 'en', }; diff --git a/lib/routes/stockedge/utils.ts b/lib/routes/stockedge/utils.ts index 56ccbe49c3f684..33bd2b42ffea88 100644 --- a/lib/routes/stockedge/utils.ts +++ b/lib/routes/stockedge/utils.ts @@ -1,32 +1,35 @@ -import got from '@/utils/got'; +import { DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; const baseUrl = 'https://web.stockedge.com/share/'; const getData = (url) => - got - .get(url, { - headers: { - Host: 'api.stockedge.com', - Origin: 'https://web.stockedge.com', - Referer: 'https://web.stockedge.com/', - accept: 'application/json, text/plain, */*', - }, - }) - .json(); + ofetch(url, { + headers: { + Host: 'api.stockedge.com', + Origin: 'https://web.stockedge.com', + Referer: 'https://web.stockedge.com/', + accept: 'application/json, text/plain, */*', + }, + }); const getList = (data) => data.map((value) => { const { ID, Description: title, Date: createdOn, NewsitemSecurities, NewsitemSectors, NewsitemIndustries } = value; - const securityID = NewsitemSecurities[0].SecurityID; + const securityID = NewsitemSecurities?.[0]?.SecurityID; + const securitySlug = NewsitemSecurities?.[0]?.SecuritySlug; + const sectors = NewsitemSectors.map((v) => v.SectorName); + const industries = NewsitemIndustries.map((v) => v.IndustryName); return { id: ID, - title: `${title} [${NewsitemSectors.map((v) => v.SectorName).join(', ')}]`, + title: `${title} [${sectors.join(', ')}]`, description: title, securityID, - link: `${baseUrl}${NewsitemSecurities[0].SecuritySlug}/${securityID}`, + link: NewsitemSecurities?.length === 0 ? '' : `${baseUrl}${securitySlug}/${securityID}?section=news`, + guid: NewsitemSecurities?.length === 0 ? ID : `${baseUrl}${securitySlug}/${securityID}`, pubDate: parseDate(createdOn), - category: [...NewsitemIndustries.map((v) => v.IndustryName), ...NewsitemSectors.map((v) => v.SectorName)], - }; + category: [...industries, ...sectors], + } as DataItem; }); export { getData, getList }; diff --git a/lib/routes/storm/index.ts b/lib/routes/storm/index.ts index b63d91022ba604..7539331b8bd466 100644 --- a/lib/routes/storm/index.ts +++ b/lib/routes/storm/index.ts @@ -26,14 +26,14 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 新聞總覽 | 地方新聞 | 歷史頻道 | 評論總覽 | - | -------- | ------------- | -------- | ----------- | - | articles | localarticles | history | all-comment | +| -------- | ------------- | -------- | ----------- | +| articles | localarticles | history | all-comment | - :::tip +::: tip 支持形如 \`https://www.storm.mg/category/118\` 的路由,即 [\`/storm/category/118\`](https://rsshub.app/storm/category/118) 支持形如 \`https://www.storm.mg/localarticle-category/s149845\` 的路由,即 [\`/storm/localarticle-category/s149845\`](https://rsshub.app/storm/localarticle-category/s149845) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/storm/namespace.ts b/lib/routes/storm/namespace.ts index fe2bb53c7c949c..d1462be813b181 100644 --- a/lib/routes/storm/namespace.ts +++ b/lib/routes/storm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '風傳媒', url: 'storm.mg', + lang: 'zh-TW', }; diff --git a/lib/routes/storyfm/namespace.ts b/lib/routes/storyfm/namespace.ts index 7b9d85b6228c5b..4545df38bc63aa 100644 --- a/lib/routes/storyfm/namespace.ts +++ b/lib/routes/storyfm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '故事 FM', url: 'storyfm.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/straitstimes/index.ts b/lib/routes/straitstimes/index.ts new file mode 100644 index 00000000000000..f0995231902fd8 --- /dev/null +++ b/lib/routes/straitstimes/index.ts @@ -0,0 +1,135 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; +import { getCurrentPath } from '@/utils/helpers'; +import path from 'node:path'; +import { art } from '@/utils/render'; + +export const route: Route = { + path: '/:category?/:section?', + categories: ['traditional-media'], + example: '/straitstimes/singapore', + parameters: { + category: 'Category, see below for more information', + section: 'Section, see below for more information', + }, + features: { + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + requireConfig: false, + }, + name: 'News', + maintainers: ['quiniapiezoelectricity'], + handler, + description: ` +| Category | \`:category\` | +| ---------------------- | --------------------------- | +| Singapore | \`singapore\` | +| Asia | \`asia\` | +| World | \`world\` | +| Opinion | \`opinion\` | +| Life | \`life\` | +| Business | \`business\` | +| Jobs | \`jobs\` | +| Parenting & Education | \`parenting-and-education\` | +| Food | \`food\` | +| Tech | \`tech\` | +| Sport | \`sport\` | +| Podcasts | \`podcasts\` |, + +| Section | \`:section\` | +| ---------------------- | --------------------------- | +| Top Stories | \`top-stories\` | +| Latest | \`latest\` |`, + radar: [ + { + source: ['www.straitstimes.com/:category'], + target: '/:category', + }, + { + source: ['www.straitstimes.com'], + target: '/', + }, + ], +}; + +async function handler(ctx) { + const category = ctx.req.param('category') ? ctx.req.param('category').toLowerCase() : 'singapore'; + const section = ctx.req.param('section') ? ctx.req.param('section').toLowerCase() : undefined; + const apiKey = 'T9XUJM9rAZoLOd2CAx2wCBSTrm3xoyPw'; + const platform = 'iosflex'; + const __dirname = getCurrentPath(import.meta.url); + let feed; + const response = await got({ + method: 'get', + url: `https://newsapi.sphdigital.com/v2/feed/section/st?page=1&platform=${platform}§ion=${category}/star&subscribed=false&version=4.0`, + headers: { + 'x-api-key': apiKey, + }, + }); + feed = response.data.data; + const sections = new Set(feed.filter((item) => item.items[0].itemType === 'SectionFooter').map((items) => items.items[0].sectionFooterData.link.id)); + let slug = section && sections.has(`${category}-${section}-more/star`) ? `${category}-${section}-more/star` : undefined; + if (section === undefined) { + for (const section of ['latest', 'top-picks', 'top-stories']) { + if (sections.has(`${category}-${section}-more/star`)) { + slug = `${category}-${section}-more/star`; + break; + } + } + } + if (slug) { + const response = await got({ + method: 'get', + url: `https://newsapi.sphdigital.com/v2/feed/section/st?page=1&platform=${platform}§ion=${slug}&subscribed=false&version=4.0`, + headers: { + 'x-api-key': apiKey, + }, + }); + feed = response.data.data; + } + const articles = feed.filter((item) => item.items[0].itemType === 'Article'); + const items = await Promise.all( + articles.map((item) => + cache.tryGet(item.items[0].articleData.url, async () => { + const article = item.items[0].articleData; + if (article.authors) { + item.author = article.authors.map((author) => author.name).join(', '); + } + if (article.keywords) { + item.category = article.keywords.map((keyword) => keyword.name); + } + item.title = article.headline; + item.pubDate = parseDate(article.publicationTime, 'X'); + item.updated = parseDate(article.updatedTime, 'X'); + item.link = article.url; + let content = article.teaser; + if (article.documentId) { + const response = await got({ + method: 'get', + url: `https://newsapi.sphdigital.com/v2/feed/article/st/${article.documentId}?platform=${platform}&version=4.0`, + headers: { + 'x-api-key': apiKey, + }, + }); + content = response.data.data.body; + } + item.description = art(path.join(__dirname, 'templates/description.art'), { + images: article.images ?? [], + article: content, + }); + return item; + }) + ) + ); + + return { + title: `The Strait Times - ${category.replaceAll('-', ' ').toUpperCase()}`, + link: `https://www.straitstimes.com/${category.toLowerCase()}`, + item: items, + }; +} diff --git a/lib/routes/straitstimes/namespace.ts b/lib/routes/straitstimes/namespace.ts new file mode 100644 index 00000000000000..5a054da79127c0 --- /dev/null +++ b/lib/routes/straitstimes/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'The Strait Times', + url: 'straitstimes.com', + description: '', + lang: 'en', +}; diff --git a/lib/routes/straitstimes/templates/description.art b/lib/routes/straitstimes/templates/description.art new file mode 100644 index 00000000000000..697ad38bc2f202 --- /dev/null +++ b/lib/routes/straitstimes/templates/description.art @@ -0,0 +1,27 @@ +{{ if images }} + {{ each images }} + {{if $value.url }} + <figure> + <img src= + {{ if $value.large }} + "{{ $value.large }}" + {{ else }} + "{{ $value.url }}" + {{ /if }} + {{ if $value.height }} + height="{{ $value.height }}" + {{ /if }} + {{ if $value.width }} + width="{{ $value.width }}" + {{ /if }} + > + {{ if $value.caption }} + <figcaption>{{ $value.caption }}</figcaption> + {{ /if }} + </figure> + {{ /if }} + {{ /each }} +{{ /if }} +{{ if article }} + {{@ article }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/stratechery/namespace.ts b/lib/routes/stratechery/namespace.ts index 40d0ef32457159..04069ec0664dc0 100644 --- a/lib/routes/stratechery/namespace.ts +++ b/lib/routes/stratechery/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Stratechery by Ben Thompson', url: 'blog.stratechery.com', + lang: 'en', }; diff --git a/lib/routes/stream-capital/namespace.ts b/lib/routes/stream-capital/namespace.ts new file mode 100644 index 00000000000000..a0e9d859efea8a --- /dev/null +++ b/lib/routes/stream-capital/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '远川研究所', + url: 'www.stream-capital.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/stream-capital/search.ts b/lib/routes/stream-capital/search.ts new file mode 100644 index 00000000000000..c1adec94782c65 --- /dev/null +++ b/lib/routes/stream-capital/search.ts @@ -0,0 +1,79 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import { encrypt, decrypt } from './utils'; +import { EncryptedResponse, WebBlog } from './types'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/search', + name: '最新', + categories: ['finance'], + example: '/stream-capital/search', + maintainers: ['TonyRL'], + handler, + radar: [ + { + source: ['www.stream-capital.com/search'], + }, + ], +}; + +async function handler() { + const baseUrl = 'https://www.stream-capital.com'; + const apiBaseUrl = 'https://api.yuanchuan.cn'; + + const response = await ofetch<EncryptedResponse>(`${apiBaseUrl}/yc/webbloglist`, { + method: 'POST', + query: { + apptype: 9, + }, + body: encrypt( + JSON.stringify({ + type: 0, + name: null, + page: 1, + }) + ), + }); + + const list = (JSON.parse(decrypt(response.data)).list as WebBlog[]).map((item) => ({ + title: item.title, + author: item.userName, + pubDate: timezone(parseDate(item.ctime, 'YYYY-MM-DD HH:mm:ss'), 8), + link: `${baseUrl}/article/${item.id}`, + description: item.content, + category: item.tags.map((t) => t.tagName), + id: item.id, + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch<EncryptedResponse>(`${apiBaseUrl}/yc/webblogdetail`, { + method: 'POST', + query: { + apptype: 9, + }, + body: encrypt( + JSON.stringify({ + blogId: item.id, + }) + ), + }); + + item.description = (JSON.parse(decrypt(response.data)) as WebBlog).detailInfo.articleContent; + + return item; + }) + ) + ); + + return { + title: '最新 - 远川研究所', + link: `${baseUrl}/search`, + language: 'zh', + item: items, + }; +} diff --git a/lib/routes/stream-capital/types.ts b/lib/routes/stream-capital/types.ts new file mode 100644 index 00000000000000..6fe06499174cae --- /dev/null +++ b/lib/routes/stream-capital/types.ts @@ -0,0 +1,44 @@ +export interface EncryptedResponse { + code: number; + data: string; + msg: string; +} + +interface Tag { + blogId: number; + tagId: number; + length: number; + tagName: string; + beginIndex: number; +} + +interface DetailInfo { + articleContent: string; + blogId: number; + videoCoverPicUrl: string; + excerpt: string; + id: number; + videoUrl: string; +} + +export interface WebBlog { + title: string; + type: number; + intro: null; + avatarUrl: null; + userName: string; + ctime: string; + isPublish: number; + id: number; + themeTitle: string; + themeId: number; + viewCount: number; + coverUrl: string; + videoDuration: number; + originUrl: string; + userId: number; + content: string; + tags: Tag[]; + isCollect: number; + detailInfo: DetailInfo; +} diff --git a/lib/routes/stream-capital/utils.ts b/lib/routes/stream-capital/utils.ts new file mode 100644 index 00000000000000..1f0a72c97ebb07 --- /dev/null +++ b/lib/routes/stream-capital/utils.ts @@ -0,0 +1,18 @@ +import CryptoJS from 'crypto-js'; + +const secret = CryptoJS.enc.Utf8.parse('r4rt5A8L6ye6ts8y'); +const iv = CryptoJS.enc.Utf8.parse('fs0Hkjg8a23u8sE0'); + +export const encrypt = (plainText) => + CryptoJS.AES.encrypt(plainText, secret, { + iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }).toString(); + +export const decrypt = (encrypted) => + CryptoJS.AES.decrypt(encrypted, secret, { + iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }).toString(CryptoJS.enc.Utf8); diff --git a/lib/routes/studygolang/namespace.ts b/lib/routes/studygolang/namespace.ts index d0cd84f686b0df..7abf20effc86d3 100644 --- a/lib/routes/studygolang/namespace.ts +++ b/lib/routes/studygolang/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Go 语言中文网', url: 'studygolang.com', + lang: 'zh-CN', }; diff --git a/lib/routes/subhd/namespace.ts b/lib/routes/subhd/namespace.ts index bb684a7f87e7b2..bb53e92fbe6c4b 100644 --- a/lib/routes/subhd/namespace.ts +++ b/lib/routes/subhd/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Sub HD', url: 'subhd.tv', + lang: 'zh-CN', }; diff --git a/lib/routes/substack/namespace.ts b/lib/routes/substack/namespace.ts new file mode 100644 index 00000000000000..9c8f7df8001a2c --- /dev/null +++ b/lib/routes/substack/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Substack', + url: 'substack.com', + lang: 'en', +}; diff --git a/lib/routes/substack/subscribe.ts b/lib/routes/substack/subscribe.ts new file mode 100644 index 00000000000000..266594ad8c72f2 --- /dev/null +++ b/lib/routes/substack/subscribe.ts @@ -0,0 +1,48 @@ +import { Route, ViewType } from '@/types'; +import parser from '@/utils/rss-parser'; +import { parseDate } from '@/utils/parse-date'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +export const route: Route = { + path: '/subscribe/:user', + categories: ['blog'], + view: ViewType.SocialMedia, + example: '/substack/subscribe/mangoread', + parameters: { user: 'Username of the Substack' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Substack Subscription', + maintainers: ['pseudoyu'], + handler, +}; + +async function handler(ctx) { + const user = ctx.req.param('user'); + + if (!user) { + throw new InvalidParameterError('Invalid user'); + } + + const feed = await parser.parseURL(`https://${user}.substack.com/feed`); + + return { + title: feed.title ?? 'Substack', + description: feed.description ?? `${user}'s Substack`, + link: feed.link ?? `https://${user}.substack.com`, + image: feed.image?.url ?? '', + item: feed.items.map((item) => ({ + title: item.title ?? 'Untitled', + description: item['content:encoded'] ?? item.content ?? '', + link: item.link ?? '', + pubDate: item.pubDate ? parseDate(item.pubDate) : undefined, + guid: item.guid ?? '', + author: item.creator ?? user, + })), + }; +} diff --git a/lib/routes/supchina/index.ts b/lib/routes/supchina/index.ts index f130f8697fc06d..e6092d5a374033 100644 --- a/lib/routes/supchina/index.ts +++ b/lib/routes/supchina/index.ts @@ -40,7 +40,7 @@ async function handler(ctx) { title: item.find('title').text(), link: item.find('guid').text(), author: item - .find('dc\\:creator') + .find(String.raw`dc\:creator`) .html() .match(/CDATA\[(.*?)]/)[1], category: item diff --git a/lib/routes/supchina/namespace.ts b/lib/routes/supchina/namespace.ts index 571807c6c9472e..4dea8ad0b6eca6 100644 --- a/lib/routes/supchina/namespace.ts +++ b/lib/routes/supchina/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'SupChina', url: 'supchina.com', + lang: 'zh-CN', }; diff --git a/lib/routes/supchina/podcasts.ts b/lib/routes/supchina/podcasts.ts index ad9a0499a42b00..ae35b509ca34ed 100644 --- a/lib/routes/supchina/podcasts.ts +++ b/lib/routes/supchina/podcasts.ts @@ -47,7 +47,7 @@ async function handler(ctx) { return { link: item.find('guid').text(), - author: item.find('itunes\\:author').text(), + author: item.find(String.raw`itunes\:author`).text(), }; }); @@ -85,8 +85,10 @@ async function handler(ctx) { return { title: 'SupChina - Podcasts', link: `${rootUrl}/podcasts`, - itunes_author: $('channel itunes\\:author').first().text(), - image: $('itunes\\:image').attr('href'), + itunes_author: $(String.raw`channel itunes\:author`) + .first() + .text(), + image: $(String.raw`itunes\:image`).attr('href'), item: items, }; } diff --git a/lib/routes/surfshark/namespace.ts b/lib/routes/surfshark/namespace.ts index 18a86726a1be1c..f7e46bd1a72cfd 100644 --- a/lib/routes/surfshark/namespace.ts +++ b/lib/routes/surfshark/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Surfshark', url: 'surfshark.com', + lang: 'en', }; diff --git a/lib/routes/sustainabilitymag/articles.ts b/lib/routes/sustainabilitymag/articles.ts new file mode 100644 index 00000000000000..7562c013b048d9 --- /dev/null +++ b/lib/routes/sustainabilitymag/articles.ts @@ -0,0 +1,146 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import oftech from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/articles', + name: 'Articles', + url: 'sustainabilitymag.com/articles', + maintainers: ['mintyfrankie'], + categories: ['other'], + example: '/sustainabilitymag/articles', + radar: [ + { + source: ['https://sustainabilitymag.com/articles'], + target: '/sustainabilitymag/articles', + }, + ], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + handler, +}; + +const findLargestImgKey = (images) => + Object.keys(images) + .filter((key) => key.startsWith('inline_free_') || key.startsWith('hero_landscape_')) + .sort((a, b) => Number.parseInt(b.split('_')[2]) - Number.parseInt(a.split('_')[2]))[0]; + +const renderFigure = (url, caption) => `<figure><img src="${url}" alt="${caption}" /><figcaption>${caption}</figcaption></figure>`; + +const render = (widgets) => + widgets + .map((w) => { + switch (w.type) { + case 'text': + return w.html; + case 'blockquote': + return `<blockquote>${w.html}</blockquote>`; + case 'keyFacts': + return `<div><ul>${w.keyFacts.map((k) => `<li>${k.text}</li>`).join('')}</ul></div>`; + case 'inlineVideo': + return w.provider === 'youtube' + ? `<iframe id="ytplayer" type="text/html" width="640" height="360" src="https://www.youtube-nocookie.com/embed/${w.videoId}" frameborder="0" allowfullscreen></iframe>` + : new Error(`Unhandled inlineVideo provider: ${w.provider}`); + case 'inlineImage': + return w.inlineImageImages + .map((image) => { + const i = image.images[findLargestImgKey(image.images)][0]; + return renderFigure(i.url, i.caption); + }) + .join(''); + default: + throw new Error(`Unhandled widget type: ${w.type}`); + } + }) + .join(''); + +async function handler() { + const baseURL = `https://sustainabilitymag.com`; + const feedURL = `${baseURL}/articles`; + const feedLang = 'en'; + const feedDescription = 'Sustainability Magazine Articles'; + + const requestEndpoint = `${baseURL}/graphql`; + const requestBody = JSON.stringify({ + query: `query PaginatedQuery($url: String!, $page: Int = 1, $widgetType: String!) { + paginatedWidget(url: $url, widgetType: $widgetType) { + ... on SimpleArticleGridWidget { + articles(page: $page) { + results { + _id + headline + fullUrlPath + featured + category + contentType + tags { + tag + } + attribution + subAttribution + sell + images { + thumbnail_widescreen_553 { + url + } + } + } + } + } + } + }`, + operationName: 'PaginatedQuery', + variables: { + widgetType: 'simpleArticleGrid', + page: 1, + url: 'https://sustainabilitymag.com/articles', + }, + }); + + const results = await oftech(requestEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: requestBody, + }); + + const list = results.data.paginatedWidget.articles.results.map((item) => ({ + title: item.headline, + link: `${baseURL}${item.fullUrlPath}`, + image: item.images?.thumbnail_widescreen_553?.url, + category: item.category, + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await oftech(item.link); + const $ = load(response); + + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + const article = nextData.props.pageProps.article; + item.pubDate = parseDate(article.displayDate); + item.author = article.author.name; + const heroImage = article.images[findLargestImgKey(article.images)][0]; + item.description = renderFigure(heroImage.url, heroImage.caption) + render(article.body.widgets); + + return item; + }) + ) + ); + + return { + title: 'Sustainability Magazine Articles', + language: feedLang, + description: feedDescription, + link: `https://${feedURL}`, + item: items, + }; +} diff --git a/lib/routes/sustainabilitymag/namespace.ts b/lib/routes/sustainabilitymag/namespace.ts new file mode 100644 index 00000000000000..89161b6621f43e --- /dev/null +++ b/lib/routes/sustainabilitymag/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Sustainability Magazine', + url: 'sustainabilitymag.com', + lang: 'en', +}; diff --git a/lib/routes/sustech/namespace.ts b/lib/routes/sustech/namespace.ts index 257dec6ee2d4dd..be104126fa708e 100644 --- a/lib/routes/sustech/namespace.ts +++ b/lib/routes/sustech/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南方科技大学', url: 'biddingoffice.sustech.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/swissinfo/namespace.ts b/lib/routes/swissinfo/namespace.ts index 17aa0c2ec206ce..f5ea2e629a7808 100644 --- a/lib/routes/swissinfo/namespace.ts +++ b/lib/routes/swissinfo/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'swissinfo', url: 'swissinfo.ch', + lang: 'en', }; diff --git a/lib/routes/swjtu/gsee/yjs.ts b/lib/routes/swjtu/gsee/yjs.ts new file mode 100644 index 00000000000000..4c747778c3815e --- /dev/null +++ b/lib/routes/swjtu/gsee/yjs.ts @@ -0,0 +1,76 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +const rootURL = 'https://gsee.swjtu.edu.cn'; +const urlAddr = `${rootURL}/xwzx/tzgg.htm`; + +const getItem = (item) => { + const newsInfo = item.find('dt'); + const newsDate = item + .find('dd') + .text() + .match(/\d{4}(-|\/|.)\d{1,2}\1\d{1,2}/)[0]; + + const infoTitle = newsInfo.text(); + const link = rootURL + newsInfo.find('a').last().attr('href').slice(2); + return cache.tryGet(link, async () => { + const resp = await ofetch(link); + const $$ = load(resp); + const infoText = $$('.article').html(); + + return { + title: infoTitle, + pubDate: parseDate(newsDate), + link, + description: infoText, + }; + }) as any; +}; + +export const route: Route = { + path: '/gsee/yjs', + categories: ['university'], + example: '/swjtu/gsee/yjs', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['gsee.swjtu.edu.cn/'], + }, + ], + name: '地球科学与工程学院', + maintainers: ['E1nzbern'], + handler, + description: `研究生教育通知公告`, +}; + +async function handler() { + const resp = await ofetch(urlAddr); + const $ = load(resp); + + const list = $('dl'); + + const items = await Promise.all( + list.toArray().map((i) => { + const item = $(i); + return getItem(item); + }) + ); + + return { + title: '西南交大地学学院-研究生通知', + link: urlAddr, + item: items, + allowEmpty: true, + }; +} diff --git a/lib/routes/swjtu/namespace.ts b/lib/routes/swjtu/namespace.ts index be986e9ec5f80d..150a9a4c7a5b0c 100644 --- a/lib/routes/swjtu/namespace.ts +++ b/lib/routes/swjtu/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '西南交通大学', - url: 'ctt.swjtu.edu.cn', + url: 'www.swjtu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/swjtu/scai.ts b/lib/routes/swjtu/scai.ts new file mode 100644 index 00000000000000..2e76f8d936c240 --- /dev/null +++ b/lib/routes/swjtu/scai.ts @@ -0,0 +1,102 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; + +const rootURL = 'https://scai.swjtu.edu.cn'; + +export const route: Route = { + path: '/scai/:type', + categories: ['university'], + example: '/swjtu/scai/bks', + parameters: { type: '通知类型' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['scai.swjtu.edu.cn/'], + }, + ], + name: '计算机与人工智能学院', + description: ` +| 分区 | 参数 | +| ----------------- | ----------- | +| 本科生教育 | bks | +| 研究生教育 | yjs | +| 学生工作 | xsgz | +`, + maintainers: ['AzureG03', 'SuperJeason'], + handler, +}; + +const partition = { + bks: { + title: '本科生教育', + url: `${rootURL}/web/page-module.html?mid=B730BEB095B31840`, + }, + yjs: { + title: '研究生教育', + url: `${rootURL}/web/page-module.html?mid=6A69B0E32021446B`, + }, + xsgz: { + title: '学生工作', + url: `${rootURL}/web/page-module.html?mid=F3D3909EB1861B5D`, + }, +}; + +const getItem = (item, cache) => { + const title = item.find('a').text(); + const link = `${rootURL}${item.find('a').attr('href').slice(2)}`; + + return cache.tryGet(link, async () => { + const res = await ofetch(link); + const $ = load(res); + + let dateText = $('div.news-info span:nth-of-type(2)').text(); + // 转教务通知时的时间获取方法 + if (!dateText) { + dateText = $('div.news-top-bar span:nth-of-type(1)').text(); + } + // 'date' may be undefined. and 'parseDate' will return current time. + const date = dateText.match(/\d{4}(-|\/|.)\d{1,2}\1\d{1,2}/)?.[0]; + const pubDate = parseDate(date); + const description = $('div.content-main').html(); + + return { + title, + pubDate, + link, + description, + }; + }); +}; + +async function handler(ctx) { + const { type } = ctx.req.param(); + const url = partition[type].url; + const res = await ofetch(url); + + const $ = load(res); + const $list = $('div.list-top-item, div.item-wrapper'); + + const items = await Promise.all( + $list.toArray().map((i) => { + const $item = $(i); + return getItem($item, cache); + }) + ); + + return { + title: `西南交大计院-${partition[type].title}`, + link: url, + item: items, + allowEmpty: true, + }; +} diff --git a/lib/routes/swjtu/sports.ts b/lib/routes/swjtu/sports.ts new file mode 100644 index 00000000000000..8e8ef5dd265866 --- /dev/null +++ b/lib/routes/swjtu/sports.ts @@ -0,0 +1,77 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { ofetch } from 'ofetch'; + +const rootURL = 'https://sports.swjtu.edu.cn'; +const pageURL = `${rootURL}/xwzx.htm`; + +export const route: Route = { + path: '/sports', + categories: ['university'], + example: '/swjtu/sports', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['sports.swjtu.edu.cn/'], + }, + ], + name: '体育学院', + description: '新闻资讯', + maintainers: ['AzureG03'], + handler, +}; + +const getItem = (item, cache) => { + const title = item.find('p.toe').text(); + const link = `${rootURL}/${item.find('a').attr('href')}`; + + return cache.tryGet(link, async () => { + const res = await ofetch(link); + const $ = load(res); + + const pubDate = parseDate( + $('div.info span:nth-of-type(3)') + .text() + .slice(3) + .match(/\d{4}(-|\/|.)\d{1,2}\1\d{1,2}/)?.[0] + ); + const description = $('div.detail-wrap').html(); + return { + title, + pubDate, + link, + description, + }; + }); +}; + +async function handler() { + const res = await ofetch(pageURL); + + const $ = load(res); + const $list = $('div.news-list > ul > li'); + + const items = await Promise.all( + $list.toArray().map((i) => { + const $item = $(i); + return getItem($item, cache); + }) + ); + + return { + title: '西南交大体院-新闻资讯', + link: pageURL, + item: items, + allowEmpty: true, + }; +} diff --git a/lib/routes/swjtu/xg.ts b/lib/routes/swjtu/xg.ts index 06e734110d67eb..2556b8a52cff9d 100644 --- a/lib/routes/swjtu/xg.ts +++ b/lib/routes/swjtu/xg.ts @@ -75,9 +75,9 @@ export const route: Route = { url: 'xg.swjtu.edu.cn/web/Home/PushNewsList', description: `栏目列表: - | 通知公告 | 扬华新闻 | 多彩学院 | 学工之家 | - | -------- | -------- | -------- | -------- | - | tzgg | yhxw | dcxy | xgzj |`, +| 通知公告 | 扬华新闻 | 多彩学院 | 学工之家 | +| -------- | -------- | -------- | -------- | +| tzgg | yhxw | dcxy | xgzj |`, }; async function handler(ctx) { diff --git a/lib/routes/swpu/bgw.ts b/lib/routes/swpu/bgw.ts index 062bab150434f0..48800ee6c0af88 100644 --- a/lib/routes/swpu/bgw.ts +++ b/lib/routes/swpu/bgw.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { DataItem, Route, Data } from '@/types'; import cache from '@/utils/cache'; import { joinUrl } from './utils'; import { parseDate } from '@/utils/parse-date'; @@ -29,13 +29,13 @@ export const route: Route = { maintainers: ['CYTMWIA'], handler, url: 'swpu.edu.cn/', - description: `| 栏目 | 重要通知公告 | 部门通知公告 | 本周活动 | 学术报告 | - | ---- | ------------ | ------------ | -------- | -------- | - | 代码 | zytzgg | bmtzgg | bzhd | xsbg |`, + description: `| 栏目 | 重要通知公告 | 部门通知公告 | 本周活动 | +| ---- | ------------ | ------------ | -------- | +| 代码 | zytzgg | bmtzgg | bzhd |`, }; -async function handler(ctx) { - const url = `https://www.swpu.edu.cn/bgw2/${ctx.req.param('code')}.htm`; +async function handler(ctx): Promise<Data> { + const url = `https://www.swpu.edu.cn/bgw/${ctx.req.param('code')}.htm`; const res = await got.get(url); const $ = load(res.data); @@ -43,39 +43,37 @@ async function handler(ctx) { const title = $('.title').text(); // 获取标题、时间及链接 - const items = []; - $('.notice > ul > li > a').each((i, elem) => { - items.push({ + const items: DataItem[] = $('.notice > ul > li > a') + .toArray() + .map((elem) => ({ title: $(elem.children[0]).text(), pubDate: timezone(parseDate($(elem.children[1]).text()), +8), link: joinUrl('https://www.swpu.edu.cn', $(elem).attr('href')), // 实际获得连接 "../info/1312/17891.htm" - }); - }); + })); // 请求全文 - const out = await Promise.all( - items.map(async (item) => { - const $ = await cache.tryGet(item.link, async () => { - const res = await got.get(item.link); - return load(res.data); - }); - - if ($('title').text().startsWith('系统提示')) { - item.author = '系统'; - item.description = '无权访问'; - } else { - item.author = '办公网'; - item.description = $('.v_news_content').html(); - for (const elem of $('.v_news_content p')) { - if ($(elem).css('text-align') === 'right') { - item.author = $(elem).text(); - break; + const out: DataItem[] = await Promise.all( + items.map( + async (item: DataItem) => + (await cache.tryGet(item.link!, async () => { + const resp = await got.get(item.link); + const $ = load(resp.data); + if ($('title').text().startsWith('系统提示')) { + item.author = '系统'; + item.description = '无权访问'; + } else { + item.author = '办公网'; + item.description = $('.v_news_content').html()!; + for (const elem of $('.v_news_content p')) { + if ($(elem).css('text-align') === 'right') { + item.author = $(elem).text(); + break; + } + } } - } - } - - return item; - }) + return item; + })) as DataItem + ) ); return { diff --git a/lib/routes/swpu/cjxy.ts b/lib/routes/swpu/cjxy.ts index ef36972d4a1c25..e12ddcbc62aed8 100644 --- a/lib/routes/swpu/cjxy.ts +++ b/lib/routes/swpu/cjxy.ts @@ -29,8 +29,8 @@ export const route: Route = { handler, url: 'swpu.edu.cn/', description: `| 栏目 | 学院新闻 | 学院通知 | - | ---- | -------- | -------- | - | 代码 | xyxw | xytz |`, +| ---- | -------- | -------- | +| 代码 | xyxw | xytz |`, }; async function handler(ctx) { diff --git a/lib/routes/swpu/dean.ts b/lib/routes/swpu/dean.ts index 9072e813e1e60f..3d4ad7c276024a 100644 --- a/lib/routes/swpu/dean.ts +++ b/lib/routes/swpu/dean.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { DataItem, Route, Data } from '@/types'; import cache from '@/utils/cache'; import { joinUrl } from './utils'; import { parseDate } from '@/utils/parse-date'; @@ -30,11 +30,11 @@ export const route: Route = { handler, url: 'swpu.edu.cn/', description: `| 栏目 | 通知公告 | 新闻报道 | 视点声音 | - | ---- | -------- | -------- | -------- | - | 代码 | tzgg | xwbd | sdsy |`, +| ---- | -------- | -------- | -------- | +| 代码 | tzgg | xwbd | sdsy |`, }; -async function handler(ctx) { +async function handler(ctx): Promise<Data> { const url = `https://www.swpu.edu.cn/dean/${ctx.req.param('code')}.htm`; const res = await got.get(url); @@ -44,39 +44,37 @@ async function handler(ctx) { title = title.substring(title.indexOf(':') + 1); // 获取标题、时间及链接 - const items = []; - $('.r_list > ul > li').each((i, elem) => { - items.push({ + const items: DataItem[] = $('.r_list > ul > li') + .toArray() + .map((elem) => ({ title: $('label:eq(0)', elem).text().trim(), link: joinUrl('https://www.swpu.edu.cn/dean/', $('a', elem).attr('href')), - }); - }); + })); // 请求全文 const out = await Promise.all( - items.map(async (item) => { - const $ = await cache.tryGet(item.link, async () => { - const res = await got.get(item.link); - return load(res.data); - }); - - if ($('title').text().startsWith('系统提示')) { - item.author = '系统'; - item.description = '无权访问'; - } else { - item.author = '教务处'; - item.description = $('.v_news_content').html(); - item.pubDate = timezone(parseDate($('#lbDate').text(), '更新时间:YYYY年MM月DD日'), +8); - for (const elem of $('.v_news_content p')) { - if ($(elem).css('text-align') === 'right') { - item.author = $(elem).text(); - break; + items.map( + async (item) => + (await cache.tryGet(item.link!, async () => { + const resp = await got.get(item.link); + const $ = load(resp.data); + if ($('title').text().startsWith('系统提示')) { + item.author = '系统'; + item.description = '无权访问'; + } else { + item.author = '教务处'; + item.description = $('.v_news_content').html()!; + item.pubDate = timezone(parseDate($('#lbDate').text(), '更新时间:YYYY年MM月DD日'), +8); + for (const elem of $('.v_news_content p')) { + if ($(elem).css('text-align') === 'right') { + item.author = $(elem).text(); + break; + } + } } - } - } - - return item; - }) + return item; + })) as DataItem + ) ); return { diff --git a/lib/routes/swpu/dxy.ts b/lib/routes/swpu/dxy.ts index 404b524d04da39..b446be8d6de1af 100644 --- a/lib/routes/swpu/dxy.ts +++ b/lib/routes/swpu/dxy.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { DataItem, Route, Data } from '@/types'; import cache from '@/utils/cache'; import { joinUrl } from './utils'; import { parseDate } from '@/utils/parse-date'; @@ -30,11 +30,11 @@ export const route: Route = { handler, url: 'swpu.edu.cn/', description: `| 栏目 | 学院新闻 | 学院通知 | - | ---- | -------- | -------- | - | 代码 | 1122 | 1156 |`, +| ---- | -------- | -------- | +| 代码 | 1122 | 1156 |`, }; -async function handler(ctx) { +async function handler(ctx): Promise<Data> { // 移除 urltype=tree.TreeTempUrl 虽然也能顺利访问页面, // 但标题会缺失,而且在其他地方定位提取标题也比较麻烦。 const url = `https://www.swpu.edu.cn/dxy/list1.jsp?urltype=tree.TreeTempUrl&wbtreeid=${ctx.req.param('code')}`; @@ -46,39 +46,37 @@ async function handler(ctx) { title = title.substring(0, title.indexOf('-')); // 获取标题、时间及链接 - const items = []; - $('tr[height="20"]').each((i, elem) => { - items.push({ + const items: DataItem[] = $('tr[height="20"]') + .toArray() + .map((elem) => ({ title: $('a[title]', elem).text().trim(), pubDate: timezone(parseDate($('td:eq(1)', elem).text(), 'YYYY年MM月DD日'), +8), link: joinUrl('https://www.swpu.edu.cn/dxy/', $('a[title]', elem).attr('href')), - }); - }); + })); // 请求全文 const out = await Promise.all( - items.map(async (item) => { - const $ = await cache.tryGet(item.link, async () => { - const res = await got.get(item.link); - return load(res.data); - }); - - if ($('title').text().startsWith('系统提示')) { - item.author = '系统'; - item.description = '无权访问'; - } else { - item.author = '电气信息学院'; - item.description = $('.v_news_content').html(); - for (const elem of $('.v_news_content p')) { - if ($(elem).css('text-align') === 'right') { - item.author = $(elem).text(); - break; + items.map( + async (item) => + (await cache.tryGet(item.link!, async () => { + const resp = await got.get(item.link); + const $ = load(resp.data); + if ($('title').text().startsWith('系统提示')) { + item.author = '系统'; + item.description = '无权访问'; + } else { + item.author = '电气信息学院'; + item.description = $('.v_news_content').html()!; + for (const elem of $('.v_news_content p')) { + if ($(elem).css('text-align') === 'right') { + item.author = $(elem).text(); + break; + } + } } - } - } - - return item; - }) + return item; + })) as DataItem + ) ); return { diff --git a/lib/routes/swpu/is.ts b/lib/routes/swpu/is.ts index 0a9307543de540..71fba7a77f308f 100644 --- a/lib/routes/swpu/is.ts +++ b/lib/routes/swpu/is.ts @@ -29,8 +29,8 @@ export const route: Route = { handler, url: 'swpu.edu.cn/', description: `| 栏目 | 学院新闻 | 通知公告 | 教育教学 | 学生工作 | 招生就业 | - | ---- | -------- | -------- | -------- | -------- | -------- | - | 代码 | xyxw | tzgg | jyjx | xsgz | zsjy |`, +| ---- | -------- | -------- | -------- | -------- | -------- | +| 代码 | xyxw | tzgg | jyjx | xsgz | zsjy |`, }; async function handler(ctx) { diff --git a/lib/routes/swpu/namespace.ts b/lib/routes/swpu/namespace.ts index 3c1b5370329b29..1b609d0475fda5 100644 --- a/lib/routes/swpu/namespace.ts +++ b/lib/routes/swpu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '西南石油大学', url: 'swpu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/swpu/scs.ts b/lib/routes/swpu/scs.ts index 63d5f15644e3e9..0a39d6e723a992 100644 --- a/lib/routes/swpu/scs.ts +++ b/lib/routes/swpu/scs.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { DataItem, Route, Data } from '@/types'; import cache from '@/utils/cache'; import { joinUrl } from './utils'; import { parseDate } from '@/utils/parse-date'; @@ -25,16 +25,16 @@ export const route: Route = { target: '', }, ], - name: '计算机科学学院', + name: '计算机与软件学院', maintainers: ['CYTMWIA'], handler, url: 'swpu.edu.cn/', description: `| 栏目 | 通知公告 | 新闻速递 | - | ---- | -------- | -------- | - | 代码 | tzgg | xwsd |`, +| ---- | -------- | -------- | +| 代码 | tzgg | xwsd |`, }; -async function handler(ctx) { +async function handler(ctx): Promise<Data> { const url = `https://www.swpu.edu.cn/scs/index/${ctx.req.param('code')}.htm`; const res = await got.get(url); @@ -43,45 +43,43 @@ async function handler(ctx) { const title = $('.r_list > h3').text(); // 获取标题、时间及链接 - const items = []; - $('.main_conRCb > ul > li').each((i, elem) => { - items.push({ + const items: DataItem[] = $('.main_conRCb > ul > li') + .toArray() + .map((elem) => ({ title: $('em', elem).text().trim(), pubDate: timezone(parseDate($('span', elem).text()), +8), link: joinUrl('https://www.swpu.edu.cn/scs/index/', $('a', elem).attr('href')), - }); - }); + })); // 请求全文 const out = await Promise.all( - items.map(async (item) => { - const $ = await cache.tryGet(item.link, async () => { - const res = await got.get(item.link); - return load(res.data); - }); - - if ($('title').text().startsWith('系统提示')) { - item.author = '系统'; - item.description = '无权访问'; - } else { - item.author = '计算机科学学院'; - item.description = $('.v_news_content').html(); - for (const elem of $('.v_news_content p')) { - if ($(elem).css('text-align') === 'right') { - item.author = $(elem).text(); - break; + items.map( + async (item) => + (await cache.tryGet(item.link!, async () => { + const resp = await got.get(item.link); + const $ = load(resp.data); + if ($('title').text().startsWith('系统提示')) { + item.author = '系统'; + item.description = '无权访问'; + } else { + item.author = '计算机与软件学院'; + item.description = $('.v_news_content').html()!; + for (const elem of $('.v_news_content p')) { + if ($(elem).css('text-align') === 'right') { + item.author = $(elem).text(); + break; + } + } } - } - } - - return item; - }) + return item; + })) as DataItem + ) ); return { - title: `西南石油大学计算机科学学院 ${title}`, + title: `西南石油大学计算机与软件学院 ${title}`, link: url, - description: `西南石油大学计算机科学学院 ${title}`, + description: `西南石油大学计算机与软件学院 ${title}`, language: 'zh-CN', item: out, }; diff --git a/lib/routes/sycl/feeds.ts b/lib/routes/sycl/feeds.ts new file mode 100644 index 00000000000000..6da6f50b412245 --- /dev/null +++ b/lib/routes/sycl/feeds.ts @@ -0,0 +1,47 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/:feed?', + categories: ['programming'], + example: '/sycl/news', + parameters: { feed: 'Feed source, defaults to news, references https://feeds.sycl.tech/' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Feeds', + maintainers: ['mocusez'], + handler, + description: `| Events | News | Research Paper | Videos | +| :----: | :--: | :-------------: | :----: | +| events | news | research_papers | videos |`, +}; + +async function handler(ctx) { + const feeds: string[] = ['news', 'events', 'research_papers', 'videos']; + let { feed = 'news' } = ctx.req.param(); + if (!feeds.includes(feed)) { + feed = 'news'; + } + const data = await ofetch(`https://feeds.sycl.tech/${feed}/feed.json`); + + const items = data.items.map((item) => ({ + title: item.title, + link: item.external_url, + description: item.content_html, + pubDate: parseDate(item.date_published), + author: item.author.name, + })); + + return { + title: `SYCL.tech ${feed}`, + link: `https://feeds.sycl.tech/${feed}/feed.json`, + item: items, + }; +} diff --git a/lib/routes/sycl/namespace.ts b/lib/routes/sycl/namespace.ts new file mode 100644 index 00000000000000..38dfbc30135b99 --- /dev/null +++ b/lib/routes/sycl/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'SYCL', + url: 'sycl.tech', + lang: 'en', +}; diff --git a/lib/routes/syosetu/chapter.ts b/lib/routes/syosetu/chapter.ts deleted file mode 100644 index 5fb22c03523bc9..00000000000000 --- a/lib/routes/syosetu/chapter.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; -import { CookieJar } from 'tough-cookie'; - -const cookieJar = new CookieJar(); -cookieJar.setCookieSync('over18=yes', 'https://novel18.syosetu.com/'); - -export const route: Route = { - path: '/chapter/:id', - categories: ['reading'], - example: '/syosetu/chapter/n1976ey', - parameters: { id: 'Novel id, can be found in URL' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['novel18.syosetu.com/:id'], - }, - ], - name: 'chapter', - maintainers: ['huangliangshusheng'], - handler, - description: `Eg: \`https://ncode.syosetu.com/n1976ey/\``, -}; - -async function handler(ctx) { - const id = ctx.req.param('id'); - const limit = Number.parseInt(ctx.req.query('limit')) || 5; - const link = `https://ncode.syosetu.com/${id}`; - const $ = load(await get(link)); - - const title = $('p.novel_title').text(); - const description = $('#novel_ex').html(); - - const chapter_list = $('dl.novel_sublist2') - .map((_, chapter) => { - const $_chapter = $(chapter); - const chapter_link = $_chapter.find('a'); - return { - title: chapter_link.text(), - link: chapter_link.attr('href'), - pubDate: timezone(parseDate($_chapter.find('dt').text(), 'YYYY/MM/DD HH:mm'), +9), - }; - }) - .toArray() - .sort((a, b) => (a.pubDate <= b.pubDate ? 1 : -1)) - .slice(0, limit); - - const item_list = await Promise.all( - chapter_list.map((chapter) => - cache.tryGet(chapter.link, async () => { - chapter.link = `https://ncode.syosetu.com${chapter.link}`; - const content = load(await get(chapter.link)); - chapter.description = content('#novel_honbun').html(); - return chapter; - }) - ) - ); - - return { - title, - description, - link, - language: 'ja', - item: item_list, - }; -} - -const get = async (url) => { - const response = await got({ - method: 'get', - url, - cookieJar, - }); - - return response.data; -}; diff --git a/lib/routes/syosetu/dev.ts b/lib/routes/syosetu/dev.ts new file mode 100644 index 00000000000000..64dd139c7db1dc --- /dev/null +++ b/lib/routes/syosetu/dev.ts @@ -0,0 +1,67 @@ +import { Data, Route } from '@/types'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/dev', + categories: ['program-update'], + example: '/syosetu/dev', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'なろう小説 API の更新履歴', + url: 'dev.syosetu.com', + maintainers: ['SnowAgar25'], + handler, + radar: [ + { + title: 'なろう小説 API の更新履歴', + source: ['dev.syosetu.com'], + target: '/dev', + }, + ], +}; + +async function handler(): Promise<Data> { + const url = 'https://dev.syosetu.com'; + + const data = await ofetch(url); + const $ = load(data); + + const logContainer = $('.c-log'); + + const dates = logContainer + .find('dt') + .toArray() + .map((element) => $(element).text().trim()); + + const contents = logContainer + .find('dd') + .toArray() + .map((element) => $(element).text().trim()); + + const updates = dates + .map((date, index) => ({ + date, + content: contents[index]?.replace(/\n/g, '<br>') ?? '', + })) + .filter((update) => update.content); + + return { + title: 'なろうデベロッパー - なろう小説 API の更新履歴', + link: url, + language: 'ja', + item: updates.map((update) => ({ + title: update.date, + description: update.content, + pubDate: parseDate(update.date.replace('/', '-')), + guid: `syosetu:dev:${update.date}`, + })), + }; +} diff --git a/lib/routes/syosetu/index.ts b/lib/routes/syosetu/index.ts new file mode 100644 index 00000000000000..0779793e3f376a --- /dev/null +++ b/lib/routes/syosetu/index.ts @@ -0,0 +1,85 @@ +import { Route, Data, DataItem } from '@/types'; +import { fetchNovelInfo, fetchChapterContent } from './utils'; +import { Context } from 'hono'; +import { NovelType } from 'narou'; + +export const route: Route = { + path: '/:ncode', + categories: ['reading'], + example: '/syosetu/n9292ii', + parameters: { + ncode: 'Novel code, can be found in URL', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Novel Updates', + maintainers: ['eternasuno', 'SnowAgar25'], + handler, + radar: [ + { + title: 'Novel Updates', + source: ['ncode.syosetu.com/:ncode', 'ncode.syosetu.com/:ncode/:chapter'], + target: '/:ncode', + }, + { + title: 'Novel Updates', + source: ['novel18.syosetu.com/:ncode', 'novel18.syosetu.com/:ncode/:chapter'], + target: '/:ncode', + }, + ], +}; + +async function handler(ctx: Context): Promise<Data> { + const { ncode } = ctx.req.param(); + const limit = Math.min(Number(ctx.req.query('limit') ?? 5), 20); + + const { baseUrl, novel } = await fetchNovelInfo(ncode); + novel.story = novel.story.replaceAll('\n', '<br>') || ''; + + // Tanpen = Short + if (novel.noveltype === NovelType.Tanpen) { + const chapterUrl = `${baseUrl}/${ncode}`; + const item = await fetchChapterContent(chapterUrl); + + // Shorts are updated rather than having new chapters + // Use novelupdated_at as pubDate since RSS 2.0 doesn't have updated field + item.pubDate = novel.novelupdated_at; + + return { + title: novel.title, + description: novel.story, + link: chapterUrl, + item: [item] as DataItem[], + language: 'ja', + }; + } + + // Rensai = Series + // if (novel.noveltype === NovelType.Rensai) + const totalChapters = novel.general_all_no ?? 1; + const startChapter = Math.max(totalChapters - limit + 1, 1); + + const items = await Promise.all( + Array.from({ length: Math.min(limit, totalChapters) }, async (_, index) => { + const chapterNumber = startChapter + index; + const chapterUrl = `${baseUrl}/${ncode}/${chapterNumber}`; + + const item = await fetchChapterContent(chapterUrl, chapterNumber); + return item; + }).reverse() + ); + + return { + title: novel.title, + description: novel.story, + link: `${baseUrl}/${ncode}`, + item: items as DataItem[], + language: 'ja', + }; +} diff --git a/lib/routes/syosetu/namespace.ts b/lib/routes/syosetu/namespace.ts index 4bec78aa0b6cf6..704df0a4304a0b 100644 --- a/lib/routes/syosetu/namespace.ts +++ b/lib/routes/syosetu/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'syosetu', - url: 'ncode.syosetu.com', + name: 'Syosetu', + url: 'syosetu.com', + lang: 'ja', }; diff --git a/lib/routes/syosetu/ranking-isekai.ts b/lib/routes/syosetu/ranking-isekai.ts new file mode 100644 index 00000000000000..09f05cd51bed47 --- /dev/null +++ b/lib/routes/syosetu/ranking-isekai.ts @@ -0,0 +1,93 @@ +import { Data, DataItem } from '@/types'; +import { NarouNovelFetch, SearchBuilder, SearchParams, BigGenre } from 'narou'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import { Join } from 'narou/util/type'; +import { RankingPeriod, NovelType, periodToJapanese, novelTypeToJapanese, periodToOrder, periodToPointField, IsekaiCategory, isekaiCategoryToJapanese } from './types/ranking'; + +const __dirname = getCurrentPath(import.meta.url); + +export function parseIsekaiRankingType(type: string): { period: RankingPeriod; category: IsekaiCategory; novelType: NovelType } { + const [periodStr, categoryStr, novelTypeStr = NovelType.TOTAL] = type.split('_'); + + const period = periodStr as RankingPeriod; + const category = categoryStr as IsekaiCategory; + const novelType = novelTypeStr as NovelType; + + const isValid = [Object.values(RankingPeriod).includes(period), Object.values(IsekaiCategory).includes(category), Object.values(NovelType).includes(novelType)].every(Boolean); + + if (!isValid) { + throw new InvalidParameterError(`Invalid isekai ranking type: ${type}`); + } + + return { period, category, novelType }; +} + +function getIsekaiSearchParams(period, category, novelType, limit): SearchParams { + const searchParams: SearchParams = { + order: periodToOrder[period], + gzip: 5, + // Request 20% more items to compensate for potential duplicates between tensei/tenni + lim: Math.ceil((limit / 2) * 1.2), + }; + + if (novelType !== NovelType.TOTAL) { + searchParams.type = novelType; + } + + switch (category) { + case IsekaiCategory.RENAI: + searchParams.biggenre = BigGenre.Renai; + break; + case IsekaiCategory.FANTASY: + searchParams.biggenre = BigGenre.Fantasy; + break; + case IsekaiCategory.OTHER: + searchParams.biggenre = `${BigGenre.Bungei}-${BigGenre.Sf}-${BigGenre.Sonota}` as Join<BigGenre>; + break; + default: + throw new InvalidParameterError(`Invalid Isekai category: ${category}`); + } + + return searchParams; +} + +export async function handleIsekaiRanking(type: string, limit: number): Promise<Data> { + const { period, category, novelType } = parseIsekaiRankingType(type); + const rankingUrl = `https://yomou.syosetu.com/rank/isekailist/type/${type}`; + const rankingTitle = `[${periodToJapanese[period]}] 異世界転生/転移${isekaiCategoryToJapanese[category]}ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`; + + const searchParams = getIsekaiSearchParams(period, category, novelType, limit); + const api = new NarouNovelFetch(); + + const [tenseiResult, tenniResult] = await Promise.all([new SearchBuilder({ ...searchParams, istensei: 1 }, api).execute(), new SearchBuilder({ ...searchParams, istenni: 1 }, api).execute()]); + + const combinedNovels = [...tenseiResult.values, ...tenniResult.values]; + const uniqueNovels = [...new Map(combinedNovels.map((novel) => [novel.ncode, novel])).values()]; + + const pointField = periodToPointField[period]; + if (!pointField) { + throw new InvalidParameterError(`Invalid period: ${period}`); + } + + const items = uniqueNovels + .sort((a, b) => (b[pointField] || 0) - (a[pointField] || 0)) + .map((novel, index) => ({ + title: `#${index + 1} ${novel.title}`, + link: `https://ncode.syosetu.com/${String(novel.ncode).toLowerCase()}`, + description: art(path.join(__dirname, 'templates', 'description.art'), { + novel, + }), + author: novel.writer, + category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean), + })); + + return { + title: `小説家になろう - ${rankingTitle}`, + link: rankingUrl, + item: items.slice(0, limit) as DataItem[], + language: 'ja', + }; +} diff --git a/lib/routes/syosetu/ranking-r18.ts b/lib/routes/syosetu/ranking-r18.ts new file mode 100644 index 00000000000000..bae375da5d4c00 --- /dev/null +++ b/lib/routes/syosetu/ranking-r18.ts @@ -0,0 +1,192 @@ +import { Route, Data, DataItem } from '@/types'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { Context } from 'hono'; +import { SearchBuilderR18, SearchParams, NarouNovelFetch } from 'narou'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import { getCurrentPath } from '@/utils/helpers'; +import { RankingPeriod, periodToJapanese, novelTypeToJapanese, periodToOrder, NovelType, SyosetuSub, syosetuSubToJapanese, syosetuSubToNocgenre } from './types/ranking-r18'; + +const __dirname = getCurrentPath(import.meta.url); + +/** + * Implementation of "Syosetu" R18 Rankings + * + * While "Syosetu" only provides ranking API for "Syosetu o yomou" (general audience), + * equivalent ranking functionality can be achieved using the point-based sorting in the search API. + * + * This implementation utilizes the 'order' parameter (e.g., dailypoint, weeklypoint) + * of the search API to replicate ranking functionality across all Syosetu subsidiary sites. + */ + +const getParameters = () => { + // Generate options for sub parameter + const subOptions = Object.entries(SyosetuSub).map(([, value]) => ({ + value, + label: syosetuSubToJapanese[value], + })); + + // Generate period options + const periodOptions = Object.entries(RankingPeriod).map(([key, value]) => ({ + value, + label: `${periodToJapanese[value]} (${key})`, + })); + + // Generate novel type options + const novelTypeOptions = Object.entries(NovelType).map(([key, value]) => ({ + value, + label: `${novelTypeToJapanese[value]} (${key})`, + })); + + return { + sub: { + description: 'Target site for R18 rankings', + options: subOptions, + }, + type: { + description: 'Detailed ranking type (format: period_noveltype)', + options: periodOptions.flatMap((period) => + novelTypeOptions.map((type) => ({ + value: `${period.value}_${type.value}`, + label: `${period.label} ${type.label}`, + })) + ), + }, + }; +}; + +const getBest5RadarItems = () => + Object.entries(SyosetuSub).flatMap(([, domain]) => + Object.values(RankingPeriod).map((period) => ({ + title: `${syosetuSubToJapanese[domain]} ${periodToJapanese[period]}ランキング BEST5`, + source: [`${domain === SyosetuSub.MOONLIGHT_BL ? SyosetuSub.MOONLIGHT : domain}.syosetu.com/rank/${domain === SyosetuSub.MOONLIGHT_BL ? 'bltop' : 'top'}`], + target: `/rankingr18/${domain}/${period}_${NovelType.TOTAL}?limit=5`, + })) + ); + +export const route: Route = { + path: '/rankingr18/:sub/:type', + categories: ['reading'], + example: '/syosetu/rankingr18/noc/daily_total?limit=50', + parameters: getParameters(), + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'R18 Rankings', + url: 'syosetu.com/site/group', + maintainers: ['SnowAgar25'], + handler, + description: ` +| Period | Description | 説明 | +| --- | --- | --- | +| daily | Daily Ranking | 日間ランキング | +| weekly | Weekly Ranking | 週間ランキング | +| monthly | Monthly Ranking | 月間ランキング | +| quarter | Quarterly Ranking | 四半期ランキング | +| yearly | Yearly Ranking | 年間ランキング | + +| Novel Type | Description | 説明 | +| --- | --- | --- | +| total | All Works | 総合 | +| t | Short Stories | 短編 | +| r | Ongoing Series | 連載中 | +| er | Completed Series | 完結済 | + +::: tip +Combine Period and Novel Type with \`_\`. +For example: \`daily_total\`, \`weekly_r\`, \`monthly_er\` +:::`, + radar: [ + { + source: ['noc.syosetu.com/rank/list/type/:type'], + target: '/rankingr18/noc/:type', + }, + { + source: ['mid.syosetu.com/rank/list/type/:type'], + target: '/rankingr18/mid/:type', + }, + { + source: ['mnlt.syosetu.com/rank/list/type/:type'], + target: '/rankingr18/mnlt/:type', + }, + { + source: ['mnlt.syosetu.com/rank/bllist/type/:type'], + target: '/rankingr18/mnlt-bl/:type', + }, + ...getBest5RadarItems(), + ], +}; + +function parseRankingType(type: string): { period: RankingPeriod; novelType: NovelType } { + const [periodStr, novelTypeStr] = type.split('_'); + + const period = periodStr as RankingPeriod; + const novelType = novelTypeStr as NovelType; + + const isValid = [Object.values(RankingPeriod).includes(period), Object.values(NovelType).includes(novelType)].every(Boolean); + + if (!isValid) { + throw new InvalidParameterError(`Invalid ranking type: ${type}`); + } + + return { + period: periodStr as RankingPeriod, + novelType: novelTypeStr as NovelType, + }; +} + +function getRankingTitle(type: string, limit: number): string { + const { period, novelType } = parseRankingType(type); + return `${periodToJapanese[period]}${novelTypeToJapanese[novelType]}ランキング BEST${limit}`; +} + +async function handler(ctx: Context): Promise<Data> { + const { sub, type } = ctx.req.param(); + const baseUrl = `https://${sub === SyosetuSub.MOONLIGHT_BL ? SyosetuSub.MOONLIGHT : sub}.syosetu.com`; + const rankingUrl = `${baseUrl}/rank/list/type/${type}`; + const api = new NarouNovelFetch(); + + const limit = Math.min(Number(ctx.req.query('limit') ?? 300), 300); + const { period, novelType } = parseRankingType(type); + + const searchParams: SearchParams = { + gzip: 5, + lim: limit, + order: periodToOrder[period], + }; + + // TOTAL: Skip type filter to get all types combined + if (novelType !== NovelType.TOTAL) { + searchParams.type = novelType; + } + + if (!(sub in syosetuSubToNocgenre)) { + throw new InvalidParameterError(`Invalid subsite: ${sub}`); + } + const nocgenre = syosetuSubToNocgenre[sub]; + + const builder = new SearchBuilderR18(searchParams, api).r18Site(nocgenre); + const result = await builder.execute(); + + const items = result.values.map((novel, index) => ({ + title: `#${index + 1} ${novel.title}`, + link: `https://novel18.syosetu.com/${String(novel.ncode).toLowerCase()}`, + description: art(path.join(__dirname, 'templates', 'description.art'), { + novel, + }), + author: novel.writer, + category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean), + })); + + return { + title: `小説家になろう (${sub}) - ${getRankingTitle(type, limit)}`, + link: rankingUrl, + item: items as DataItem[], + language: 'ja', + }; +} diff --git a/lib/routes/syosetu/ranking.ts b/lib/routes/syosetu/ranking.ts new file mode 100644 index 00000000000000..2c91406bfa59f0 --- /dev/null +++ b/lib/routes/syosetu/ranking.ts @@ -0,0 +1,285 @@ +import { Route, Data, DataItem } from '@/types'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { Context } from 'hono'; +import { Genre, SearchBuilder, SearchParams, NarouNovelFetch, GenreNotation } from 'narou'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import { getCurrentPath } from '@/utils/helpers'; +import { handleIsekaiRanking } from './ranking-isekai'; +import { RankingPeriod, periodToJapanese, novelTypeToJapanese, periodToOrder, RankingType, NovelType, isekaiCategoryToJapanese, IsekaiCategory } from './types/ranking'; + +const __dirname = getCurrentPath(import.meta.url); + +const getParameters = () => { + // Generate ranking type options + const rankingTypeOptions = [ + { value: RankingType.LIST, label: '総合ランキング (General Ranking)' }, + { value: RankingType.GENRE, label: 'ジャンル別ランキング (Genre Ranking)' }, + { value: RankingType.ISEKAI, label: '異世界転生/転移ランキング (Isekai Ranking)' }, + ]; + + // Generate period options + const periodOptions = Object.entries(RankingPeriod).map(([key, value]) => ({ + value, + label: `${periodToJapanese[value]} (${key})`, + })); + + // Generate novel type options + const novelTypeOptions = Object.entries(NovelType).map(([key, value]) => ({ + value, + label: `${novelTypeToJapanese[value]} (${key})`, + })); + + // Generate genre options + const genreOptions = Object.entries(Genre) + .filter(([, value]) => typeof value === 'number') // Filter out reverse mappings + .map(([key, value]) => ({ + value: value.toString(), + label: key, + })); + + // Generate isekai category options + const isekaiOptions = Object.entries(IsekaiCategory).map(([key, value]) => ({ + value, + label: `${isekaiCategoryToJapanese[value]} (${key})`, + })); + + return { + listType: { + description: 'Ranking type', + options: rankingTypeOptions, + }, + type: { + description: 'Detailed ranking type, can be found in Syosetu ranking URLs', + options: [ + // General ranking options + ...periodOptions.flatMap((period) => + novelTypeOptions.map((novelType) => ({ + value: `${period.value}_${novelType.value}`, + label: `${RankingType.LIST} - [${periodToJapanese[period.value]}] 総合ランキング - ${novelTypeToJapanese[novelType.value]}`, + })) + ), + // Genre ranking options + ...periodOptions.flatMap((period) => + genreOptions.flatMap((genre) => + novelTypeOptions.map((novelType) => ({ + value: `${period.value}_${genre.value}_${novelType.value}`, + label: `${RankingType.GENRE} - [${periodToJapanese[period.value]}] ${GenreNotation[genre.value]}ランキング - ${novelTypeToJapanese[novelType.value]}`, + })) + ) + ), + // Isekai ranking options + ...periodOptions.flatMap((period) => + isekaiOptions.flatMap((category) => + novelTypeOptions.map((novelType) => ({ + value: `${period.value}_${category.value}_${novelType.value}`, + label: `${RankingType.ISEKAI} - [${periodToJapanese[period.value]}] 異世界転生/転移${isekaiCategoryToJapanese[category.value]}ランキング - ${novelTypeToJapanese[novelType.value]}`, + })) + ) + ), + ], + }, + }; +}; + +const getBest5RadarItems = () => { + // List + const periodRankings = Object.values(RankingPeriod).map((period) => ({ + title: `${periodToJapanese[period]}ランキング BEST5`, + source: ['yomou.syosetu.com/rank/top/'], + target: `/ranking/list/${period}_total?limit=5`, + })); + + // Genre + const genreRankings = Object.entries(Genre) + .filter(([, value]) => typeof value === 'number' && value !== Genre.SonotaReplay && value !== Genre.NonGenre) + .map(([, value]) => ({ + title: `[${periodToJapanese.daily}] ${GenreNotation[value]}ランキング BEST5`, + source: ['yomou.syosetu.com/rank/top/'], + target: `/ranking/genre/daily_${value}_total?limit=5`, + })); + + // Isekai + const isekaiRankings = Object.values(IsekaiCategory).map((category) => ({ + title: `[${periodToJapanese.daily}] 異世界転生/転移${isekaiCategoryToJapanese[category]}ランキング BEST5`, + source: ['yomou.syosetu.com/rank/top/'], + target: `/ranking/isekai/daily_${category}_total?limit=5`, + })); + + return [...periodRankings, ...genreRankings, ...isekaiRankings]; +}; + +export const route: Route = { + path: '/ranking/:listType/:type', + categories: ['reading'], + example: '/syosetu/ranking/list/daily_total?limit=50', + parameters: getParameters(), + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Rankings', + url: 'yomou.syosetu.com/rank/top', + maintainers: ['SnowAgar25'], + handler, + description: ` +| Keyword | Description | 説明 | +| --- | --- | --- | +| list | Overall Ranking | 総合ランキング | +| genre | Genre Ranking | ジャンル別ランキング | +| isekai | Isekai/Reincarnation/Transfer Ranking | 異世界転生/転移ランキング | + +| Period | Description | +| --- | --- | +| daily | Daily Ranking | +| weekly | Weekly Ranking | +| monthly | Monthly Ranking | +| quarter | Quarterly Ranking | +| yearly | Yearly Ranking | + + +| Type | Description | +| --- | --- | +| total | All Works | +| t | Short Stories | +| r | Ongoing Series | +| er | Completed Series | + +::: warning +Please note that novel type options may vary depending on the ranking category. + +ランキングの種類によって、小説タイプが異なる場合がございますのでご注意ください。 +::: + +::: danger 注意事項 +The "注目度ランキング" (Attention Ranking) is not supported as syosetu does not provide a public API for this feature and the results cannot be replicated through the search API. + +「注目度ランキング」については、API が非公開で検索 API でも同様の結果を得ることができないため、本 Route ではサポートしておりません。 +::: + +::: tip 異世界転生/転移ランキングについて (Isekai) +When multiple works have the same points, their order may differ from syosetu's ranking as syosetu randomizes the order for works with identical points. + +集計の結果、同じポイントの作品が複数存在する場合、Syosetu ではランダムで順位が決定されるため、本 Route の順位と異なる場合があります。 +::: +`, + radar: [ + { + source: ['yomou.syosetu.com/rank/list/type/:type'], + target: '/ranking/list/:type', + }, + { + source: ['yomou.syosetu.com/rank/genrelist/type/:type'], + target: '/ranking/genre/:type', + }, + { + source: ['yomou.syosetu.com/rank/isekailist/type/:type'], + target: '/ranking/isekai/:type', + }, + ...getBest5RadarItems(), + ], +}; + +function parseGeneralRankingType(type: string): { period: RankingPeriod; novelType: NovelType } { + const [periodStr, novelTypeStr] = type.split('_'); + + const period = periodStr as RankingPeriod; + const novelType = novelTypeStr as NovelType; + + const isValid = [Object.values(RankingPeriod).includes(period), Object.values(NovelType).includes(novelType)].every(Boolean); + + if (!isValid) { + throw new InvalidParameterError(`Invalid general ranking type: ${type}`); + } + + return { period, novelType }; +} + +function parseGenreRankingType(type: string): { period: RankingPeriod; genre: number; novelType: NovelType } { + const [periodStr, genreStr, novelTypeStr = NovelType.TOTAL] = type.split('_'); + + const period = periodStr as RankingPeriod; + const genre = Number(genreStr) as Genre; + const novelType = novelTypeStr as NovelType; + + const isValid = [Object.values(RankingPeriod).includes(period), Object.values(Genre).includes(genre), Object.values(NovelType).includes(novelType), genre !== Genre.SonotaReplay, genre !== Genre.NonGenre].every(Boolean); + + if (!isValid) { + throw new InvalidParameterError(`Invalid genre ranking type: ${type}`); + } + + return { period, genre, novelType }; +} + +async function handler(ctx: Context): Promise<Data> { + const { listType, type } = ctx.req.param(); + const rankingType = listType as RankingType; + const limit = Math.min(Number(ctx.req.query('limit') ?? 300), 300); + + const api = new NarouNovelFetch(); + const searchParams: SearchParams = { + gzip: 5, + lim: limit, + }; + + let rankingUrl: string; + let rankingTitle: string; + + // Build search parameters and titles based on ranking type + switch (rankingType) { + case RankingType.LIST: { + const { period, novelType } = parseGeneralRankingType(type); + rankingUrl = `https://yomou.syosetu.com/rank/list/type/${type}`; + rankingTitle = `[${periodToJapanese[period]}] 総合ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`; + + searchParams.order = periodToOrder[period]; + if (novelType !== NovelType.TOTAL) { + searchParams.type = novelType; + } + break; + } + + case RankingType.GENRE: { + const { period, genre, novelType } = parseGenreRankingType(type); + rankingUrl = `https://yomou.syosetu.com/rank/genrelist/type/${type}`; + rankingTitle = `[${periodToJapanese[period]}] ${GenreNotation[genre]}ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`; + + searchParams.order = periodToOrder[period]; + searchParams.genre = genre as Genre; + if (novelType !== NovelType.TOTAL) { + searchParams.type = novelType; + } + break; + } + + case RankingType.ISEKAI: + return handleIsekaiRanking(type, limit); + + default: + throw new InvalidParameterError(`Invalid ranking type: ${type}`); + } + + const builder = new SearchBuilder(searchParams, api); + const result = await builder.execute(); + + const items = result.values.map((novel, index) => ({ + title: `#${index + 1} ${novel.title}`, + link: `https://ncode.syosetu.com/${String(novel.ncode).toLowerCase()}`, + description: art(path.join(__dirname, 'templates', 'description.art'), { + novel, + }), + author: novel.writer, + category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean), + })); + + return { + title: `小説家になろう - ${rankingTitle}`, + link: rankingUrl, + item: items as DataItem[], + language: 'ja', + }; +} diff --git a/lib/routes/syosetu/search.ts b/lib/routes/syosetu/search.ts new file mode 100644 index 00000000000000..676c601057d171 --- /dev/null +++ b/lib/routes/syosetu/search.ts @@ -0,0 +1,161 @@ +import { Route, Data } from '@/types'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { Context } from 'hono'; +import { Genre, GenreNotation, NarouNovelFetch, NovelTypeParam, Order, R18Site, SearchBuilder, SearchBuilderR18, SearchParams } from 'narou'; +import queryString from 'query-string'; +import { Join } from 'narou/util/type'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import { SyosetuSub, NarouSearchParams, syosetuSubToJapanese } from './types/search'; + +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +export const route: Route = { + path: '/search/:sub/:query', + categories: ['reading'], + example: '/syosetu/search/noc/word=ハーレム¬word=&type=r&mintime=&maxtime=&minlen=30000&maxlen=&min_globalpoint=&max_globalpoint=&minlastup=&maxlastup=&minfirstup=&maxfirstup=&isgl=1¬bl=1&order=new?limit=5', + parameters: { + sub: { + description: 'The target Syosetu subsite.', + options: Object.entries(SyosetuSub).map(([, value]) => ({ + value, + label: syosetuSubToJapanese[value], + })), + }, + query: 'Search parameters in Syosetu format.', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Search', + maintainers: ['SnowAgar25'], + handler, +}; + +const setIfExists = (value) => value ?? undefined; + +/** + * This function converts query string generated by Syosetu website into API-compatible format. + * It is not intended for users to freely adjust values. + * + * @see https://deflis.github.io/node-narou/index.html + * @see https://dev.syosetu.com/man/api/ + */ +function mapToSearchParams(query: string, limit: number): SearchParams { + const params = queryString.parse(query) as NarouSearchParams; + + const searchParams: SearchParams = { + gzip: 5, + lim: limit, + }; + + searchParams.word = setIfExists(params.word); + searchParams.notword = setIfExists(params.notword); + + searchParams.title = setIfExists(params.title); + searchParams.ex = setIfExists(params.ex); + searchParams.keyword = setIfExists(params.keyword); + searchParams.wname = setIfExists(params.wname); + + searchParams.sasie = setIfExists(params.sasie); + searchParams.iszankoku = setIfExists(params.iszankoku); + searchParams.isbl = setIfExists(params.isbl); + searchParams.isgl = setIfExists(params.isgl); + searchParams.istensei = setIfExists(params.istensei); + searchParams.istenni = setIfExists(params.istenni); + + searchParams.stop = setIfExists(params.stop); + searchParams.notzankoku = setIfExists(params.notzankoku); + searchParams.notbl = setIfExists(params.notbl); + searchParams.notgl = setIfExists(params.notgl); + searchParams.nottensei = setIfExists(params.nottensei); + searchParams.nottenni = setIfExists(params.nottenni); + + searchParams.minlen = setIfExists(params.minlen); + searchParams.maxlen = setIfExists(params.maxlen); + + searchParams.type = setIfExists(params.type as NovelTypeParam); + searchParams.order = setIfExists(params.order as Order); + searchParams.genre = setIfExists(params.genre as Join<Genre> | Genre); + searchParams.nocgenre = setIfExists(params.nocgenre as Join<R18Site> | R18Site); + + if (params.mintime || params.maxtime) { + searchParams.time = `${params.mintime || ''}-${params.maxtime || ''}`; + } + + return searchParams; +} + +const isGeneral = (sub: string): boolean => sub === SyosetuSub.YOMOU; + +function createNovelSearchBuilder(sub: string, searchParams: SearchParams) { + if (isGeneral(sub)) { + return new SearchBuilder(searchParams, new NarouNovelFetch()); + } + + const r18Params = { ...searchParams }; + + switch (sub) { + case SyosetuSub.NOCTURNE: + r18Params.nocgenre = R18Site.Nocturne; + break; + case SyosetuSub.MOONLIGHT: + // If either 女性向け/BL is chosen, nocgenre will be in query string + // If no specific genre selected, include both + if (!r18Params.nocgenre) { + r18Params.nocgenre = [R18Site.MoonLight, R18Site.MoonLightBL].join('-') as Join<R18Site>; + } + break; + case SyosetuSub.MIDNIGHT: + r18Params.nocgenre = R18Site.Midnight; + break; + default: + throw new InvalidParameterError('Invalid Syosetu subsite.\nValid subsites are: yomou, noc, mnlt, mid'); + } + + return new SearchBuilderR18(r18Params, new NarouNovelFetch()); +} + +async function handler(ctx: Context): Promise<Data> { + const { sub, query } = ctx.req.param(); + const searchUrl = `https://${sub}.syosetu.com/search/search/search.php?${query}`; + + const limit = Math.min(Number(ctx.req.query('limit') ?? 40), 40); + const searchParams = mapToSearchParams(query, limit); + const builder = createNovelSearchBuilder(sub, searchParams); + const result = await builder.execute(); + + const items = result.values.map((novel) => ({ + title: novel.title, + link: `https://${isGeneral(sub) ? 'ncode' : 'novel18'}.syosetu.com/${String(novel.ncode).toLowerCase()}`, + description: art(path.join(__dirname, 'templates', 'description.art'), { + novel, + genreText: GenreNotation[novel.genre], + }), + // Skip pubDate - search results prioritize search sequence over timestamps + // pubDate: novel.general_lastup, + author: novel.writer, + // Split by whitespace characters(\s), slash(/), full-width slash(/) + category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean), + })); + + const searchTerms: string[] = []; + if (searchParams.word) { + searchTerms.push(searchParams.word); + } + if (searchParams.notword) { + searchTerms.push(`-${searchParams.notword}`); + } + + return { + title: searchTerms.length > 0 ? `Syosetu Search: ${searchTerms.join(' ')}` : 'Syosetu Search', + link: searchUrl, + item: items, + }; +} diff --git a/lib/routes/syosetu/templates/description.art b/lib/routes/syosetu/templates/description.art new file mode 100644 index 00000000000000..7da2927b78ce7f --- /dev/null +++ b/lib/routes/syosetu/templates/description.art @@ -0,0 +1,73 @@ +{{ if novel.story }} +<p>{{@ novel.story.replaceAll('\n', '<br>') }}</p> +{{ /if }} + +<h2>作品情報</h2> +<table> + <tr> + <td>状態</td> + <td> + {{ if novel.novel_type === 2 }} + 短編 + {{ else }} + {{ if novel.end === 0 }} + 完結済 + {{ else }} + 連載中 + {{ /if }} + (全{{ novel.general_all_no }}エピソード) + {{ /if }} + </td> + </tr> + {{ if genreText }} + <tr> + <td>ジャンル</td> + <td>{{ genreText }}</td> + </tr> + {{ /if }} + <tr> + <td>キーワード</td> + <td>{{ novel.keyword.replaceAll(' ', '、') }}</td> + </tr> + <tr> + <td>最終掲載</td> + <td>{{ novel.general_lastup }}</td> + </tr> + <tr> + <td>Nコード</td> + <td>{{ novel.ncode }}</td> + </tr> + <tr> + <td>読了時間</td> + <td>約{{ novel.time }}分({{ novel.length }}文字)</td> + </tr> +</table> +<br> +<table> + {{ if novel.weekly_unique }} + <tr> + <td>週別ユニークユーザ</td> + <td>{{ novel.weekly_unique < 100 ? '100未満' : novel.weekly_unique }}</td> + </tr> + {{ /if }} + <tr> + <td>総合ポイント</td> + <td>{{ novel.global_point }} pt</td> + </tr> + <tr> + <td>評価人数</td> + <td>{{ novel.all_hyoka_cnt }} 人</td> + </tr> + <tr> + <td>評価ポイント</td> + <td>{{ novel.all_point }} pt</td> + </tr> + <tr> + <td>ブックマーク</td> + <td>{{ novel.fav_novel_cnt }} 件</td> + </tr> +</table> + +{{ if novel.sasie_cnt > 0 }} +<p>挿絵数:{{ novel.sasie_cnt }}枚</p> +{{ /if }} diff --git a/lib/routes/syosetu/types/ranking-r18.ts b/lib/routes/syosetu/types/ranking-r18.ts new file mode 100644 index 00000000000000..d22cf65507d28a --- /dev/null +++ b/lib/routes/syosetu/types/ranking-r18.ts @@ -0,0 +1,60 @@ +import { R18Site } from 'narou'; + +export enum SyosetuSub { + NOCTURNE = 'noc', + MOONLIGHT = 'mnlt', + MIDNIGHT = 'mid', + MOONLIGHT_BL = 'mnlt-bl', +} + +export enum RankingPeriod { + DAILY = 'daily', + WEEKLY = 'weekly', + MONTHLY = 'monthly', + QUARTER = 'quarter', + YEARLY = 'yearly', +} + +export enum NovelType { + TOTAL = 'total', + SHORT = 't', + ONGOING = 'r', + COMPLETE = 'er', +} + +export const syosetuSubToNocgenre = { + [SyosetuSub.NOCTURNE]: R18Site.Nocturne, + [SyosetuSub.MOONLIGHT]: R18Site.MoonLight, + [SyosetuSub.MOONLIGHT_BL]: R18Site.MoonLightBL, + [SyosetuSub.MIDNIGHT]: R18Site.Midnight, +} as const; + +export const syosetuSubToJapanese = { + [SyosetuSub.NOCTURNE]: 'ノクターン', + [SyosetuSub.MOONLIGHT]: 'ムーンライト', + [SyosetuSub.MOONLIGHT_BL]: 'ムーンライト BL', + [SyosetuSub.MIDNIGHT]: 'ミッドナイト', +} as const; + +export const periodToOrder = { + [RankingPeriod.DAILY]: 'dailypoint', + [RankingPeriod.WEEKLY]: 'weeklypoint', + [RankingPeriod.MONTHLY]: 'monthlypoint', + [RankingPeriod.QUARTER]: 'quarterpoint', + [RankingPeriod.YEARLY]: 'yearlypoint', +} as const; + +export const periodToJapanese = { + [RankingPeriod.DAILY]: '日間', + [RankingPeriod.WEEKLY]: '週間', + [RankingPeriod.MONTHLY]: '月間', + [RankingPeriod.QUARTER]: '四半期', + [RankingPeriod.YEARLY]: '年間', +} as const; + +export const novelTypeToJapanese = { + [NovelType.TOTAL]: '総合', + [NovelType.SHORT]: '短編', + [NovelType.ONGOING]: '連載中', + [NovelType.COMPLETE]: '完結済', +} as const; diff --git a/lib/routes/syosetu/types/ranking.ts b/lib/routes/syosetu/types/ranking.ts new file mode 100644 index 00000000000000..558bfd151e0c0e --- /dev/null +++ b/lib/routes/syosetu/types/ranking.ts @@ -0,0 +1,67 @@ +export enum RankingPeriod { + DAILY = 'daily', + WEEKLY = 'weekly', + MONTHLY = 'monthly', + QUARTER = 'quarter', + YEARLY = 'yearly', + TOTAL = 'total', +} + +export enum NovelType { + TOTAL = 'total', + SHORT = 't', + ONGOING = 'r', + COMPLETE = 'er', +} + +export enum RankingType { + LIST = 'list', + GENRE = 'genre', + ISEKAI = 'isekai', +} + +export const periodToOrder = { + [RankingPeriod.DAILY]: 'dailypoint', + [RankingPeriod.WEEKLY]: 'weeklypoint', + [RankingPeriod.MONTHLY]: 'monthlypoint', + [RankingPeriod.QUARTER]: 'quarterpoint', + [RankingPeriod.YEARLY]: 'yearlypoint', + [RankingPeriod.TOTAL]: 'hyoka', +} as const; + +export const periodToPointField = { + [RankingPeriod.DAILY]: 'pt', + [RankingPeriod.WEEKLY]: 'weekly_point', + [RankingPeriod.MONTHLY]: 'monthly_point', + [RankingPeriod.QUARTER]: 'quarter_point', + [RankingPeriod.YEARLY]: 'yearly_point', + [RankingPeriod.TOTAL]: 'global_point', +} as const; + +export const periodToJapanese = { + [RankingPeriod.DAILY]: '日間', + [RankingPeriod.WEEKLY]: '週間', + [RankingPeriod.MONTHLY]: '月間', + [RankingPeriod.QUARTER]: '四半期', + [RankingPeriod.YEARLY]: '年間', + [RankingPeriod.TOTAL]: '累計', +} as const; + +export const novelTypeToJapanese = { + [NovelType.TOTAL]: 'すべて', + [NovelType.SHORT]: '短編', + [NovelType.ONGOING]: '連載中', + [NovelType.COMPLETE]: '完結済', +} as const; + +export enum IsekaiCategory { + RENAI = '1', + FANTASY = '2', + OTHER = 'o', +} + +export const isekaiCategoryToJapanese = { + [IsekaiCategory.RENAI]: '〔恋愛〕', + [IsekaiCategory.FANTASY]: '〔ファンタジー〕', + [IsekaiCategory.OTHER]: '〔文芸・SF・その他〕', +} as const; diff --git a/lib/routes/syosetu/types/search.ts b/lib/routes/syosetu/types/search.ts new file mode 100644 index 00000000000000..130d1a8d93f3de --- /dev/null +++ b/lib/routes/syosetu/types/search.ts @@ -0,0 +1,138 @@ +export enum SyosetuSub { + YOMOU = 'yomou', + NOCTURNE = 'noc', + MOONLIGHT = 'mnlt', + MIDNIGHT = 'mid', +} + +export const syosetuSubToJapanese = { + [SyosetuSub.YOMOU]: '小説を読もう', + [SyosetuSub.NOCTURNE]: 'ノクターン', + [SyosetuSub.MOONLIGHT]: 'ムーンライト', + [SyosetuSub.MIDNIGHT]: 'ミッドナイト', +} as const; + +export interface NarouSearchParams { + /** + * 作品種別の絞り込み Work Type Filter + * + * t 短編 Short + * r 連載中 Ongoing Series + * er 完結済連載作品 Completed Series + * re すべての連載作品 (連載中および完結済) Series and Completed Series + * ter 短編と完結済連載作品 Completed Works (Including Short and Series) + * + * tr 短編と連載中小説 Short and Ongoing Series + * all 全ての種別 (Default) All Types + * + * Note: While the official documentation describes 5 values, all 7 values above are functional. + */ + type?: 't' | 'r' | 'er' | 're' | 'ter' | 'tr' | 'all'; + + /** 検索ワード Search Keywords */ + word?: string; + + /** 除外ワード Excluded Keywords */ + notword?: string; + + /** + * 検索範囲指定 Search Range Specifications + * + * - 読了時間 Reading Time + * - 文字数 Character Count + * - 総合ポイント Total Points + * - 最新掲載日(年月日)Latest Update Date (Year/Month/Day) + * - 初回掲載日(年月日)First Publication Date (Year/Month/Day) + */ + mintime?: number; + maxtime?: number; + minlen?: number; + maxlen?: number; + min_globalpoint?: number; + max_globalpoint?: number; + minlastup?: string; + maxlastup?: string; + minfirstup?: string; + maxfirstup?: string; + + /** + * 抽出条件の指定 Extraction Conditions + * + * - 挿絵のある作品 Works with Illustrations + * - 小説 PickUp!対象作品 Featured Novels + * + * 作品に含まれる要素:Elements Included in Works: + * - 残酷な描写あり Contains Cruel Content + * - ボーイズラブ Boys' Love + * - ガールズラブ Girls' Love + * - 異世界転生 Reincarnation in Another World + * - 異世界転移 Transportation to Another World + */ + sasie?: string; + ispickup?: boolean; + iszankoku?: boolean; + isbl?: boolean; + isgl?: boolean; + istensei?: boolean; + istenni?: boolean; + + /** + * 除外条件の指定 Exclusion Conditions + * + * - 長期連載停止中の作品 Works on Long-term Hiatus + * + * 作品に含まれる要素:Elements to Exclude: + * - 残酷な描写あり Cruel Content + * - ボーイズラブ Boys' Love + * - ガールズラブ Girls' Love + * - 異世界転生 Reincarnation in Another World + * - 異世界転移 Transportation to Another World + */ + stop?: boolean; + notzankoku?: boolean; + notbl?: boolean; + notgl?: boolean; + nottensei?: boolean; + nottenni?: boolean; + + /** + * ワード検索範囲指定 Word Search Scope + * すべてのチェックを解除した場合、すべての項目がワード検索の対象となります。 + * If all boxes are unchecked, all items will become targets for word search. + * + * 作品タイトル Work Title + * あらすじ Synopsis + * キーワード Keywords + * 作者名 Author Name + */ + title?: boolean; + ex?: boolean; + keyword?: boolean; + wname?: boolean; + + /** + * 並び順 Sort Order + * - new: 新着更新順 (Default) Latest Updates + * - weekly: 週間ユニークアクセスが多い順 Most Weekly Unique Access + * - favnovelcnt: ブックマーク登録の多い順 Most Bookmarks + * - reviewcnt: レビューの多い順 Most Reviews + * - hyoka: 総合ポイントの高い順 Highest Total Points + * - dailypoint: 日間ポイントの高い順 Highest Daily Points + * - weeklypoint: 週間ポイントの高い順 Highest Weekly Points + * - monthlypoint: 月間ポイントの高い順 Highest Monthly Points + * - quarterpoint: 四半期ポイントの高い順 Highest Quarterly Points + * - yearlypoint: 年間ポイントの高い順 Highest Yearly Points + * - hyokacnt: 評価者数の多い順 Most Ratings + * - lengthdesc: 文字数の多い順 Most Characters + * - generalfirstup: 初回掲載順 First Publication Order + * - ncodedesc: N コード降順 Ncode Descending + * - old: 更新が古い順 Oldest Updates + */ + order?: 'new' | 'weekly' | 'favnovelcnt' | 'reviewcnt' | 'hyoka' | 'dailypoint' | 'weeklypoint' | 'monthlypoint' | 'quarterpoint' | 'yearlypoint' | 'hyokacnt' | 'lengthdesc' | 'generalfirstup' | 'ncodedesc' | 'old'; + + /** ジャンル Genre */ + genre?: string; + + /** 掲載サイト指定 Site */ + nocgenre?: number; +} diff --git a/lib/routes/syosetu/utils.ts b/lib/routes/syosetu/utils.ts new file mode 100644 index 00000000000000..9970c6dc3d91da --- /dev/null +++ b/lib/routes/syosetu/utils.ts @@ -0,0 +1,50 @@ +import { DataItem } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import { config } from '@/config'; +import { NarouNovelFetch, NarouSearchResult, SearchBuilder, SearchBuilderR18 } from 'narou'; + +export async function fetchNovelInfo(ncode: string): Promise<{ baseUrl: string; novel: NarouSearchResult }> { + const api = new NarouNovelFetch(); + const [generalRes, r18Res] = await Promise.all([new SearchBuilder({ gzip: 5, of: 't-s-k-ga-nt-nu' }, api).ncode(ncode).execute(), new SearchBuilderR18({ gzip: 5, of: 't-s-k-ga-nt-nu' }, api).ncode(ncode).execute()]); + + const isGeneral = generalRes.allcount !== 0; + const novelData = isGeneral ? generalRes : r18Res; + const baseUrl = isGeneral ? 'https://ncode.syosetu.com' : 'https://novel18.syosetu.com'; + + if (novelData.allcount === 0) { + throw new InvalidParameterError('Novel not found in both APIs'); + } + + return { + baseUrl, + novel: novelData.values[0] as NarouSearchResult, + }; +} + +export async function fetchChapterContent(chapterUrl: string, chapter?: number): Promise<DataItem> { + return (await cache.tryGet(chapterUrl, async () => { + const response = await ofetch(chapterUrl, { + headers: { + Cookie: 'over18=yes', + 'User-Agent': config.ua, + }, + }); + + const $ = load(response); + + const title = `${chapter ? `#${chapter} ` : ''}${$('.p-novel__title').html() || ''}`; + const description = $('.p-novel__body').html() || ''; + const pubDate = $('meta[name=WWWC]').attr('content'); + + return { + title, + description, + link: chapterUrl, + pubDate, + language: 'ja', + }; + })) as DataItem; +} diff --git a/lib/routes/sysu/namespace.ts b/lib/routes/sysu/namespace.ts index 20a7ed3c4fe891..3535a82fad369a 100644 --- a/lib/routes/sysu/namespace.ts +++ b/lib/routes/sysu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中山大学', url: 'cse.sysu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/sysu/ygafz.ts b/lib/routes/sysu/ygafz.ts index 56f5c7c3bf1fee..c056329bc5a041 100644 --- a/lib/routes/sysu/ygafz.ts +++ b/lib/routes/sysu/ygafz.ts @@ -27,12 +27,12 @@ export const route: Route = { ], name: '粤港澳发展研究院', description: `| 人才招聘 | 人才培养 | 新闻动态 | 通知公告 | 专家观点 | - | ---------- | ------------- | -------- | -------- | -------- | - | jobopening | personnelplan | news | notice | opinion | +| ---------- | ------------- | -------- | -------- | -------- | +| jobopening | personnelplan | news | notice | opinion | - | 研究成果 | 研究论文 | 学术著作 | 形势政策 | - | -------- | -------- | -------- | -------- | - | results | papers | writings | policy |`, +| 研究成果 | 研究论文 | 学术著作 | 形势政策 | +| -------- | -------- | -------- | -------- | +| results | papers | writings | policy |`, maintainers: ['TonyRL'], handler, }; @@ -63,7 +63,7 @@ async function handler(ctx) { return jar; }, cookieJar); - browser.close(); + await browser.close(); const $ = load(response); const list = $('.list-content a') diff --git a/lib/routes/szftedu/dongtai.ts b/lib/routes/szftedu/dongtai.ts new file mode 100644 index 00000000000000..c2acc657f0efc2 --- /dev/null +++ b/lib/routes/szftedu/dongtai.ts @@ -0,0 +1,62 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const host = 'https://ylxx.szftedu.cn/xx_5828/xydt_5829/bxfbx_6371/'; +const baseLink = 'https://ylxx.szftedu.cn'; + +export const route: Route = { + path: '/dongtai', + categories: ['university'], + example: '/szftedu/dongtai', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '动态', + maintainers: ['valuex'], + handler, + description: '', +}; + +async function handler() { + const link = host; + const response = await got(link); + const $ = load(response.data); + const lists = $('div.pagenews04 div ul li') + .toArray() + .map((el) => ({ + title: $('a', el).text().trim(), + link: $('a', el).attr('href'), + pubDate: timezone(parseDate($('span[class=canedit]', el).text()), 8), + })); + + const items = await Promise.all( + lists.map((item) => + cache.tryGet(item.link, async () => { + const thisUrl = item.link; + const trueLink = thisUrl.includes('http') ? thisUrl : baseLink + thisUrl; + const response = await got(trueLink); + const $ = load(response.data); + item.description = thisUrl.includes('http') ? $('#page-content').html() : $('div.TRS_Editor').html(); + item.pubDate = timezone(parseDate($('#publish_time').first().text()), 8); + return item; + }) + ) + ); + + return { + title: '园岭小学动态', + link: host, + description: '园岭小学动态', + item: items, + }; +} diff --git a/lib/routes/szftedu/gonggao.ts b/lib/routes/szftedu/gonggao.ts new file mode 100644 index 00000000000000..0cda89550ff0b0 --- /dev/null +++ b/lib/routes/szftedu/gonggao.ts @@ -0,0 +1,62 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const host = 'https://ylxx.szftedu.cn/xx_5828/xygg_5832/'; + +export const route: Route = { + path: '/gonggao', + categories: ['university'], + example: '/szftedu/gonggao', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '公告', + maintainers: ['valuex'], + handler, + description: '', +}; + +async function handler() { + const link = host; + const response = await got(link); + const $ = load(response.data); + + const lists = $('div.pagenews04 div ul li') + .toArray() + .map((el) => ({ + title: $('a', el).text().trim(), + link: $('a', el).attr('href'), + pubDate: timezone(parseDate($('span[class=canedit]', el).text()), 8), + })); + + const items = await Promise.all( + lists.map((item) => + cache.tryGet(item.link, async () => { + const thisUrl = item.link; + const trueLink = thisUrl.includes('http') ? thisUrl : host + thisUrl.substring(1); + const response = await got(trueLink); + const $ = load(response.data); + item.description = thisUrl.includes('http') ? $('#page-content').html() : $('div.TRS_Editor').html(); + item.pubDate = timezone(parseDate($('.item').first().text().replace('发布时间:', '')), 8); + return item; + }) + ) + ); + + return { + title: '园岭小学公告', + link: host, + description: '园岭小学公告', + item: items, + }; +} diff --git a/lib/routes/szftedu/namespace.ts b/lib/routes/szftedu/namespace.ts new file mode 100644 index 00000000000000..09ea986617eae5 --- /dev/null +++ b/lib/routes/szftedu/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '园岭小学', + url: 'ylxx.szftedu.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/szse/disclosure/listed-notice.ts b/lib/routes/szse/disclosure/listed-notice.ts new file mode 100644 index 00000000000000..9e6aeb28e7cbab --- /dev/null +++ b/lib/routes/szse/disclosure/listed-notice.ts @@ -0,0 +1,111 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +import { type CheerioAPI, load } from 'cheerio'; +import { type Context } from 'hono'; + +export const handler = async (ctx: Context): Promise<Data> => { + const { category = '' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '50', 10); + + const baseUrl: string = 'https://www.szse.cn'; + const staticBaseUrl: string = 'https://disc.static.szse.cn'; + const apiUrl: string = new URL('api/disc/announcement/annList', baseUrl).href; + const targetUrl: string = new URL(`disclosure/listed/notice${category}`, baseUrl).href; + + const targetResponse = await ofetch(targetUrl); + const $: CheerioAPI = load(targetResponse); + const language = $('html').attr('lang') ?? 'zh-CN'; + + const response = await ofetch(apiUrl, { + method: 'POST', + body: { + seDate: ['', ''], + channelCode: ['listedNotice_disc'], + pageSize: limit, + pageNum: 1, + }, + }); + + const items: DataItem[] = response.data + .slice(0, limit) + .map((item): DataItem => { + const title: string = item.title; + const pubDate: number | string = item.publishTime; + const linkUrl: string | undefined = `disclosure/listed/bulletinDetail/index.html?${item.id}`; + const categories: string[] = [...new Set([...item.secCode, ...item.secName, item.bigCategoryId, item.bigIndustryCode, item.smallCategoryId].filter(Boolean))]; + const guid: string = `szse-${item.id}`; + const updated: number | string = item.publishTime; + + let processedItem: DataItem = { + title, + pubDate: pubDate ? timezone(parseDate(pubDate), +8) : undefined, + link: new URL(linkUrl, baseUrl).href, + category: categories, + guid, + id: guid, + updated: updated ? timezone(parseDate(updated), +8) : undefined, + language, + }; + + const enclosureUrl: string | undefined = new URL(`download${item.attachPath}`, staticBaseUrl).href; + + if (enclosureUrl) { + const enclosureType: string = `application/${item.attachFormat}`; + + processedItem = { + ...processedItem, + enclosure_url: enclosureUrl, + enclosure_type: enclosureType, + enclosure_title: title, + enclosure_length: undefined, + }; + } + + return processedItem; + }) + .filter((_): _ is DataItem => true); + + return { + title: '深圳证券交易所 - 上市公司公告', + description: $('meta[name="description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('a.navbar-brand img').attr('src'), + author: $('meta[name="author"]').attr('content'), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/disclosure/listed/notice', + name: '上市公司公告', + url: 'www.szse.cn', + maintainers: ['nczitzk'], + handler, + example: '/szse/disclosure/listed/notice', + parameters: undefined, + description: undefined, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.szse.cn/disclosure/listed/notice/index.html'], + target: '/disclosure/listed/notice', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/szse/inquire.ts b/lib/routes/szse/inquire.ts index 7d105d467a61a5..f45fe99d126b62 100644 --- a/lib/routes/szse/inquire.ts +++ b/lib/routes/szse/inquire.ts @@ -32,14 +32,14 @@ export const route: Route = { url: 'szse.cn/disclosure/supervision/inquire/index.html', description: `类型 - | 主板 | 创业板 | - | ---- | ------ | - | 0 | 1 | +| 主板 | 创业板 | +| ---- | ------ | +| 0 | 1 | 函件类别 - | 全部函件类别 | 非许可类重组问询函 | 问询函 | 违法违规线索分析报告 | 许可类重组问询函 | 监管函(会计师事务所模板) | 提请关注函(会计师事务所模板) | 年报问询函 | 向中介机构发函 | 半年报问询函 | 关注函 | 公司部函 | 三季报问询函 | - | ------------ | ------------------ | ------ | -------------------- | ---------------- | -------------------------- | ------------------------------ | ---------- | -------------- | ------------ | ------ | -------- | ------------ |`, +| 全部函件类别 | 非许可类重组问询函 | 问询函 | 违法违规线索分析报告 | 许可类重组问询函 | 监管函(会计师事务所模板) | 提请关注函(会计师事务所模板) | 年报问询函 | 向中介机构发函 | 半年报问询函 | 关注函 | 公司部函 | 三季报问询函 | +| ------------ | ------------------ | ------ | -------------------- | ---------------- | -------------------------- | ------------------------------ | ---------- | -------------- | ------------ | ------ | -------- | ------------ |`, }; async function handler(ctx) { diff --git a/lib/routes/szse/namespace.ts b/lib/routes/szse/namespace.ts index 1a7dc951654c64..b857b5dd4e63d7 100644 --- a/lib/routes/szse/namespace.ts +++ b/lib/routes/szse/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '深圳证券交易所', url: 'szse.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/szse/projectdynamic.ts b/lib/routes/szse/projectdynamic.ts index 116db6a196d7dc..3ad17deab2b985 100644 --- a/lib/routes/szse/projectdynamic.ts +++ b/lib/routes/szse/projectdynamic.ts @@ -33,37 +33,37 @@ export const route: Route = { url: 'listing.szse.cn/projectdynamic/1/index.html', description: `类型 - | IPO | 再融资 | 重大资产重组 | - | --- | ------ | ------------ | - | 1 | 2 | 3 | +| IPO | 再融资 | 重大资产重组 | +| --- | ------ | ------------ | +| 1 | 2 | 3 | 阶段 - | 全部 | 受理 | 问询 | 上市委会议 | - | ---- | ---- | ---- | ---------- | - | 0 | 10 | 20 | 30 | +| 全部 | 受理 | 问询 | 上市委会议 | +| ---- | ---- | ---- | ---------- | +| 0 | 10 | 20 | 30 | - | 提交注册 | 注册结果 | 中止 | 终止 | - | -------- | -------- | ---- | ---- | - | 35 | 40 | 50 | 60 | +| 提交注册 | 注册结果 | 中止 | 终止 | +| -------- | -------- | ---- | ---- | +| 35 | 40 | 50 | 60 | 状态 - | 全部 | 新受理 | 已问询 | 通过 | 未通过 | - | ---- | ------ | ------ | ---- | ------ | - | 0 | 20 | 30 | 45 | 44 | +| 全部 | 新受理 | 已问询 | 通过 | 未通过 | +| ---- | ------ | ------ | ---- | ------ | +| 0 | 20 | 30 | 45 | 44 | - | 暂缓审议 | 复审通过 | 复审不通过 | 提交注册 | - | -------- | -------- | ---------- | -------- | - | 46 | 56 | 54 | 60 | +| 暂缓审议 | 复审通过 | 复审不通过 | 提交注册 | +| -------- | -------- | ---------- | -------- | +| 46 | 56 | 54 | 60 | - | 注册生效 | 不予注册 | 补充审核 | 终止注册 | - | -------- | -------- | -------- | -------- | - | 70 | 74 | 78 | 76 | +| 注册生效 | 不予注册 | 补充审核 | 终止注册 | +| -------- | -------- | -------- | -------- | +| 70 | 74 | 78 | 76 | - | 中止 | 审核不通过 | 撤回 | - | ---- | ---------- | ---- | - | 80 | 90 | 95 |`, +| 中止 | 审核不通过 | 撤回 | +| ---- | ---------- | ---- | +| 80 | 90 | 95 |`, }; async function handler(ctx) { diff --git a/lib/routes/szse/rule.ts b/lib/routes/szse/rule.ts index 0c9b2d116e44c4..cbb7f9674e3263 100644 --- a/lib/routes/szse/rule.ts +++ b/lib/routes/szse/rule.ts @@ -1,77 +1,350 @@ 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: '/rule', - categories: ['finance'], - example: '/szse/rule', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['szse.cn/lawrules/rule/new', 'szse.cn/'], - }, - ], - name: '最新规则', - maintainers: ['nczitzk'], - handler, - url: 'szse.cn/lawrules/rule/new', -}; +export const handler = async (ctx) => { + const { channel = 'allrules/bussiness' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + + const rootUrl = 'https://www.szse.cn'; + const apiUrl = new URL('api/search/content', rootUrl).href; + const currentUrl = new URL(`www/lawrules/rule/${channel}/`, rootUrl).href; + + const { data: currentResponse } = await got(currentUrl); -async function handler() { - const rootUrl = 'http://www.szse.cn'; - const currentUrl = `${rootUrl}/api/search/content`; + const $ = load(currentResponse); - const response = await got({ - method: 'post', - url: currentUrl, + const channelEl = $('ul.side-menu-con li.active').last(); + const channelCode = channelEl.prop('chnlcode'); + + const { data: response } = await got.post(apiUrl, { form: { keyword: '', time: 0, range: 'title', - 'channelCode[]': 'szserulesAllRulesBuss', + 'channelCode[]': channelCode, currentPage: 1, - pageSize: 30, + pageSize: limit, scope: 0, }, }); - let items = response.data.data.map((item) => ({ + let items = response.data.slice(0, limit).map((item) => ({ title: item.doctitle, + pubDate: parseDate(item.docpubtime, 'X'), link: item.docpuburl, - pubDate: parseDate(item.docpubtime), + category: item.navigation, })); 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); + + const $$ = load(detailResponse); - const content = load(detailResponse.data); + const title = $$('h2.title').text(); + const description = $$('div#desContent').html(); - item.description = content('#desContent').html(); + item.title = title; + item.description = description; + item.pubDate = item.pubDate ?? parseDate($$('div.time span').text()); + item.author = $$('meta[name="author"]').prop('content'); + item.content = { + html: description, + text: $$('div#desContent').text(), + }; + item.language = $$('html').prop('lang'); return item; }) ) ); + const image = $('a.navbar-brand img').prop('src'); + return { - title: '最新规则 - 深圳证券交易所', - link: `${rootUrl}/lawrules/rule/new`, + title: `深圳证券交易所 - ${channelEl.text()}`, + description: $('meta[name="description"]').prop('content'), + link: currentUrl, item: items, + allowEmpty: true, + image, + author: $('meta[name="author"]').prop('content'), + language: $('html').prop('lang'), }; -} +}; + +export const route: Route = { + path: '/rule/:channel{.+}?', + name: '本所业务规则', + url: 'www.szse.cn', + maintainers: ['nczitzk'], + handler, + example: '/szse/rule/allrules/bussiness', + parameters: { channel: '频道,默认为 `allrules/bussiness`,即全部业务规则,可在对应频道页 URL 中找到' }, + description: `::: tip + 若订阅 [综合类](https://www.szse.cn/www/lawrules/rule/all/index.html),网址为 \`https://www.szse.cn/www/lawrules/rule/all/index.html\`。截取 \`https://www.szse.cn/www/lawrules/rule/\` 到末尾 \`/index.html\` 的部分 \`all\` 作为参数填入,此时路由为 [\`/szse/rule/all\`](https://rsshub.app/szse/rule/all)。 +::: + +| 频道 | ID | +| --------------------------------------------------------------------------- | ----------------------------------------------------- | +| [综合类](https://www.szse.cn/www/lawrules/rule/all/index.html) | [all](https://rsshub.app/szes/rule/all) | +| [基础设施REITs类](https://www.szse.cn/www/lawrules/rule/reits/index.html) | [reits](https://rsshub.app/szes/rule/reits) | +| [衍生品类](https://www.szse.cn/www/lawrules/rule/derivative/index.html) | [derivative](https://rsshub.app/szes/rule/derivative) | +| [会员管理类](https://www.szse.cn/www/lawrules/rule/memberty/index.html) | [memberty](https://rsshub.app/szes/rule/memberty) | +| [纪律处分与内部救济类](https://www.szse.cn/www/lawrules/rule/pr/index.html) | [pr](https://rsshub.app/szes/rule/pr) | + +#### 股票类 + +| 频道 | ID | +| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| [发行上市审核](https://www.szse.cn/www/lawrules/rule/stock/audit/index.html) | [stock/audit](https://rsshub.app/szes/rule/stock/audit) | +| [发行承销](https://www.szse.cn/www/lawrules/rule/stock/issue/index.html) | [stock/issue](https://rsshub.app/szes/rule/stock/issue) | +| [通用](https://www.szse.cn/www/lawrules/rule/stock/supervision/currency/index.html) | [stock/supervision/currency](https://rsshub.app/szes/rule/stock/supervision/currency) | +| [主板专用](https://www.szse.cn/www/lawrules/rule/stock/supervision/mb/index.html) | [stock/supervision/mb](https://rsshub.app/szes/rule/stock/supervision/mb) | +| [创业板专用](https://www.szse.cn/www/lawrules/rule/stock/supervision/chinext/index.html) | [stock/supervision/chinext](https://rsshub.app/szes/rule/stock/supervision/chinext) | +| [交易](https://www.szse.cn/www/lawrules/rule/stock/trade/index.html) | [stock/trade](https://rsshub.app/szes/rule/stock/trade) | + +#### 固收类 + +| 频道 | ID | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | +| [发行上市(挂牌)](https://www.szse.cn/www/lawrules/rule/bond/bonds/list/index.html) | [bond/bonds/list](https://rsshub.app/szes/rule/bond/bonds/list) | +| [持续监管](https://www.szse.cn/www/lawrules/rule/bond/bonds/supervision/index.html) | [bond/bonds/supervision](https://rsshub.app/szes/rule/bond/bonds/supervision) | +| [交易](https://www.szse.cn/www/lawrules/rule/bond/bonds/trade/index.html) | [bond/bonds/trade](https://rsshub.app/szes/rule/bond/bonds/trade) | +| [资产支持证券](https://www.szse.cn/www/lawrules/rule/bond/abs/index.html) | [bond/abs](https://rsshub.app/szes/rule/bond/abs) | + +#### 基金类 + +| 频道 | ID | +| ------------------------------------------------------------------- | ----------------------------------------------------- | +| [上市](https://www.szse.cn/www/lawrules/rule/fund/list/index.html) | [fund/list](https://rsshub.app/szes/rule/fund/list) | +| [交易](https://www.szse.cn/www/lawrules/rule/fund/trade/index.html) | [fund/trade](https://rsshub.app/szes/rule/fund/trade) | + +#### 交易类 + +| 频道 | ID | +| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| [通用](https://www.szse.cn/www/lawrules/rule/trade/current/index.html) | [trade/current](https://rsshub.app/szes/rule/trade/current) | +| [融资融券](https://www.szse.cn/www/lawrules/rule/trade/business/margin/index.html) | [trade/business/margin](https://rsshub.app/szes/rule/trade/business/margin) | +| [转融通](https://www.szse.cn/www/lawrules/rule/trade/business/refinancing/index.html) | [trade/business/refinancing](https://rsshub.app/szes/rule/trade/business/refinancing) | +| [股票质押式回购](https://www.szse.cn/www/lawrules/rule/trade/business/pledge/index.html) | [trade/business/pledge](https://rsshub.app/szes/rule/trade/business/pledge) | +| [质押式报价回购](https://www.szse.cn/www/lawrules/rule/trade/business/price/index.html) | [trade/business/price](https://rsshub.app/szes/rule/trade/business/price) | +| [约定购回](https://www.szse.cn/www/lawrules/rule/trade/business/promise/index.html) | [trade/business/promise](https://rsshub.app/szes/rule/trade/business/promise) | +| [协议转让](https://www.szse.cn/www/lawrules/rule/trade/business/transfer/index.html) | [trade/business/transfer](https://rsshub.app/szes/rule/trade/business/transfer) | +| [其他](https://www.szse.cn/www/lawrules/rule/trade/business/oth/index.html) | [trade/business/oth](https://rsshub.app/szes/rule/trade/business/oth) | + +#### 跨境创新类 + +| 频道 | ID | +| ----------------------------------------------------------------------------- | ----------------------------------------------------- | +| [深港通](https://www.szse.cn/www/lawrules/rule/inno/szhk/index.html) | [inno/szhk](https://rsshub.app/szes/rule/inno/szhk) | +| [试点创新企业](https://www.szse.cn/www/lawrules/rule/inno/pilot/index.html) | [inno/pilot](https://rsshub.app/szes/rule/inno/pilot) | +| [H股全流通](https://www.szse.cn/www/lawrules/rule/inno/hc/index.html) | [inno/hc](https://rsshub.app/szes/rule/inno/hc) | +| [互联互通存托凭证](https://www.szse.cn/www/lawrules/rule/inno/gdr/index.html) | [inno/gdr](https://rsshub.app/szes/rule/inno/gdr) | + +#### 全部规则 + +| 频道 | ID | +| ----------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| [全部业务规则](https://www.szse.cn/www/lawrules/rule/allrules/bussiness/index.html) | [allrules/bussiness](https://rsshub.app/szes/rule/allrules/bussiness) | +| [规则汇编下载](https://www.szse.cn/www/lawrules/rule/allrules/rulejoin/index.html) | [allrules/rulejoin](https://rsshub.app/szes/rule/allrules/rulejoin) | + +#### 已废止规则 + +| 频道 | ID | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | +| [规则废止公告](https://www.szse.cn/www/lawrules/rule/repeal/announcement/index.html) | [repeal/announcement](https://rsshub.app/szes/rule/repeal/announcement) | +| [已废止规则文本](https://www.szse.cn/www/lawrules/rule/repeal/rules/index.html) | [repeal/rules](https://rsshub.app/szes/rule/repeal/rules) | + `, + categories: ['finance'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.szse.cn/www/lawrules/rule/:category'], + target: (params) => { + const category = params.category; + + return `/szse/rule${category ? `/${category}` : ''}`; + }, + }, + { + title: '综合类', + source: ['www.szse.cn/www/lawrules/rule/all/index.html'], + target: '/rule/all', + }, + { + title: '基础设施REITs类', + source: ['www.szse.cn/www/lawrules/rule/reits/index.html'], + target: '/rule/reits', + }, + { + title: '衍生品类', + source: ['www.szse.cn/www/lawrules/rule/derivative/index.html'], + target: '/rule/derivative', + }, + { + title: '会员管理类', + source: ['www.szse.cn/www/lawrules/rule/memberty/index.html'], + target: '/rule/memberty', + }, + { + title: '纪律处分与内部救济类', + source: ['www.szse.cn/www/lawrules/rule/pr/index.html'], + target: '/rule/pr', + }, + { + title: '股票类 - 发行上市审核', + source: ['www.szse.cn/www/lawrules/rule/stock/audit/index.html'], + target: '/rule/stock/audit', + }, + { + title: '股票类 - 发行承销', + source: ['www.szse.cn/www/lawrules/rule/stock/issue/index.html'], + target: '/rule/stock/issue', + }, + { + title: '股票类 - 通用', + source: ['www.szse.cn/www/lawrules/rule/stock/supervision/currency/index.html'], + target: '/rule/stock/supervision/currency', + }, + { + title: '股票类 - 主板专用', + source: ['www.szse.cn/www/lawrules/rule/stock/supervision/mb/index.html'], + target: '/rule/stock/supervision/mb', + }, + { + title: '股票类 - 创业板专用', + source: ['www.szse.cn/www/lawrules/rule/stock/supervision/chinext/index.html'], + target: '/rule/stock/supervision/chinext', + }, + { + title: '股票类 - 交易', + source: ['www.szse.cn/www/lawrules/rule/stock/trade/index.html'], + target: '/rule/stock/trade', + }, + { + title: '固收类 - 发行上市(挂牌)', + source: ['www.szse.cn/www/lawrules/rule/bond/bonds/list/index.html'], + target: '/rule/bond/bonds/list', + }, + { + title: '固收类 - 持续监管', + source: ['www.szse.cn/www/lawrules/rule/bond/bonds/supervision/index.html'], + target: '/rule/bond/bonds/supervision', + }, + { + title: '固收类 - 交易', + source: ['www.szse.cn/www/lawrules/rule/bond/bonds/trade/index.html'], + target: '/rule/bond/bonds/trade', + }, + { + title: '固收类 - 资产支持证券', + source: ['www.szse.cn/www/lawrules/rule/bond/abs/index.html'], + target: '/rule/bond/abs', + }, + { + title: '基金类 - 上市', + source: ['www.szse.cn/www/lawrules/rule/fund/list/index.html'], + target: '/rule/fund/list', + }, + { + title: '基金类 - 交易', + source: ['www.szse.cn/www/lawrules/rule/fund/trade/index.html'], + target: '/rule/fund/trade', + }, + { + title: '交易类 - 通用', + source: ['www.szse.cn/www/lawrules/rule/trade/current/index.html'], + target: '/rule/trade/current', + }, + { + title: '交易类 - 融资融券', + source: ['www.szse.cn/www/lawrules/rule/trade/business/margin/index.html'], + target: '/rule/trade/business/margin', + }, + { + title: '交易类 - 转融通', + source: ['www.szse.cn/www/lawrules/rule/trade/business/refinancing/index.html'], + target: '/rule/trade/business/refinancing', + }, + { + title: '交易类 - 股票质押式回购', + source: ['www.szse.cn/www/lawrules/rule/trade/business/pledge/index.html'], + target: '/rule/trade/business/pledge', + }, + { + title: '交易类 - 质押式报价回购', + source: ['www.szse.cn/www/lawrules/rule/trade/business/price/index.html'], + target: '/rule/trade/business/price', + }, + { + title: '交易类 - 约定购回', + source: ['www.szse.cn/www/lawrules/rule/trade/business/promise/index.html'], + target: '/rule/trade/business/promise', + }, + { + title: '交易类 - 协议转让', + source: ['www.szse.cn/www/lawrules/rule/trade/business/transfer/index.html'], + target: '/rule/trade/business/transfer', + }, + { + title: '交易类 - 其他', + source: ['www.szse.cn/www/lawrules/rule/trade/business/oth/index.html'], + target: '/rule/trade/business/oth', + }, + { + title: '跨境创新类 - 深港通', + source: ['www.szse.cn/www/lawrules/rule/inno/szhk/index.html'], + target: '/rule/inno/szhk', + }, + { + title: '跨境创新类 - 试点创新企业', + source: ['www.szse.cn/www/lawrules/rule/inno/pilot/index.html'], + target: '/rule/inno/pilot', + }, + { + title: '跨境创新类 - H股全流通', + source: ['www.szse.cn/www/lawrules/rule/inno/hc/index.html'], + target: '/rule/inno/hc', + }, + { + title: '跨境创新类 - 互联互通存托凭证', + source: ['www.szse.cn/www/lawrules/rule/inno/gdr/index.html'], + target: '/rule/inno/gdr', + }, + { + title: '全部规则 - 全部业务规则', + source: ['www.szse.cn/www/lawrules/rule/allrules/bussiness/index.html'], + target: '/rule/allrules/bussiness', + }, + { + title: '全部规则 - 规则汇编下载', + source: ['www.szse.cn/www/lawrules/rule/allrules/rulejoin/index.html'], + target: '/rule/allrules/rulejoin', + }, + { + title: '已废止规则 - 规则废止公告', + source: ['www.szse.cn/www/lawrules/rule/repeal/announcement/index.html'], + target: '/rule/repeal/announcement', + }, + { + title: '已废止规则 - 已废止规则文本', + source: ['www.szse.cn/www/lawrules/rule/repeal/rules/index.html'], + target: '/rule/repeal/rules', + }, + ], +}; diff --git a/lib/routes/szse/templates/inquire.art b/lib/routes/szse/templates/inquire.art index 7929c17eb9db4f..651ea3d6f37674 100644 --- a/lib/routes/szse/templates/inquire.art +++ b/lib/routes/szse/templates/inquire.art @@ -2,5 +2,5 @@ <p>公司简称:{{ item.gsjc }}</p> <p>发函日期:{{ item.fhrq }}</p> <p>函件类别:{{ item.hjlb }}</p> -<p>函件内容:<a href="{{ item.ck }}">详细内容</a></p> +<p>函件内容:<a href="http://reportdocs.static.szse.cn/{{ item.ck }}">详细内容</a></p> <p>公司回复:{{@ item.hfck }}</p> \ No newline at end of file diff --git a/lib/routes/szu/namespace.ts b/lib/routes/szu/namespace.ts index fc4521e37d9bd4..922fa25771c510 100644 --- a/lib/routes/szu/namespace.ts +++ b/lib/routes/szu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '深圳大学', url: 'yz.szu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/szu/yz/index.ts b/lib/routes/szu/yz/index.ts index bbd038b1e2c5e7..37a921b24c0dbc 100644 --- a/lib/routes/szu/yz/index.ts +++ b/lib/routes/szu/yz/index.ts @@ -26,8 +26,8 @@ export const route: Route = { maintainers: ['NagaruZ'], handler, description: `| 研究生 | 博士生 | - | ------ | ------ | - | 1 | 2 |`, +| ------ | ------ | +| 1 | 2 |`, }; async function handler(ctx) { diff --git a/lib/routes/t66y/index.ts b/lib/routes/t66y/index.ts index e316d4084e91f5..f8cc4825f09ccf 100644 --- a/lib/routes/t66y/index.ts +++ b/lib/routes/t66y/index.ts @@ -6,10 +6,10 @@ import { parseDate } from '@/utils/parse-date'; import { baseUrl, parseContent } from './utils'; export const route: Route = { - path: '/:id/:type?', + path: '/:id/:type?/:search?', categories: ['multimedia'], example: '/t66y/20/2', - parameters: { id: '分区 id, 可在分区页 URL 中找到', type: '类型 id, 可在分区类型过滤后的 URL 中找到' }, + parameters: { id: '分区 id, 可在分区页 URL 中找到', type: '类型 id, 可在分区类型过滤后的 URL 中找到', search: '主题类型筛选,可在分区主题类型筛选后的 URL 中找到,默认为 `today`' }, features: { requireConfig: false, requirePuppeteer: false, @@ -23,24 +23,45 @@ export const route: Route = { handler, description: `> 注意:并非所有的分区都有子类型,可以参考成人文学交流区的 \`古典武侠\` 这一子类型。 - | 亚洲无码原创区 | 亚洲有码原创区 | 欧美原创区 | 动漫原创区 | 国产原创区 | - | -------------- | -------------- | ---------- | ---------- | ---------- | - | 2 | 15 | 4 | 5 | 25 | +| 亚洲无码原创区 | 亚洲有码原创区 | 欧美原创区 | 动漫原创区 | 国产原创区 | +| -------------- | -------------- | ---------- | ---------- | ---------- | +| 2 | 15 | 4 | 5 | 25 | - | 中字原创区 | 转帖交流区 | HTTP 下载区 | 在线成人区 | - | ---------- | ---------- | ----------- | ---------- | - | 26 | 27 | 21 | 22 | +| 中字原创区 | 转帖交流区 | HTTP 下载区 | 在线成人区 | +| ---------- | ---------- | ----------- | ---------- | +| 26 | 27 | 21 | 22 | - | 技术讨论区 | 新时代的我们 | 达盖尔的旗帜 | 成人文学交流 | - | ---------- | ------------ | ------------ | ------------ | - | 7 | 8 | 16 | 20 |`, +| 技术讨论区 | 新时代的我们 | 达盖尔的旗帜 | 成人文学交流 | +| ---------- | ------------ | ------------ | ------------ | +| 7 | 8 | 16 | 20 | + + **主题过滤** + + > 因为该类型无法搭配子类型使用,所以使用时 \`type\` 子类型需使用 \`-999\` 占位 + +| 今日主题 | 热门主题 | 精华主题 | 原创主题 | 今日新作 | +| ------- | ------- | ------- | ------- | ------ | +| today | hot | digest | 1 | 2 |`, }; +const SEARCH_NAMES = { + today: '今日主题', + hot: '热门主题', + digest: '精华主题', + 1: '原创主题', + 2: '今日新作', +}; + +const DEFAULT_SEARCH_TYPE = 'today'; + async function handler(ctx) { - const { id, type } = ctx.req.param(); + const id = ctx.req.param('id'); + const type = (Number.parseInt(ctx.req.param('type')) || -999).toString(); + const isValidType = type !== '-999'; + const search = isValidType ? DEFAULT_SEARCH_TYPE : (ctx.req.param('search') ?? DEFAULT_SEARCH_TYPE); - const url = new URL(`thread0806.php?fid=${id}&search=today`, baseUrl); - type && url.searchParams.set('type', type); + const url = new URL(`thread0806.php?fid=${id}&search=${search}`, baseUrl); + isValidType && url.searchParams.set('type', type); const { data: res } = await got(url); const $ = cheerio.load(res); @@ -80,8 +101,9 @@ async function handler(ctx) { ); return { - title: (type ? `[${$('.t .fn b').text()}]` : '') + $('head title').text(), + title: (isValidType ? `[${$('.t .fn b').text()}] ` : '') + (search ? `[${SEARCH_NAMES[search]}] ` : '') + $('head title').text(), link: url.href, item: out, + allowEmpty: true, }; } diff --git a/lib/routes/t66y/namespace.ts b/lib/routes/t66y/namespace.ts index e51ba523e0a289..6afde2f911f571 100644 --- a/lib/routes/t66y/namespace.ts +++ b/lib/routes/t66y/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '草榴社区', url: 't66y.com', + lang: 'zh-CN', }; diff --git a/lib/routes/t66y/post.ts b/lib/routes/t66y/post.ts index 809d14cae2cc00..06cdd332e0c2c7 100644 --- a/lib/routes/t66y/post.ts +++ b/lib/routes/t66y/post.ts @@ -46,11 +46,11 @@ export const route: Route = { name: '帖子跟踪', maintainers: ['cnzgray'], handler, - description: `:::tip + description: `::: tip 帖子 id 查找办法: 打开想跟踪的帖子,比如:\`https://t66y.com/htm_data/20/1811/3286088.html\` 其中 \`3286088\` 就是帖子 id。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/tableau/namespace.ts b/lib/routes/tableau/namespace.ts index 5eb48b7fca587a..7b2c8088e48603 100644 --- a/lib/routes/tableau/namespace.ts +++ b/lib/routes/tableau/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Tableau', url: 'public.tableau.com', + lang: 'en', }; diff --git a/lib/routes/taiwanmobile/namespace.ts b/lib/routes/taiwanmobile/namespace.ts new file mode 100644 index 00000000000000..0b7182d2082af2 --- /dev/null +++ b/lib/routes/taiwanmobile/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '台灣大哥大', + url: 'www.taiwanmobile.com', + lang: 'zh-TW', +}; diff --git a/lib/routes/taiwanmobile/rate-plans.ts b/lib/routes/taiwanmobile/rate-plans.ts new file mode 100644 index 00000000000000..2eda102ce77c2e --- /dev/null +++ b/lib/routes/taiwanmobile/rate-plans.ts @@ -0,0 +1,64 @@ +import { DataItem, Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/rate-plans', + categories: ['other'], + example: '/taiwanmobile/rate-plans', + radar: [ + { + source: ['taiwanmobile.com/cs/public/servAnn/queryList.htm'], + }, + ], + name: '資費公告', + maintainers: ['Tsuyumi25'], + handler, + url: 'www.taiwanmobile.com/cs/public/servAnn/queryList.htm?type=1', +}; + +async function handler() { + const baseUrl = 'https://www.taiwanmobile.com'; + const listUrl = `${baseUrl}/cs/public/servAnn/queryList.htm?type=1`; + const response = await ofetch(listUrl); + + const $ = load(response); + + const list = $('.pagination_data') + .toArray() + .map((item) => { + const element = $(item); + const title = element.find('a').text().trim(); + const link = new URL(element.find('a').attr('href') ?? '', baseUrl).href; + const pubDate = parseDate(element.find('td').first().text(), 'YYYY/MM/DD'); + + return { + title, + link, + pubDate, + }; + }) + .slice(0, 20); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await ofetch(item.link); + const content = load(detailResponse); + + return { + ...item, + description: content('.v2-page-change__current').find('.v2-uikit__typography-text.-h3, .v2-m-faq-card__description.gray.pad_btm1').remove().end().html() || '暫無內容', + }; + }) + ) + ); + + return { + title: '台灣大哥大 - 資費公告', + link: listUrl, + item: items as DataItem[], + }; +} diff --git a/lib/routes/taiwannews/namespace.ts b/lib/routes/taiwannews/namespace.ts index 88f6a1432ff8c4..78e581db5bae72 100644 --- a/lib/routes/taiwannews/namespace.ts +++ b/lib/routes/taiwannews/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Taiwan News 台灣英文新聞', + name: 'Taiwan News', url: 'taiwannews.com.tw', + lang: 'en', }; diff --git a/lib/routes/tangshufang/index.ts b/lib/routes/tangshufang/index.ts index 8195f72c9e8536..913777196f5d95 100644 --- a/lib/routes/tangshufang/index.ts +++ b/lib/routes/tangshufang/index.ts @@ -26,16 +26,16 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 首页 | 老唐实盘 | 书房拾遗 | 理念 & 估值 | 经典陪读 | 财务套利 | - | ---- | -------- | -------- | ----------- | -------- | -------- | - | | shipan | wenda | linian | peidu | taoli | +| ---- | -------- | -------- | ----------- | -------- | -------- | +| | shipan | wenda | linian | peidu | taoli | - | 企业分析 | 白酒企业 | 腾讯控股 | 分众传媒 | 海康威视 | 其他企业 | - | -------- | -------- | -------- | -------- | -------- | -------- | - | qiye | baijiu | tengxun | fenzhong | haikang | qita | +| 企业分析 | 白酒企业 | 腾讯控股 | 分众传媒 | 海康威视 | 其他企业 | +| -------- | -------- | -------- | -------- | -------- | -------- | +| qiye | baijiu | tengxun | fenzhong | haikang | qita | - | 核心五篇 | 读者投稿 | 读书随笔 | 财报浅析 | 出行游记 | 巴芒连载 | - | -------- | -------- | -------- | -------- | -------- | -------- | - | hexin | tougao | suibi | caibao | youji | bamang |`, +| 核心五篇 | 读者投稿 | 读书随笔 | 财报浅析 | 出行游记 | 巴芒连载 | +| -------- | -------- | -------- | -------- | -------- | -------- | +| hexin | tougao | suibi | caibao | youji | bamang |`, }; async function handler(ctx) { diff --git a/lib/routes/tangshufang/namespace.ts b/lib/routes/tangshufang/namespace.ts index 596eef187596d2..eedb1027c2f885 100644 --- a/lib/routes/tangshufang/namespace.ts +++ b/lib/routes/tangshufang/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '唐书房', url: 'tangshufang.com', + lang: 'zh-CN', }; diff --git a/lib/routes/taobao/namespace.ts b/lib/routes/taobao/namespace.ts index 7247f08d3f200d..6c6a2f335e2777 100644 --- a/lib/routes/taobao/namespace.ts +++ b/lib/routes/taobao/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '淘宝众筹', url: 'izhongchou.taobao.com', + lang: 'zh-CN', }; diff --git a/lib/routes/taobao/zhongchou.ts b/lib/routes/taobao/zhongchou.ts index 2e92886bf0ec28..2573c34d9f7b97 100644 --- a/lib/routes/taobao/zhongchou.ts +++ b/lib/routes/taobao/zhongchou.ts @@ -23,8 +23,8 @@ export const route: Route = { maintainers: ['xyqfer', 'Fatpandac'], handler, description: `| 全部 | 科技 | 食品 | 动漫 | 设计 | 公益 | 娱乐 | 影音 | 书籍 | 游戏 | 其他 | - | ---- | ---- | ----------- | ---- | ------ | ---- | ---- | ----- | ---- | ---- | ----- | - | all | tech | agriculture | acg | design | love | tele | music | book | game | other |`, +| ---- | ---- | ----------- | ---- | ------ | ---- | ---- | ----- | ---- | ---- | ----- | +| all | tech | agriculture | acg | design | love | tele | music | book | game | other |`, }; async function handler(ctx) { diff --git a/lib/routes/taoguba/blog.ts b/lib/routes/taoguba/blog.ts index 871c0938ac6368..6b15d56a5f0738 100644 --- a/lib/routes/taoguba/blog.ts +++ b/lib/routes/taoguba/blog.ts @@ -1,12 +1,10 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; -import timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; +import { rootUrl, renderPostDetail } from './util'; export const route: Route = { - path: ['/blog/:id', '/user/:id'], + path: '/blog/:id', categories: ['finance'], example: '/taoguba/blog/252069', parameters: { id: '博客 id,可在对应博客页中找到' }, @@ -20,7 +18,7 @@ export const route: Route = { }, radar: [ { - source: ['taoguba.com.cn/blog/:id', 'taoguba.com.cn/'], + source: ['tgb.cn/blog/:id', 'tgb.cn/'], }, ], name: '用户博客', @@ -31,7 +29,6 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const rootUrl = 'https://www.taoguba.com.cn'; const currentUrl = `${rootUrl}/blog/${id}`; const response = await got({ @@ -51,53 +48,13 @@ async function handler(ctx) { const a = item.find('a').first(); return { - title: a.text(), + title: a.text().trim(), link: `${rootUrl}/${a.attr('href')}`, author, }; }); - items = await Promise.all( - items.map((item) => - cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - if (detailResponse.url.startsWith('https://www.taoguba.com.cn/topic/transfer')) { - item.description = '登录后查看完整文章'; - return item; - } - - const content = load(detailResponse.data); - - content('#videoImg').remove(); - content('img').each((_, img) => { - if (img.attribs.src2) { - img.attribs.src = img.attribs.src2; - delete img.attribs.src2; - delete img.attribs['data-original']; - } - }); - - item.description = content('#first').html(); - item.pubDate = timezone( - parseDate( - content('.article-data span') - .eq(1) - .text() - .match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/) - ), - +8 - ); - item.category = content('.article-topic-list span') - .toArray() - .map((item) => $(item).text().trim()); - - return item; - }) - ) - ); + items = await Promise.all(items.map(async (item) => await renderPostDetail(item))); return { title: `淘股吧 - ${author}`, diff --git a/lib/routes/taoguba/index.ts b/lib/routes/taoguba/index.ts index ce5c54cde0e02c..b062740e014894 100644 --- a/lib/routes/taoguba/index.ts +++ b/lib/routes/taoguba/index.ts @@ -1,24 +1,24 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; -import timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; +import { rootUrl, renderPostDetail } from './util'; export const route: Route = { - path: ['/index', '/:category?'], - name: 'Unknown', + path: '/:category?', + categories: ['finance'], + example: '/taoguba', + parameters: { id: '分类,见下表,默认为社区总版' }, + name: '淘股论坛', maintainers: ['nczitzk'], handler, description: `| 淘股论坛 | 社区总版 | 精华加油 | 网友点赞 | - | -------- | -------- | -------- | -------- | - | bbs | zongban | jinghua | dianzan |`, +| -------- | -------- | -------- | -------- | +| bbs | zongban | jinghua | dianzan |`, }; async function handler(ctx) { const category = ctx.req.param('category') ?? 'zongban'; - const rootUrl = 'https://www.taoguba.com.cn'; const currentUrl = `${rootUrl}/${category}/`; const response = await got({ @@ -28,63 +28,22 @@ async function handler(ctx) { const $ = load(response.data); - let items = $('.items-comment-list') + let items = $('.Nbbs-tiezi-lists') .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 70) .toArray() .map((item) => { item = $(item); - const a = item.find('.items-list-tittle a'); - a.find('b').remove(); + const a = item.find('.middle-list-tittle a'); return { - title: a.text(), + title: a.text().trim(), link: `${rootUrl}/${a.attr('href')}`, - author: item.find('.items-list-user a').text().trim(), + author: item.find('.middle-list-user a').text().trim(), }; }); - items = await Promise.all( - items.map((item) => - cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - if (detailResponse.url.startsWith('https://www.taoguba.com.cn/topic/transfer')) { - item.description = '登录后查看完整文章'; - return item; - } - - const content = load(detailResponse.data); - - content('#videoImg').remove(); - content('img').each((_, img) => { - if (img.attribs.src2) { - img.attribs.src = img.attribs.src2; - delete img.attribs.src2; - delete img.attribs['data-original']; - } - }); - - item.description = content('#first').html(); - item.pubDate = timezone( - parseDate( - content('.article-data span') - .eq(1) - .text() - .match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/) - ), - +8 - ); - item.category = content('.article-topic-list span') - .toArray() - .map((item) => $(item).text().trim()); - - return item; - }) - ) - ); + items = await Promise.all(items.map(async (item) => await renderPostDetail(item))); return { title: $('head title').text().trim().split('_')[0], diff --git a/lib/routes/taoguba/namespace.ts b/lib/routes/taoguba/namespace.ts index 869a807a69a09b..2e08e11bac56c0 100644 --- a/lib/routes/taoguba/namespace.ts +++ b/lib/routes/taoguba/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '淘股吧', - url: 'taoguba.com.cn', + url: 'tgb.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/taoguba/util.ts b/lib/routes/taoguba/util.ts new file mode 100644 index 00000000000000..8d2400ef9e03fa --- /dev/null +++ b/lib/routes/taoguba/util.ts @@ -0,0 +1,48 @@ +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; + +const rootUrl = 'https://www.tgb.cn'; + +const renderPostDetail = async (item) => + await cache.tryGet(item.link, async () => { + const detailResponse = await got({ + method: 'get', + url: item.link, + }); + + const content = load(detailResponse.data); + + content('#videoImg').remove(); + content('img').each((_, img) => { + if (img.attribs.src2) { + img.attribs.src = img.attribs.src2; + delete img.attribs.src2; + delete img.attribs['data-original']; + } + }); + + item.description = content('#first').html(); + if (detailResponse.url?.startsWith('https://www.tgb.cn/topic/transfer') || content('.login-view-button').length !== 0) { + item.description += '<br>登录后可查看完整文章'; + } + + item.pubDate = timezone( + parseDate( + content('.article-data > span:nth-child(2)') + .text() + .match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/)[0] + ), + +8 + ); + + item.category = content('.classify') + .toArray() + .map((item) => content(item).text().trim()); + + return item; + }); + +export { rootUrl, renderPostDetail }; diff --git a/lib/routes/taptap/changelog-cn.ts b/lib/routes/taptap/changelog-cn.ts new file mode 100644 index 00000000000000..52c9574b0347b5 --- /dev/null +++ b/lib/routes/taptap/changelog-cn.ts @@ -0,0 +1,29 @@ +import { Route } from '@/types'; +import { handler } from './common/changelog'; + +export const route: Route = { + path: '/changelog/:id/:lang?', + categories: ['game'], + example: '/taptap/changelog/60809/en_US', + parameters: { + id: '游戏 ID,游戏主页 URL 中获取', + lang: '语言,默认使用 `zh_CN`,亦可使用 `en_US`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.taptap.cn/app/:id'], + target: '/changelog/:id', + }, + ], + name: '游戏更新', + maintainers: ['hoilc', 'ETiV'], + handler, +}; diff --git a/lib/routes/taptap/changelog-intl.ts b/lib/routes/taptap/changelog-intl.ts new file mode 100644 index 00000000000000..11163c18154c5e --- /dev/null +++ b/lib/routes/taptap/changelog-intl.ts @@ -0,0 +1,34 @@ +import { Route } from '@/types'; +import { handler } from './common/changelog'; + +export const route: Route = { + path: '/intl/changelog/:id/:lang?', + categories: ['game'], + example: '/taptap/intl/changelog/191001/zh_TW', + parameters: { + id: "Game's App ID, you may find it from the URL of the Game", + lang: 'Language, checkout the table below for possible values, default is `en_US`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.taptap.io/app/:id'], + target: '/intl/changelog/:id', + }, + ], + name: "Game's Changelog", + maintainers: ['hoilc', 'ETiV'], + handler, + description: `Language Code + +| English (US) | 繁體中文 | 한국어 | 日本語 | +| ------------ | -------- | ------ | ------ | +| en_US | zh_TW | ko_KR | ja_JP |`, +}; diff --git a/lib/routes/taptap/changelog.ts b/lib/routes/taptap/changelog.ts deleted file mode 100644 index 7f7d124da3c5f1..00000000000000 --- a/lib/routes/taptap/changelog.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import { parseDate } from '@/utils/parse-date'; -import { getRootUrl, appDetail, X_UA } from './utils'; - -export const route: Route = { - path: ['/changelog/:id/:lang?', '/intl/changelog/:id/:lang?'], - categories: ['game'], - example: '/taptap/changelog/60809/en_US', - parameters: { id: '游戏 ID,游戏主页 URL 中获取', lang: '语言,默认使用 `zh_CN`,亦可使用 `en_US`' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['taptap.com/app/:id'], - target: '/changelog/:id', - }, - ], - name: '游戏更新', - maintainers: ['hoilc', 'ETiV'], - handler, - description: `#### 语言代码 - - | English (US) | 繁體中文 | 한국어 | 日本語 | - | ------------ | -------- | ------ | ------ | - | en\_US | zh\_TW | ko\_KR | ja\_JP |`, -}; - -async function handler(ctx) { - const is_intl = ctx.req.url.indexOf('/intl/') === 0; - const id = ctx.req.param('id'); - const lang = ctx.req.param('lang') ?? (is_intl ? 'en_US' : 'zh_CN'); - - const url = `${getRootUrl(is_intl)}/app/${id}`; - - const app_detail = await appDetail(id, lang, is_intl); - - const app_img = app_detail.app.icon.original_url; - const app_name = app_detail.app.title; - const app_description = `${app_name} by ${app_detail.app.developers.map((item) => item.name).join(' & ')}`; - - const response = await got({ - method: 'get', - url: `${getRootUrl(is_intl)}/webapiv2/apk/v1/list-by-app?app_id=${id}&from=0&limit=10&${X_UA(lang)}`, - headers: { - Referer: url, - }, - }); - - const list = response.data.data.list; - - return { - title: `TapTap 更新记录 ${app_name}`, - description: app_description, - link: url, - image: app_img, - item: list.map((item) => ({ - title: `${app_name} / ${item.version_label}`, - description: item.whatsnew.text, - pubDate: parseDate(item.update_date * 1000), - link: url, - guid: item.version_label, - })), - }; -} diff --git a/lib/routes/taptap/common/changelog.ts b/lib/routes/taptap/common/changelog.ts new file mode 100644 index 00000000000000..fd320e3a0a35bc --- /dev/null +++ b/lib/routes/taptap/common/changelog.ts @@ -0,0 +1,40 @@ +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { getRootUrl, appDetail, X_UA } from '../utils'; + +export async function handler(ctx) { + const requestPath = ctx.req.path.replace('/taptap', ''); + const isIntl = requestPath.startsWith('/intl/'); + const id = ctx.req.param('id'); + const lang = ctx.req.param('lang') ?? (isIntl ? 'en_US' : 'zh_CN'); + + const url = `${getRootUrl(isIntl)}/app/${id}`; + + const detail = await appDetail(id, lang, isIntl); + + const appImg = detail.app.icon.original_url; + const appName = detail.app.title; + const appDescription = `${appName}${detail.app.developers ? ' by' + detail.app.developers.map((item) => item.name).join(' & ') : ''}`; + + const response = await ofetch(`${getRootUrl(isIntl)}/webapiv2/apk/v1/list-by-app?app_id=${id}&from=0&limit=10&${X_UA(lang)}`, { + headers: { + Referer: url, + }, + }); + + const list = response.data.list; + + return { + title: `TapTap 更新记录 ${appName}`, + description: appDescription, + link: url, + image: appImg, + item: list.map((item) => ({ + title: `${appName} / ${item.version_label}`, + description: item.whatsnew.text, + pubDate: parseDate(item.update_date, 'X'), + link: url, + guid: item.version_label, + })), + }; +} diff --git a/lib/routes/taptap/common/review.ts b/lib/routes/taptap/common/review.ts new file mode 100644 index 00000000000000..80bb0602e6e994 --- /dev/null +++ b/lib/routes/taptap/common/review.ts @@ -0,0 +1,126 @@ +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { getRootUrl, appDetail, X_UA } from '../utils'; + +/* +const sortMap = { + default: { + en_US: 'Default', + zh_CN: '预设', + zh_TW: '預設', + }, + recent: { + en_US: 'Latest', + zh_CN: '最新', + zh_TW: '最新', + }, + hot: { + en_US: 'Popular', + zh_CN: '热门', + zh_TW: '熱門', + }, + spent: { + en_US: 'Play Time', + zh_CN: '游戏时长', + zh_TW: '遊戲時長', + }, +}; + +const intlSortMap = { + helpful: { + en_US: 'Most Helpful', + zh_TW: '最有幫助', + ja_JP: '最も役立つ', + ko_KR: '가장 도움이 된', + }, + recent: { + en_US: 'Most Recent', + zh_TW: '最新', + ja_JP: '最も最近', + ko_KR: '최근순', + }, +}; +*/ + +const makeSortParam = (isIntl: boolean, order: string) => { + if (isIntl) { + if (order === 'helpful' || order === 'recent') { + return `type=${order}`; + } + return 'type=helpful'; + } else { + if (order === 'new' || order === 'hot') { + return `sort=${order}`; + } + return 'sort=hot'; + } +}; + +const fetchMainlandItems = async (params) => { + const id = params.id; + const order = params.order ?? 'hot'; + const lang = params.lang ?? 'zh_CN'; + + let url = `${getRootUrl(false)}/webapiv2/review/v2/list-by-app?app_id=${id}&limit=10`; + url += `&${makeSortParam(false, order)}`; + url += `&${X_UA(lang)}`; + + const reviewListResponse = await ofetch(url); + + return reviewListResponse.data.list.map((review) => { + const author = review.moment.author.user.name; + const score = review.moment.review.score; + return { + title: `${author} - ${score}星`, + author, + description: review.moment.review.contents.text + (review.moment.review.contents.images ? review.moment.review.contents.images.map((img) => `<img src="${img.original_url}">`).join('') : ''), + link: `${getRootUrl(false)}/review/${review.moment.review.id}`, + pubDate: parseDate(review.moment.publish_time, 'X'), + }; + }); +}; + +const fetchIntlItems = async (params) => { + const id = params.id; + const order = params.order ?? 'helpful'; + const lang = params.lang ?? 'en_US'; + + let url = `${getRootUrl(true)}/webapiv2/feeds/v3/by-app?app_id=${id}&limit=10`; + url += `&${makeSortParam(true, order)}`; + url += `&${X_UA(lang)}`; + + const reviewListResponse = await ofetch(url); + + return reviewListResponse.data.list.map((review) => { + const author = review.post.user.name; + const score = review.post.list_fields.app_ratings[id].score; + return { + title: `${author} - ${'★'.repeat(score)}`, + author, + description: review.post.list_fields.summary || review.post.list_fields.title, + link: `${getRootUrl(true)}/post/${review.post.id_str}`, + pubDate: parseDate(review.post.published_time, 'X'), + }; + }); +}; + +export async function handler(ctx) { + const requestPath = ctx.req.path.replace('/taptap', ''); + const isIntl = requestPath.startsWith('/intl/'); + const id = ctx.req.param('id'); + const order = ctx.req.param('order') ?? 'default'; + const lang = ctx.req.param('lang') ?? (isIntl ? 'en_US' : 'zh_CN'); + + const detail = await appDetail(id, lang, isIntl); + const appImg = detail.app.icon.original_url; + const appName = detail.app.title; + + const items = isIntl ? await fetchIntlItems({ id, order, lang }) : await fetchMainlandItems({ id, order, lang }); + + return { + title: `TapTap 评价 ${appName}`, + link: `${getRootUrl(isIntl)}/app/${id}/review?${makeSortParam(isIntl, order)}`, + image: appImg, + item: items, + }; +} diff --git a/lib/routes/taptap/namespace.ts b/lib/routes/taptap/namespace.ts index a09009cacbc761..1d1a54ce9a9be2 100644 --- a/lib/routes/taptap/namespace.ts +++ b/lib/routes/taptap/namespace.ts @@ -1,10 +1,11 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'TapTap 中国', - url: 'taptap.com', - description: `:::warning + name: 'TapTap', + url: 'www.taptap.io', + description: `::: warning 由于区域限制,需要在有国内 IP 的机器上自建才能正常获取 RSS。\ 而对于《TapTap 国际版》则需要部署在具有海外出口的 IP 上才可正常获取 RSS。 :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/taptap/review-cn.ts b/lib/routes/taptap/review-cn.ts new file mode 100644 index 00000000000000..dc38b771a21d1c --- /dev/null +++ b/lib/routes/taptap/review-cn.ts @@ -0,0 +1,33 @@ +import { Route } from '@/types'; +import { handler } from './common/review'; + +export const route: Route = { + path: '/review/:id/:order?/:lang?', + categories: ['game'], + example: '/taptap/review/142793/hot', + parameters: { + id: '游戏 ID,游戏主页 URL 中获取', + order: '排序方式,空为综合,可选如下', + lang: '语言,`zh-CN` 或 `zh-TW`,默认为 `zh-CN`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.taptap.cn/app/:id/review', 'www.taptap.cn/app/:id'], + target: '/review/:id', + }, + ], + name: '游戏评价', + maintainers: ['hoilc', 'TonyRL'], + handler, + description: `| 最新 | 综合 | +| --- | --- | +| new | hot |`, +}; diff --git a/lib/routes/taptap/review-intl.ts b/lib/routes/taptap/review-intl.ts new file mode 100644 index 00000000000000..88936dec4a63a1 --- /dev/null +++ b/lib/routes/taptap/review-intl.ts @@ -0,0 +1,41 @@ +import { Route } from '@/types'; +import { handler } from './common/review'; + +export const route: Route = { + path: '/intl/review/:id/:order?/:lang?', + categories: ['game'], + example: '/taptap/intl/review/82354/recent', + parameters: { + id: "Game's App ID, you may find it from the URL of the Game", + order: 'Sort Method, default is `helpful`, checkout the table below for possible values', + lang: 'Language, checkout the table below for possible values, default is `en_US`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.taptap.io/app/:id/review', 'www.taptap.io/app/:id'], + target: '/intl/review/:id', + }, + ], + name: 'Ratings & Reviews', + maintainers: ['hoilc', 'TonyRL', 'ETiV'], + handler, + description: `Sort Method + +| Most Helpful | Most Recent | +| ------------ | ----------- | +| helpful | recent | + +Language Code + +| English (US) | 繁體中文 | 한국어 | 日本語 | +| ------------ | -------- | ------ | ------ | +| en_US | zh_TW | ko_KR | ja_JP |`, +}; diff --git a/lib/routes/taptap/review.ts b/lib/routes/taptap/review.ts deleted file mode 100644 index f2caa46ec661d0..00000000000000 --- a/lib/routes/taptap/review.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import { parseDate } from '@/utils/parse-date'; -import { getRootUrl, appDetail, X_UA } from './utils'; - -const sortMap = { - default: { - en_US: 'Default', - zh_CN: '预设', - zh_TW: '預設', - }, - new: { - en_US: 'Latest', - zh_CN: '最新', - zh_TW: '最新', - }, - hot: { - en_US: 'Popular', - zh_CN: '热门', - zh_TW: '熱門', - }, - spent: { - en_US: 'Play Time', - zh_CN: '游戏时长', - zh_TW: '遊戲時長', - }, -}; - -const intlSortMap = { - default: { - en_US: 'Most Relevant', - zh_TW: '最相關', - ja_JP: '関連性が高い', - ko_KR: '관련도순', - }, - new: { - en_US: 'Most Recent', - zh_TW: '最新', - ja_JP: '最も最近', - ko_KR: '최근순', - }, -}; - -const makeSortParam = (isIntl, order) => { - if (isIntl) { - if (order === 'new') { - return `sort=${order}`; - } - } else { - if (order === 'new' || order === 'hot' || order === 'spent') { - return `sort=${order}`; - } - } - return ''; -}; - -const fetchMainlandItems = async (params) => { - const id = params.id; - const order = params.order ?? 'default'; - const lang = params.lang ?? 'zh_CN'; - - let url = `${getRootUrl(false)}/webapiv2/review/v2/by-app?app_id=${id}&limit=10`; - url += `&${makeSortParam(false, order)}`; - url += `&${X_UA(lang)}`; - - const reviews_list_response = await got(url); - const reviews_list = reviews_list_response.data.data.list; - - return reviews_list.map((review) => { - const author = review.moment.author.user.name; - const score = review.moment.extended_entities.reviews[0].score; - return { - title: `${author} - ${score}星`, - author, - description: review.moment.extended_entities.reviews[0].contents.text, - link: `${getRootUrl(false)}/review/${review.moment.extended_entities.reviews[0].id}`, - pubDate: parseDate(review.moment.extended_entities.reviews[0].created_time * 1000), - }; - }); -}; - -const fetchIntlItems = async (params) => { - const id = params.id; - const order = params.order ?? 'default'; - const lang = params.lang ?? 'en_US'; - - let url = `${getRootUrl(true)}/webapiv2/feeds/v1/app-ratings?app_id=${id}&limit=10`; - url += `&${makeSortParam(true, order)}`; - url += `&${X_UA(lang)}`; - - const reviews_list_response = await got(url); - const reviews_list = reviews_list_response.data.data.list; - - return reviews_list.map((review) => { - const author = review.post.user.name; - const score = review.post.list_fields.app_ratings[id].score; - return { - title: `${author} - ${score}星`, - author, - description: review.post.list_fields.summary || review.post.list_fields.title, - link: `${getRootUrl(true)}/post/${review.post.id_str}`, - pubDate: parseDate(review.post.published_time * 1000), - }; - }); -}; - -export const route: Route = { - path: ['/review/:id/:order?/:lang?', '/intl/review/:id/:order?/:lang?'], - categories: ['game'], - example: '/taptap/review/142793/hot', - parameters: { id: '游戏 ID,游戏主页 URL 中获取', order: '排序方式,空为默认排序,可选如下', lang: '语言,`zh-CN`或`zh-TW`,默认为`zh-CN`' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['taptap.com/app/:id/review', 'taptap.com/app/:id'], - target: '/review/:id', - }, - ], - name: '游戏评价', - maintainers: ['hoilc', 'TonyRL'], - handler, - description: `#### 排序方式 - - | 最相关 | 最新 | - | ------- | ---- | - | default | new | - - #### 语言代码 - - | English (US) | 繁體中文 | 한국어 | 日本語 | - | ------------ | -------- | ------ | ------ | - | en\_US | zh\_TW | ko\_KR | ja\_JP |`, - description: `| 最新 | 最热 | 游戏时长 | 默认排序 | - | ------ | ---- | -------- | -------- | - | update | hot | spent | default |`, -}; - -async function handler(ctx) { - const is_intl = ctx.req.url.indexOf('/intl/') === 0; - const id = ctx.req.param('id'); - const order = ctx.req.param('order') ?? 'default'; - const lang = ctx.req.param('lang') ?? (is_intl ? 'en_US' : 'zh_CN'); - - const app_detail = await appDetail(id, lang, is_intl); - const app_img = app_detail.app.icon.original_url; - const app_name = app_detail.app.title; - - const items = is_intl ? await fetchIntlItems(ctx.params) : await fetchMainlandItems(ctx.params); - - const ret = { - title: `TapTap 评价 ${app_name} - ${(is_intl ? intlSortMap : sortMap)[order][lang]}排序`, - link: `${getRootUrl(is_intl)}/app/${id}/review?${makeSortParam(is_intl, order)}`, - image: app_img, - item: items, - }; - - ctx.set('json', ret); - return ret; -} diff --git a/lib/routes/taptap/templates/videoPost.art b/lib/routes/taptap/templates/videoPost.art index 184bcecb2bebf2..89d092cdf86487 100644 --- a/lib/routes/taptap/templates/videoPost.art +++ b/lib/routes/taptap/templates/videoPost.art @@ -1,3 +1,2 @@ -{{ if intro }}{{@ intro }}{{ /if }} <p>Preview</p> <p><img src="{{@ previewUrl }}" /></p> diff --git a/lib/routes/taptap/topic.ts b/lib/routes/taptap/topic.ts index 5df3417e78e0b4..f99216b3870b1d 100644 --- a/lib/routes/taptap/topic.ts +++ b/lib/routes/taptap/topic.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 { parseDate } from '@/utils/parse-date'; import { getRootUrl, X_UA, appDetail, imagePost, topicPost, videoPost } from './utils'; @@ -27,7 +27,12 @@ export const route: Route = { path: '/topic/:id/:type?/:sort?/:lang?', categories: ['game'], example: '/taptap/topic/142793/official', - parameters: { id: '游戏 ID,游戏主页 URL 中获取', type: '论坛版块,默认显示所有帖子,论坛版块 URL 中 `type` 参数,见下表,默认为 `feed`', sort: '排序,见下表,默认为 `created`', lang: '语言,`zh-CN`或`zh-TW`,默认为`zh-CN`' }, + parameters: { + id: '游戏 ID,游戏主页 URL 中获取', + type: '论坛版块,默认显示所有帖子,论坛版块 URL 中 `type` 参数,见下表,默认为 `feed`', + sort: '排序,见下表,默认为 `created`', + lang: '语言,`zh-CN`或`zh-TW`,默认为`zh-CN`', + }, features: { requireConfig: false, requirePuppeteer: false, @@ -38,7 +43,7 @@ export const route: Route = { }, radar: [ { - source: ['taptap.com/app/:id/topic', 'taptap.com/app/:id'], + source: ['taptap.cn/app/:id/topic', 'taptap.cn/app/:id'], target: '/topic/:id', }, ], @@ -46,12 +51,12 @@ export const route: Route = { maintainers: ['hoilc', 'TonyRL'], handler, description: `| 全部 | 精华 | 官方 | 影片 | - | ---- | ----- | -------- | ----- | - | feed | elite | official | video | +| ---- | ----- | -------- | ----- | +| feed | elite | official | video | - | 发布时间 | 回复时间 | - | -------- | --------- | - | created | commented |`, +| 发布时间 | 回复时间 | 默认排序 | +| -------- | --------- | ------- | +| created | commented | default |`, }; async function handler(ctx) { @@ -61,31 +66,30 @@ async function handler(ctx) { const type = ctx.req.param('type') ?? 'feed'; const sort = ctx.req.param('sort') ?? 'created'; const groupId = appData.group.id; - const app_img = appData.app.icon.original_url || appData.app.icon.url; - const app_name = appData.app.title; - const url = `${getRootUrl(false)}/webapiv2/feed/v6/by-group?group_id=${groupId}&type=${type}&sort=${sort}&${X_UA(lang)}`; + const appImg = appData.app.icon.original_url || appData.app.icon.url; + const appName = appData.app.title; + const url = `${getRootUrl(false)}/webapiv2/feed/v7/by-group?group_id=${groupId}&type=${type}&sort=${sort}&${X_UA(lang)}`; - const topics_list_response = await got(url); - const topics_list = topics_list_response.data; + const topicsList = await ofetch(url); + const list = topicsList.data.list; const out = await Promise.all( - topics_list.data.list.map((list) => { - const link = list.moment.sharing?.url || list.moment.extended_entities?.topics?.[0].sharing.url || list.moment.extended_entities?.videos?.[0].sharing.url; + list.map(({ moment }) => { + const link = moment.sharing.url; return cache.tryGet(link, async () => { - const author = list.moment.author.user.name; - const topicId = list.moment.extended_entities?.topics?.[0].id; + const author = moment.author.user.name; + const topicId = moment.topic.id_str; // raw_text sometimes is "" so || is better than ?? - const title = list.moment.contents?.raw_text || list.moment.extended_entities?.topics?.[0].title || list.moment.extended_entities?.videos?.[0].title; - const createdTime = list.moment.created_time; - let description = ''; - if (topicId) { - description = await topicPost(appId, topicId, lang); - } else { - description = list.moment.extended_entities?.topics?.[0].summary || list.moment.contents?.raw_text || videoPost(list.moment.extended_entities?.videos?.[0]); - if (list.moment.extended_entities?.images) { - description += imagePost(list.moment.extended_entities.images); + const title = moment.topic.title || moment.topic.summary.split(' ')[0]; + let description = moment.topic.summary || ''; + if (moment.topic.pin_video) { + description += videoPost(moment.topic.pin_video); + if (moment.topic.footer_images?.images) { + description += imagePost(moment.topic.footer_images.images); } + } else { + description = await topicPost(appId, topicId, lang); } return { @@ -93,16 +97,16 @@ async function handler(ctx) { description, author, link, - pubDate: parseDate(createdTime, 'X'), + pubDate: parseDate(moment.created_time, 'X'), }; }); }) ); const ret = { - title: `${app_name} - ${typeMap[type][lang]} - TapTap 论坛`, + title: `${appName} - ${typeMap[type][lang]} - TapTap 论坛`, link: `${getRootUrl(false)}/app/${appId}/topic?type=${type}&sort=${sort}`, - image: app_img, + image: appImg, item: out.filter((item) => item !== ''), }; @@ -110,7 +114,7 @@ async function handler(ctx) { ...ret, appId, groupId, - topics_list, + topicsList, }); return ret; } diff --git a/lib/routes/taptap/types.ts b/lib/routes/taptap/types.ts new file mode 100644 index 00000000000000..d8f35e3c4e7e55 --- /dev/null +++ b/lib/routes/taptap/types.ts @@ -0,0 +1,363 @@ +export interface Detail { + group: Group; + app: App; +} + +interface Group { + id: number; + app_id: number; + title: string; + intro: string; + has_treasure: boolean; + has_official: boolean; + icon: Icon; + banner: Banner; + moderators: Moderator[]; + log: Log; + event_log: EventLog; + stat: Stat; + web_url: string; + style_info: StyleInfo; + terms: Term[]; + sharing: Sharing; + new_rec_list: NewRecListItem[]; + actions: Actions; + title_labels: TitleLabel[]; +} + +interface Icon { + url: string; + medium_url: string; + small_url: string; + original_url: string; + original_format: string; + width: number; + height: number; + color: string; + original_size: number; +} + +interface Banner { + url: string; + medium_url: string; + small_url: string; + original_url: string; + original_format: string; + width: number; + height: number; + color: string; + original_size: number; +} + +interface Moderator { + id: number; + name: string; + avatar: string; + medium_avatar: string; + avatar_pendant: string; + badges: Badge[]; + verified: Verified; +} + +interface Badge { + id: number; + title: string; + description: string; + is_wear: boolean; + icon: BadgeIcon; + style: BadgeStyle; + time: number; + unlock_tips: string; + level: number; + status: number; +} + +interface BadgeIcon { + small: string; + middle: string; + large: string; + small_border: string; +} + +interface BadgeStyle { + background_image: string; + background_color: string; + font_color: string; + border_background_color: string; +} + +interface Verified { + type: string; + reason: string; + url: string; +} + +interface Log { + follow: Follow; + unfollow: Follow; +} + +interface Follow { + uri: string; + params: FollowParams; +} + +interface FollowParams { + type: string; + APIVersion: string; + paramId: string; + paramType: string; +} + +interface EventLog { + paramType: string; + paramId: string; +} + +interface Stat { + favorite_count: number; + topic_count: number; + elite_topic_count: number; + recent_topic_count: number; + official_topic_count: number; + top_topic_count: number; + video_count: number; + user_moment_count: number; + app_moment_count: number; + image_moment_count: number; + treasure_count: number; + topic_page_view: number; + feed_count: number; + topic_pv_total: number; +} + +interface StyleInfo { + background_color: string; + font_color: string; +} + +interface Term { + label: string; + type: string; + index: string; + params: TermParams; + sort: TermSort[]; + log: TermLog; + position: string; + referer_ext: string; + log_keyword: string; + top_params: TermParams; + sub_terms: SubTerm[]; +} + +interface TermParams { + type: string; +} + +interface TermSort { + label: string; + params: SortParams; + icon_type: string; +} + +interface SortParams { + sort: string; +} + +interface TermLog { + page_view: PageView; +} + +interface PageView { + uri: string; + params: PageViewParams; +} + +interface PageViewParams { + type: string; + APIVersion: string; + paramId: string; + paramType: string; + position: string; +} + +interface SubTerm { + label: string; + type: string; + index: string; + params: SubTermParams; + top_params: SubTermTopParams; + sort: SubTermSort[]; + log: SubTermLog; + referer_ext: string; + log_keyword: string; + uri: string; + web_url: string; +} + +interface SubTermParams { + group_label_id: string; + type: string; +} + +interface SubTermTopParams { + group_label_id: string; + is_top: string; + type: string; +} + +interface SubTermSort { + label: string; + params: SortParams; + icon_type: string; +} + +interface SubTermLog { + page_view: PageView; +} + +interface Sharing { + url: string; + title: string; + description: string; + image: Icon; +} + +interface NewRecListItem { + icon: Icon; + uri: string; + url: string; + web_url: string; + label: string; +} + +interface Actions { + publish_contents: boolean; +} + +interface TitleLabel { + label: string; + icon: Icon; +} + +interface App { + id: number; + identifier: string; + title: string; + title_labels: string[]; + icon: Icon; + price: Price; + uri: Uri; + can_view: boolean; + released_time: number; + button_flag: number; + style: number; + hidden_button: boolean; + is_deny_minors: boolean; + stat: AppStat; + ad_banner: Icon; + top_banner: Icon; + banner: Icon; + tags: Tag[]; + log: AppLog; + event_log: EventLog; + can_buy_redeem_code: CanBuyRedeemCode; + show_module: ShowModule[]; + complaint: Complaint; + serial_number: SerialNumber; + rec_text: string; + readable_id: string; + m_button_map: object; + description: Description; + title_labels_v2: TitleLabel[]; + stat_key: string; + include_app_product_type_complete: boolean; + is_console_game: boolean; +} + +interface Price { + taptap_current: string; + discount_rate: number; +} + +interface Uri { + google: string; + google_play: string; + apple: string; + download_site: string; +} + +interface AppStat { + rating: Rating; + vote_info: VoteInfo; + hits_total: number; + play_total: number; + bought_count: number; + feed_count: number; + reserve_count: number; + recent_sandbox_played_count: number; + album_count: number; + review_count: number; + topic_count: number; + video_count: number; + official_topic_count: number; + official_video_count: number; + official_album_count: number; + fans_count: number; +} + +interface Rating { + score: string; + max: number; + latest_score: string; + latest_version_score: string; +} + +interface VoteInfo { + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; +} + +interface Tag { + id: number; + value: string; + uri: string; + web_url: string; +} + +interface AppLog { + follow: Follow; + open: Follow; + page_view: Follow; + play: Follow; + reserve: Follow; + unfollow: Follow; + unreserved: Follow; +} + +interface CanBuyRedeemCode { + flag: boolean; +} + +interface ShowModule { + key: string; + value: boolean; +} + +interface Complaint { + uri: string; + web_url: string; + url: string; +} + +interface SerialNumber { + number_exists: boolean; + button_action: number; +} + +interface Description { + text: string; +} diff --git a/lib/routes/taptap/utils.ts b/lib/routes/taptap/utils.ts index 9c7ff36f88471d..602e91963ada63 100644 --- a/lib/routes/taptap/utils.ts +++ b/lib/routes/taptap/utils.ts @@ -1,24 +1,26 @@ import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import path from 'node:path'; import { art } from '@/utils/render'; +import cache from '@/utils/cache'; +import { Detail } from './types'; -// Please do not change %26 to & -const X_UA = (lang = 'zh_CN') => `X-UA=V=1%26PN=WebApp%26VN=0.1.0%26LANG=${lang}%26PLT=PC`; +const X_UA = (lang = 'zh_CN') => `X-UA=${encodeURIComponent(`V=1&PN=WebApp&VN=0.1.0&LANG=${lang}&PLT=PC`)}`; -const getRootUrl = (isIntl = false) => (isIntl ? 'https://www.taptap.io' : 'https://www.taptap.com'); +const getRootUrl = (isIntl = false) => (isIntl ? 'https://www.taptap.io' : 'https://www.taptap.cn'); -const appDetail = async (appId, lang = 'zh_CN', isIntl = false) => { - const { data } = await got(`${getRootUrl(isIntl)}/webapiv2/group/v1/detail?app_id=${appId}&${X_UA(lang)}`, { - headers: { - Referer: `${getRootUrl(isIntl)}/app/${appId}`, - }, - }); - return data.data; -}; +const appDetail = (appId, lang = 'zh_CN', isIntl = false) => + cache.tryGet(`taptap:appDetail:${appId}:${lang}:${isIntl}`, async () => { + const data = await ofetch(`${getRootUrl(isIntl)}/webapiv2/group/v1/detail?app_id=${appId}&${X_UA(lang)}`, { + headers: { + Referer: `${getRootUrl(isIntl)}/app/${appId}`, + }, + }); + return data.data; + }) as Promise<Detail>; const imagePost = (images) => art(path.join(__dirname, 'templates/imagePost.art'), { @@ -26,25 +28,23 @@ const imagePost = (images) => }); const topicPost = async (appId, topicId, lang = 'zh_CN') => { - const res = await got(`${getRootUrl(false)}/webapiv2/topic/v1/detail?id=${topicId}&${X_UA(lang)}`, { + const res = await ofetch(`${getRootUrl(false)}/webapiv2/topic/v1/detail?id=${topicId}&${X_UA(lang)}`, { headers: { Referer: `${getRootUrl(false)}/app/${appId}`, }, }); - const $ = load(res.data.data.first_post.contents.text, null, false); + const $ = load(res.data.first_post.contents.text, null, false); $('img').each((_, e) => { - e = $(e); - e.attr('src', e.attr('data-origin-url')); - e.attr('referrerpolicy', 'no-referrer'); - e.removeAttr('data-origin-url'); + const $e = $(e); + $e.attr('src', $e.attr('data-origin-url')); + $e.removeAttr('data-origin-url'); }); return $.html(); }; const videoPost = (video) => art(path.join(__dirname, 'templates/videoPost.art'), { - intro: video?.intro?.text, - previewUrl: video?.video_resource.preview_animation.original_url, + previewUrl: video.thumbnail.original_url || video.thumbnail.url, }); export { getRootUrl, X_UA, appDetail, imagePost, topicPost, videoPost }; diff --git a/lib/routes/tass/namespace.ts b/lib/routes/tass/namespace.ts index 0acbce62448a6f..2cdfbc5a5ec39d 100644 --- a/lib/routes/tass/namespace.ts +++ b/lib/routes/tass/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Russian News Agency TASS', url: 'tass.com', + lang: 'en', }; diff --git a/lib/routes/tass/news.ts b/lib/routes/tass/news.ts index fd5f6088e894aa..c9f472ead76e12 100644 --- a/lib/routes/tass/news.ts +++ b/lib/routes/tass/news.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| Russian Politics & Diplomacy | World | Business & Economy | Military & Defense | Science & Space | Emergencies | Society & Culture | Press Review | Sports | - | ---------------------------- | ----- | ------------------ | ------------------ | --------------- | ----------- | ----------------- | ------------ | ------ | - | politics | world | economy | defense | science | emergencies | society | pressreview | sports |`, +| ---------------------------- | ----- | ------------------ | ------------------ | --------------- | ----------- | ----------------- | ------------ | ------ | +| politics | world | economy | defense | science | emergencies | society | pressreview | sports |`, }; async function handler(ctx) { diff --git a/lib/routes/techcrunch/namespace.ts b/lib/routes/techcrunch/namespace.ts index 0558c9f0ddff65..0c7f66beff35e7 100644 --- a/lib/routes/techcrunch/namespace.ts +++ b/lib/routes/techcrunch/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'TechCrunch', url: 'techcrunch.com', + lang: 'en', }; diff --git a/lib/routes/techcrunch/news.ts b/lib/routes/techcrunch/news.ts index 09ff9ab9031b85..604c74d61eab99 100644 --- a/lib/routes/techcrunch/news.ts +++ b/lib/routes/techcrunch/news.ts @@ -1,8 +1,12 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; -import parser from '@/utils/rss-parser'; 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 { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + const host = 'https://techcrunch.com'; export const route: Route = { path: '/news', @@ -29,33 +33,20 @@ export const route: Route = { }; async function handler() { - const rssUrl = `${host}/feed/`; - const feed = await parser.parseURL(rssUrl); - const items = await Promise.all( - feed.items.map((item) => - cache.tryGet(item.link, async () => { - const url = item.link; - const response = await got({ - url, - method: 'get', - }); - const html = response.data; - const $ = load(html); - const description = $('#root'); - description.find('.article__title').remove(); - description.find('.article__byline__meta').remove(); - description.find('.mobile-header-nav').remove(); - description.find('.desktop-nav').remove(); - return { - title: item.title, - pubDate: item.pubDate, - link: item.link, - category: item.categories, - description: description.html(), - }; - }) - ) - ); + const { data } = await got(`${host}/wp-json/wp/v2/posts`); + const items = data.map((item) => { + const head = item.yoast_head_json; + const $ = load(item.content.rendered, null, false); + return { + title: item.title.rendered, + description: art(path.join(__dirname, 'templates/description.art'), { + head, + rendered: $.html(), + }), + link: item.link, + pubDate: parseDate(item.date_gmt), + }; + }); return { title: 'TechCrunch', diff --git a/lib/routes/techcrunch/templates/description.art b/lib/routes/techcrunch/templates/description.art new file mode 100644 index 00000000000000..3cb00899e9786e --- /dev/null +++ b/lib/routes/techcrunch/templates/description.art @@ -0,0 +1,7 @@ +{{ if head.og_image }} +{{ each head.og_image img }} +<img src="{{ img.url.split('?')[0] }}"> +{{ /each }} +<br> +{{ /if }} +{{@ rendered }} diff --git a/lib/routes/techflowpost/express.ts b/lib/routes/techflowpost/express.ts index 029f888bd952da..9842c94e91ce51 100644 --- a/lib/routes/techflowpost/express.ts +++ b/lib/routes/techflowpost/express.ts @@ -1,22 +1,14 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; import dayjs from 'dayjs'; export const route: Route = { - path: ['/express', '/newsflash'], - categories: ['finance'], + path: '/express', + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/techflowpost/express', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, radar: [ { source: ['techflowpost.com/newsletter/index.html'], @@ -26,7 +18,6 @@ export const route: Route = { maintainers: ['nczitzk'], handler, url: 'techflowpost.com/', - url: 'techflowpost.com/newsletter/index.html', }; async function handler(ctx) { diff --git a/lib/routes/techflowpost/index.ts b/lib/routes/techflowpost/index.ts index 78b728bbe6cd61..f9ce49c0b21c75 100644 --- a/lib/routes/techflowpost/index.ts +++ b/lib/routes/techflowpost/index.ts @@ -1,17 +1,19 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/', + example: '/techflowpost', radar: [ { source: ['techflowpost.com/'], - target: '', }, ], - name: 'Unknown', + name: '首页', + categories: ['finance', 'popular'], + view: ViewType.Articles, maintainers: ['nczitzk'], handler, url: 'techflowpost.com/', diff --git a/lib/routes/techflowpost/namespace.ts b/lib/routes/techflowpost/namespace.ts index 35a074606e1936..39a5ecb89757a6 100644 --- a/lib/routes/techflowpost/namespace.ts +++ b/lib/routes/techflowpost/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '深潮 TechFlow', url: 'techflowpost.com', + lang: 'zh-CN', }; diff --git a/lib/routes/techpowerup/namespace.ts b/lib/routes/techpowerup/namespace.ts index d520cf3ae5c7fc..3e0aaffad712c6 100644 --- a/lib/routes/techpowerup/namespace.ts +++ b/lib/routes/techpowerup/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'TechPowerUp', url: 'techpowerup.com', + lang: 'en', }; diff --git a/lib/routes/techsir/index.ts b/lib/routes/techsir/index.ts new file mode 100644 index 00000000000000..a2ceb44ac128b5 --- /dev/null +++ b/lib/routes/techsir/index.ts @@ -0,0 +1,67 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/', + categories: ['new-media'], + example: '/techsir', + url: 'www.techsir.com', + name: '最新资讯', + maintainers: ['p3psi-boo'], + handler, +}; + +async function handler() { + const baseUrl = 'https://techsir.com'; + + const response = await got({ + method: 'get', + url: baseUrl, + }); + + const $ = load(response.data); + + const alist = $( + '#kt_wrapper > div.main-content-area > div.container.container-fluid > div:nth-child(1) > div.col-xs-12.col-sm-6.col-md-8.post-listing > div.row.flex-row-fluid > div:nth-child(2) > div > div > div.card-body.pt-2 > div.d-flex' + ); + + const list = alist.toArray().map((item) => { + const $item = $(item); + + const path = $item.find('a').attr('href'); + const link = `${baseUrl}${path}`; + const title = $item.find('a').text(); + + return { + title, + link, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await got({ + method: 'get', + url: item.link, + }); + + const $ = load(response.data); + item.description = $('.kg-card-markdown').html(); + item.pubDate = parseDate($('time.time').text()); + item.author = $('a.author').text(); + return item; + }) + ) + ); + + return { + title: 'TechSir - 最新资讯', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/techsir/namespace.ts b/lib/routes/techsir/namespace.ts new file mode 100644 index 00000000000000..b24fb4977c2050 --- /dev/null +++ b/lib/routes/techsir/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'TechSir', + url: 'techsir.com', + description: '科技先生 TechSir.Com 是新酷科技创新与创业媒体', + lang: 'zh-CN', +}; diff --git a/lib/routes/telecompaper/namespace.ts b/lib/routes/telecompaper/namespace.ts index 8712b73ad7c803..8bc0b379344ec5 100644 --- a/lib/routes/telecompaper/namespace.ts +++ b/lib/routes/telecompaper/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Telecompaper', url: 'telecompaper.com', + lang: 'en', }; diff --git a/lib/routes/telecompaper/news.ts b/lib/routes/telecompaper/news.ts index ba7af4a4d5ef23..6a78f622d3abe1 100644 --- a/lib/routes/telecompaper/news.ts +++ b/lib/routes/telecompaper/news.ts @@ -3,7 +3,7 @@ import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; import tough from 'tough-cookie'; -// eslint-disable-next-line n/no-extraneous-require + import FormData from 'form-data'; export const route: Route = { @@ -29,15 +29,15 @@ export const route: Route = { handler, description: `Category - | WIRELESS | BROADBAND | VIDEO | GENERAL | IT | INDUSTRY RESOURCES | - | -------- | --------- | --------- | ------- | -- | ------------------ | - | mobile | internet | boardcast | general | it | industry-resources | +| WIRELESS | BROADBAND | VIDEO | GENERAL | IT | INDUSTRY RESOURCES | +| -------- | --------- | --------- | ------- | -- | ------------------ | +| mobile | internet | boardcast | general | it | industry-resources | - :::tip +::: tip If \`country\` or \`type\` includes empty space, use \`-\` instead. For example, \`United States\` needs to be replaced with \`United-States\`, \`White paper\` needs to be replaced with \`White-paper\` Filters in [INDUSTRY RESOURCES](https://www.telecompaper.com/industry-resources) only provides \`Content Type\` which corresponds to \`type\`. \`year\` and \`country\` are not supported. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/telecompaper/search.ts b/lib/routes/telecompaper/search.ts index 605ac8ebbff660..54a49caaab4958 100644 --- a/lib/routes/telecompaper/search.ts +++ b/lib/routes/telecompaper/search.ts @@ -21,15 +21,15 @@ export const route: Route = { handler, description: `Sorting - | Date Ascending | Date Descending | - | -------------- | --------------- | - | 1 | 2 | +| Date Ascending | Date Descending | +| -------------- | --------------- | +| 1 | 2 | Date selection - | 1 month | 3 months | 6 months | 12 months | 24 months | - | ------- | -------- | -------- | --------- | --------- | - | 1 | 3 | 6 | 12 | 24 |`, +| 1 month | 3 months | 6 months | 12 months | 24 months | +| ------- | -------- | -------- | --------- | --------- | +| 1 | 3 | 6 | 12 | 24 |`, }; async function handler(ctx) { diff --git a/lib/routes/telegram/blog.ts b/lib/routes/telegram/blog.ts index 661caed28ce837..dce3188af317ef 100644 --- a/lib/routes/telegram/blog.ts +++ b/lib/routes/telegram/blog.ts @@ -1,12 +1,13 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import * as cheerio from 'cheerio'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/blog', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.Articles, example: '/telegram/blog', parameters: {}, features: { @@ -31,8 +32,8 @@ export const route: Route = { async function handler() { const link = 'https://telegram.org/blog'; - const res = await got(link); - const $$ = cheerio.load(res.body); + const res = await ofetch(link); + const $$ = cheerio.load(res); const items = await Promise.all( $$('.dev_blog_card_link_wrap') @@ -41,8 +42,8 @@ async function handler() { const $ = $$(each); const link = 'https://telegram.org' + $.attr('href'); return cache.tryGet(link, async () => { - const result = await got(link); - const $ = cheerio.load(result.body); + const result = await ofetch(link); + const $ = cheerio.load(result); return { title: $('#dev_page_title').text(), link, diff --git a/lib/routes/telegram/channel.ts b/lib/routes/telegram/channel.ts index dd4d82520c3d7f..0ae5be27e45d1f 100644 --- a/lib/routes/telegram/channel.ts +++ b/lib/routes/telegram/channel.ts @@ -1,10 +1,8 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; -const __dirname = getCurrentPath(import.meta.url); - import cache from '@/utils/cache'; import { config } from '@/config'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; @@ -13,6 +11,8 @@ import querystring from 'querystring'; import { fallback, queryToBoolean } from '@/utils/readable-social'; import tglibchannel from './tglib/channel'; +const __dirname = getCurrentPath(import.meta.url); + /* message types */ const REPLY = 'REPLY'; const FORWARDED = 'FORWARDED'; @@ -57,11 +57,78 @@ const mediaTagDict = { export const route: Route = { path: '/channel/:username/:routeParams?', - categories: ['social-media'], - example: '/telegram/channel/awesomeDIYgod/searchQuery=twitter', - parameters: { username: 'channel username', routeParams: 'extra parameters, see the table below' }, + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, + example: '/telegram/channel/awesomeRSSHub', + parameters: { + username: 'channel username', + routeParams: `extra parameters, see the table below +| Key | Description | Accepts | Defaults to | +| :--------------------: | :-------------------------------------------------------------------: | :------------------------------------------------: | :----------: | +| showLinkPreview | Show the link preview from Telegram | 0/1/true/false | true | +| showViaBot | For messages sent via bot, show the bot | 0/1/true/false | true | +| showReplyTo | For reply messages, show the target of the reply | 0/1/true/false | true | +| showFwdFrom | For forwarded messages, show the forwarding source | 0/1/true/false | true | +| showFwdFromAuthor | For forwarded messages, show the author of the forwarding source | 0/1/true/false | true | +| showInlineButtons | Show inline buttons | 0/1/true/false | false | +| showMediaTagInTitle | Show media tags in the title | 0/1/true/false | true | +| showMediaTagAsEmoji | Show media tags as emoji | 0/1/true/false | true | +| showHashtagAsHyperlink | Show hashtags as hyperlinks (\`https://t.me/s/channel?q=%23hashtag\`) | 0/1/true/false | true | +| includeFwd | Include forwarded messages | 0/1/true/false | true | +| includeReply | Include reply messages | 0/1/true/false | true | +| includeServiceMsg | Include service messages (e.g. message pinned, channel photo updated) | 0/1/true/false | true | +| includeUnsupportedMsg | Include messages unsupported by t.me | 0/1/true/false | false | +| searchQuery | search query | keywords; replace \`#hashtag\` with \`%23hashtag\` | (no keyword) | + +Specify different option values than default values can meet different needs, URL + +\`\`\` +https://rsshub.app/telegram/channel/NewlearnerChannel/showLinkPreview=0&showViaBot=0&showReplyTo=0&showFwdFrom=0&showFwdFromAuthor=0&showInlineButtons=0&showMediaTagInTitle=1&showMediaTagAsEmoji=1&includeFwd=0&includeReply=1&includeServiceMsg=0&includeUnsupportedMsg=0 +\`\`\` + +generates an RSS without any link previews and annoying metadata, with emoji media tags in the title, without forwarded messages (but with reply messages), and without messages you don't care about (service messages and unsupported messages), for people who prefer pure subscriptions. + +For backward compatibility reasons, invalid \`routeParams\` will be treated as \`searchQuery\` . +`, + }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'TELEGRAM_SESSION', + optional: true, + description: 'Telegram API Authentication', + }, + { + name: 'TELEGRAM_API_ID', + optional: true, + description: 'Telegram API ID', + }, + { + name: 'TELEGRAM_API_HASH', + optional: true, + description: 'Telegram API Hash', + }, + { + name: 'TELEGRAM_MAX_CONCURRENT_DOWNLOADS', + optional: true, + description: 'Telegram Max Concurrent Downloads', + }, + { + name: 'TELEGRAM_PROXY_HOST', + optional: true, + description: 'Telegram Proxy Host', + }, + { + name: 'TELEGRAM_PROXY_PORT', + optional: true, + description: 'Telegram Proxy Port', + }, + { + name: 'TELEGRAM_PROXY_SECRET', + optional: true, + description: 'Telegram Proxy Secret', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -75,45 +142,15 @@ export const route: Route = { }, ], name: 'Channel', - maintainers: ['DIYgod', 'Rongronggg9'], + maintainers: ['DIYgod', 'Rongronggg9', 'synchrone', 'pseudoyu'], handler, - description: `| Key | Description | Accepts | Defaults to | - | --------------------- | --------------------------------------------------------------------- | ---------------------------------------------------- | ----------------- | - | showLinkPreview | Show the link preview from Telegram | 0/1/true/false | true | - | showViaBot | For messages sent via bot, show the bot | 0/1/true/false | true | - | showReplyTo | For reply messages, show the target of the reply | 0/1/true/false | true | - | showFwdFrom | For forwarded messages, show the forwarding source | 0/1/true/false | true | - | showFwdFromAuthor | For forwarded messages, show the author of the forwarding source | 0/1/true/false | true | - | showInlineButtons | Show inline buttons | 0/1/true/false | false | - | showMediaTagInTitle | Show media tags in the title | 0/1/true/false | true | - | showMediaTagAsEmoji | Show media tags as emoji | 0/1/true/false | true | - | includeFwd | Include forwarded messages | 0/1/true/false | true | - | includeReply | Include reply messages | 0/1/true/false | true | - | includeServiceMsg | Include service messages (e.g. message pinned, channel photo updated) | 0/1/true/false | true | - | includeUnsupportedMsg | Include messages unsupported by t.me | 0/1/true/false | false | - | searchQuery | search query | keywords; replace \`#\` by \`%23\` for hashtag searching | (search disabled) | - - Specify different option values than default values can meet different needs, URL - - \`\`\` - https://rsshub.app/telegram/channel/NewlearnerChannel/showLinkPreview=0&showViaBot=0&showReplyTo=0&showFwdFrom=0&showFwdFromAuthor=0&showInlineButtons=0&showMediaTagInTitle=1&showMediaTagAsEmoji=1&includeFwd=0&includeReply=1&includeServiceMsg=0&includeUnsupportedMsg=0 - \`\`\` - - generates an RSS without any link previews and annoying metadata, with emoji media tags in the title, without forwarded messages (but with reply messages), and without messages you don't care about (service messages and unsupported messages), for people who prefer pure subscriptions. - - :::tip - For backward compatibility reasons, invalid \`routeParams\` will be treated as \`searchQuery\` . - - Due to Telegram restrictions, some channels involving pornography, copyright, and politics cannot be subscribed. You can confirm by visiting \`https://t.me/s/:username\`. - :::`, + description: ` +::: tip + Due to Telegram restrictions, some channels involving pornography, copyright, and politics cannot be subscribed. You can confirm by visiting \`https://t.me/s/:username\`, it's recommended to deploy your own instance with telegram api configs (create your telegram application via \`https://core.telegram.org/api/obtaining_api_id\`, run this command \`node ./lib/routes/telegram/scripts/get-telegram-session.mjs\` to get \`TELEGRAM_SESSION\` and set it as Environment Variable). +:::`, }; async function handler(ctx) { - const useWeb = ctx.req.param('routeParams') || !config.telegram.session; - if (!useWeb) { - return tglibchannel(ctx); - } - const username = ctx.req.param('username'); let routeParams = ctx.req.param('routeParams'); let showLinkPreview = true; @@ -124,12 +161,13 @@ async function handler(ctx) { let showInlineButtons = false; let showMediaTagInTitle = true; let showMediaTagAsEmoji = true; + let showHashtagAsHyperlink = true; let includeFwd = true; let includeReply = true; let includeServiceMsg = true; let includeUnsupportedMsg = false; let searchQuery = routeParams; // for backward compatibility - if (routeParams && routeParams.search(/(^|&)(show(LinkPreview|ViaBot|ReplyTo|FwdFrom(Author)?|InlineButtons|MediaTag(InTitle|AsEmoji))|include(Fwd|Reply|(Service|Unsupported)Msg)|searchQuery)=/) !== -1) { + if (routeParams && routeParams.search(/(^|&)(show(LinkPreview|ViaBot|ReplyTo|FwdFrom(Author)?|InlineButtons|MediaTag(InTitle|AsEmoji)|HashtagAsHyperlink)|include(Fwd|Reply|(Service|Unsupported)Msg)|searchQuery)=/) !== -1) { routeParams = querystring.parse(ctx.req.param('routeParams')); showLinkPreview = !!fallback(undefined, queryToBoolean(routeParams.showLinkPreview), showLinkPreview); showViaBot = !!fallback(undefined, queryToBoolean(routeParams.showViaBot), showViaBot); @@ -139,6 +177,7 @@ async function handler(ctx) { showInlineButtons = !!fallback(undefined, queryToBoolean(routeParams.showInlineButtons), showInlineButtons); showMediaTagInTitle = !!fallback(undefined, queryToBoolean(routeParams.showMediaTagInTitle), showMediaTagInTitle); showMediaTagAsEmoji = !!fallback(undefined, queryToBoolean(routeParams.showMediaTagAsEmoji), showMediaTagAsEmoji); + showHashtagAsHyperlink = !!fallback(undefined, queryToBoolean(routeParams.showHashtagAsHyperlink), showHashtagAsHyperlink); includeFwd = !!fallback(undefined, queryToBoolean(routeParams.includeFwd), includeFwd); includeReply = !!fallback(undefined, queryToBoolean(routeParams.includeReply), includeReply); includeServiceMsg = !!fallback(undefined, queryToBoolean(routeParams.includeServiceMsg), includeServiceMsg); @@ -146,19 +185,20 @@ async function handler(ctx) { searchQuery = fallback(undefined, routeParams.searchQuery, null); } + // some channels are not available in t.me/s/, fallback to use Telegram api const resourceUrl = searchQuery ? `https://t.me/s/${username}?q=${encodeURIComponent(searchQuery)}` : `https://t.me/s/${username}`; const data = await cache.tryGet( resourceUrl, async () => { - const _r = await got(resourceUrl); - return _r.data; + const _r = await ofetch(resourceUrl); + return _r; }, config.cache.routeExpire, false ); - const $ = load(data); + const $ = load(data as string); /* * Since 2024/4/20, t.me/s/ mistakenly have every '&' in **hyperlinks** replaced by '&'. @@ -171,11 +211,21 @@ async function handler(ctx) { href && $elem.attr('href', href.replaceAll('&', '&')); }); + !showHashtagAsHyperlink && + $('a[href^="?q=%23"]').each((_, elem) => { + const $elem = $(elem); + $elem.replaceWith($elem.text()); + }); + const list = includeServiceMsg ? $('.tgme_widget_message_wrap:not(.tgme_widget_message_wrap:has(.tme_no_messages_found))') // exclude 'no posts found' messages : $('.tgme_widget_message_wrap:not(.tgme_widget_message_wrap:has(.service_message,.tme_no_messages_found))'); // also exclude service messages if (list.length === 0 && $('.tgme_channel_history').length === 0) { + if (config.telegram.session) { + return tglibchannel(ctx); + } + throw new Error(`Unable to fetch message feed from this channel. Please check this URL to see if you can view the message preview: ${resourceUrl}`); } @@ -414,7 +464,40 @@ async function handler(ctx) { const background = $node.css('background-image'); const backgroundUrl = background && background.match(/url\('(.*)'\)/); const backgroundUrlSrc = backgroundUrl && backgroundUrl[1]; - tag_media += backgroundUrlSrc ? `<img src="${backgroundUrlSrc}">` : ''; + const attrs = [`src="${backgroundUrlSrc}"`]; + /* + * If the width is not in px, it is either a percentage (Link Preview/Instant view) + * or absent (ditto). + * Only accept px to prevent images from being invisible or too small. + */ + let width = 0; + const widthStr = $node.css('width'); + if (widthStr && widthStr.endsWith('px')) { + width = Number.parseFloat(widthStr); + } + /* + * Height is present when the message is an album but does not exist in other cases. + * Ditto, only accept px. + * !!!NOTE: images in albums may have smaller width and height. + */ + let height = 0; + const heightStr = $node.css('height'); + if (heightStr && heightStr.endsWith('px')) { + height = Number.parseFloat(heightStr); + } + /* + * Only calculate height when needed. + * The aspect ratio is either a percentage (single image) or absent (Link Preview). + * Only accept percentage to prevent images from being invisible or distorted. + */ + const aspectRatioStr = $node.find('.tgme_widget_message_photo').css('padding-top'); + if (height <= 0 && width > 0 && aspectRatioStr && aspectRatioStr.endsWith('%')) { + height = (Number.parseFloat(aspectRatioStr) / 100) * width; + } + // Only set width/height when >32 to avoid invisible images. + width > 32 && attrs.push(`width="${width}"`); + height > 32 && attrs.push(`height="${height.toFixed(2).replace('.00', '')}"`); + tag_media += backgroundUrlSrc ? `<img ${attrs.join(' ')}>` : ''; } if (tag_media) { tag_media_all += tag_media; @@ -436,7 +519,7 @@ async function handler(ctx) { const mapBackground = locationObj.find('.tgme_widget_message_location').css('background-image'); const mapBackgroundUrl = mapBackground && mapBackground.match(/url\('(.*)'\)/); const mapBackgroundUrlSrc = mapBackgroundUrl && mapBackgroundUrl[1]; - const mapImgHtml = mapBackgroundUrlSrc ? `<img src="${mapBackgroundUrlSrc}">` : showMediaTagAsEmoji ? mediaTagDict[LOCATION][1] : mediaTagDict[LOCATION][0]; + const mapImgHtml = mapBackgroundUrlSrc ? `<img src="${mapBackgroundUrlSrc}">` : (showMediaTagAsEmoji ? mediaTagDict[LOCATION][1] : mediaTagDict[LOCATION][0]); return locationLink ? `<a href="${locationLink}">${mapImgHtml}</a>` : mapImgHtml; } else { return ''; diff --git a/lib/routes/telegram/namespace.ts b/lib/routes/telegram/namespace.ts index 34414965af6287..191361e63e56a8 100644 --- a/lib/routes/telegram/namespace.ts +++ b/lib/routes/telegram/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Telegram', url: 't.me', + lang: 'en', }; diff --git a/lib/routes/telegram/scripts/get-telegram-session.mjs b/lib/routes/telegram/scripts/get-telegram-session.mjs new file mode 100644 index 00000000000000..9d1a830c14ce83 --- /dev/null +++ b/lib/routes/telegram/scripts/get-telegram-session.mjs @@ -0,0 +1,51 @@ +import { TelegramClient } from 'telegram'; +import { StringSession } from 'telegram/sessions/index.js'; +import readline from 'readline'; +import winston from 'winston'; + +function userInput(question) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer); + }); + }); +} + +const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.printf(({ level, message, timestamp }) => `${timestamp} ${level}: ${message}`) + ), + transports: [new winston.transports.Console()], +}); + +async function getSessionString() { + const apiId = Number.parseInt(await userInput('Please enter your API ID: ')); + const apiHash = await userInput('Please enter your API Hash: '); + const stringSession = new StringSession(''); + const client = new TelegramClient(stringSession, apiId, apiHash, { + connectionRetries: 5, + }); + await client.start({ + phoneNumber: async () => await userInput('Please enter your phone number: '), + password: async () => await userInput('Please enter your password: '), + phoneCode: async () => await userInput('Please enter the code you received: '), + onError: (err) => logger.error(err), + }); + + logger.info('You are now connected.'); + const sessionString = client.session.save(); + logger.info(`Your session string is: ${sessionString}`); + + await client.disconnect(); + return sessionString; +} + +// Run the function +getSessionString().catch((error) => logger.error(error)); diff --git a/lib/routes/telegram/stickerpack.ts b/lib/routes/telegram/stickerpack.ts index cd5822117638ac..96021960c7404e 100644 --- a/lib/routes/telegram/stickerpack.ts +++ b/lib/routes/telegram/stickerpack.ts @@ -1,11 +1,13 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; +import { Route, ViewType } from '@/types'; +import ofetch from '@/utils/ofetch'; import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; +import cache from '@/utils/cache'; export const route: Route = { path: '/stickerpack/:name', categories: ['social-media'], + view: ViewType.Pictures, example: '/telegram/stickerpack/DIYgod', parameters: { name: 'Sticker Pack name, available in the sharing URL' }, features: { @@ -26,21 +28,28 @@ async function handler(ctx) { throw new ConfigNotFoundError('Telegram Sticker Pack RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>'); } const name = ctx.req.param('name'); + const token = config.telegram.token; + const response = await ofetch(`https://api.telegram.org/bot${token}/getStickerSet?name=${name}`); - const response = await got({ - method: 'get', - url: `https://api.telegram.org/bot${config.telegram.token}/getStickerSet?name=${name}`, - }); + const list = response.result.stickers.map((item) => ({ + title: item.emoji, + description: item.file_id, + guid: item.file_id, + })); - const data = response.data.result; + const items = await Promise.all( + list.map((item) => + cache.tryGet(`telegram:stickerpack:${item.guid}`, async () => { + const response = await ofetch(`https://api.telegram.org/bot${token}/getFile?file_id=${item.guid}`); + item.description = `<img src="https://api.telegram.org/file/bot${token}/${response.result.file_path}" />`; + return item; + }) + ) + ); return { - title: `${data.title} - Telegram Sticker Pack`, + title: `${response.result.title} - Telegram Sticker Pack`, link: `https://t.me/addstickers/${name}`, - item: data.stickers.map((item) => ({ - title: item.emoji, - description: item.file_id, - guid: item.file_id, - })), + item: items, }; } diff --git a/lib/routes/telegram/tglib/channel.ts b/lib/routes/telegram/tglib/channel.ts index 73864bf364b6f0..ef8f91fe5c379e 100644 --- a/lib/routes/telegram/tglib/channel.ts +++ b/lib/routes/telegram/tglib/channel.ts @@ -2,6 +2,8 @@ import InvalidParameterError from '@/errors/types/invalid-parameter'; import { client, decodeMedia, getClient, getFilename, getMediaLink, streamDocument, streamThumbnail } from './client'; import { returnBigInt as bigInt } from 'telegram/Helpers'; import { HTMLParser } from 'telegram/extensions/html'; +import { DataItem } from '@/types'; +import type { Api } from 'telegram'; function parseRange(range, length) { if (!range) { @@ -111,20 +113,25 @@ async function getMedia(ctx) { } export default async function handler(ctx) { + const { username } = ctx.req.param(); const client = await getClient(); - const item = []; - const chat = await client.getInputEntity(ctx.req.param('username')); + const item: DataItem[] = []; + const chat = (await client.getInputEntity(username)) as Api.InputPeerChannel; const channelInfo = await client.getEntity(chat); - let attachments = []; + if (channelInfo.className !== 'Channel') { + throw new Error(`${username} is not a channel`); + } + + let attachments: string[] = []; const messages = await client.getMessages(chat, { limit: 50 }); for (const message of messages) { if (message.media) { // messages that have no text are shown as if they're one post // because in TG only 1 attachment per message is possible - attachments.push(getMediaLink(ctx, chat, ctx.req.param('username'), message)); + attachments.push(getMediaLink(ctx, chat, username, message)); } if (message.text !== '') { let description = attachments.join('\n'); @@ -140,8 +147,8 @@ export default async function handler(ctx) { title, description, pubDate: new Date(message.date * 1000).toUTCString(), - link: `https://t.me/s/${channelInfo.username}/${message.id}`, - author: `${channelInfo.title} (@${channelInfo.username})`, + link: `https://t.me/s/${username}/${message.id}`, + author: `${channelInfo.title} (@${username})`, }); } } @@ -149,10 +156,10 @@ export default async function handler(ctx) { return { title: channelInfo.title, language: null, - link: `https://t.me/${channelInfo.username}`, + link: `https://t.me/${username}`, item, allowEmpty: ctx.req.param('id') === 'allow_empty', - description: `@${channelInfo.username} on Telegram`, + description: `@${username} on Telegram`, }; } diff --git a/lib/routes/telegram/tglib/client.ts b/lib/routes/telegram/tglib/client.ts index 855c5f887d01db..1336f3b075096a 100644 --- a/lib/routes/telegram/tglib/client.ts +++ b/lib/routes/telegram/tglib/client.ts @@ -23,14 +23,18 @@ export async function getClient(authParams?: UserAuthParams, session?: string) { autoReconnect: true, retryDelay: 3000, maxConcurrentDownloads: Number(config.telegram.maxConcurrentDownloads ?? 10), + proxy: + config.telegram.proxy?.host && config.telegram.proxy.port && config.telegram.proxy.secret + ? { + ip: config.telegram.proxy.host, + port: Number(config.telegram.proxy.port), + MTProxy: true, + secret: config.telegram.proxy.secret, + } + : undefined, }); - await client.start( - Object.assign(authParams ?? {}, { - onError: (err) => { - throw new Error('Cannot start TG: ' + err); - }, - }) as any - ); + + await client.connect(); return client; } @@ -49,25 +53,25 @@ function ExpandInlineBytes(bytes) { return []; } const header = Buffer.from([ - 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x28, 0x1c, 0x1e, 0x23, 0x1e, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2d, 0x2b, - 0x28, 0x30, 0x3c, 0x64, 0x41, 0x3c, 0x37, 0x37, 0x3c, 0x7b, 0x58, 0x5d, 0x49, 0x64, 0x91, 0x80, 0x99, 0x96, 0x8f, 0x80, 0x8c, 0x8a, 0xa0, 0xb4, 0xe6, 0xc3, 0xa0, 0xaa, 0xda, 0xad, 0x8a, 0x8c, 0xc8, 0xff, 0xcb, 0xda, 0xee, - 0xf5, 0xff, 0xff, 0xff, 0x9b, 0xc1, 0xff, 0xff, 0xff, 0xfa, 0xff, 0xe6, 0xfd, 0xff, 0xf8, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x2b, 0x2d, 0x2d, 0x3c, 0x35, 0x3c, 0x76, 0x41, 0x41, 0x76, 0xf8, 0xa5, 0x8c, 0xa5, 0xf8, 0xf8, 0xf8, - 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, - 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, - 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, - 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, - 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, - 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, - 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, - 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, - 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, - 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, - 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, - 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x28, 0x1C, 0x1E, 0x23, 0x1E, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2D, 0x2B, + 0x28, 0x30, 0x3C, 0x64, 0x41, 0x3C, 0x37, 0x37, 0x3C, 0x7B, 0x58, 0x5D, 0x49, 0x64, 0x91, 0x80, 0x99, 0x96, 0x8F, 0x80, 0x8C, 0x8A, 0xA0, 0xB4, 0xE6, 0xC3, 0xA0, 0xAA, 0xDA, 0xAD, 0x8A, 0x8C, 0xC8, 0xFF, 0xCB, 0xDA, 0xEE, + 0xF5, 0xFF, 0xFF, 0xFF, 0x9B, 0xC1, 0xFF, 0xFF, 0xFF, 0xFA, 0xFF, 0xE6, 0xFD, 0xFF, 0xF8, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x2B, 0x2D, 0x2D, 0x3C, 0x35, 0x3C, 0x76, 0x41, 0x41, 0x76, 0xF8, 0xA5, 0x8C, 0xA5, 0xF8, 0xF8, 0xF8, + 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, + 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, + 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, + 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, + 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, + 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0, 0x15, 0x62, + 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, + 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, + 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE2, + 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3F, 0x00, ]); - const footer = Buffer.from([0xff, 0xd9]); + const footer = Buffer.from([0xFF, 0xD9]); const real = Buffer.alloc(header.length + bytes.length + footer.length); header.copy(real); bytes.copy(real, header.length, 3); @@ -77,7 +81,7 @@ function ExpandInlineBytes(bytes) { return real; } -function getMediaLink(ctx, channel, channelName, message) { +function getMediaLink(ctx, channel: Api.InputPeerChannel, channelName: string, message: Api.Message) { const base = `${ctx.protocol}://${ctx.host}/telegram/channel/${channelName}`; const src = base + `${channel.channelId}_${message.id}`; @@ -98,7 +102,7 @@ function getMediaLink(ctx, channel, channelName, message) { linkText += ` (${humanFileSize(x.document.size)})`; return `<a href="${src}" target="_blank"><img src="${src}?thumb" alt=""/><br/>${linkText}</a>`; } - return; + return ''; } function getFilename(x) { if (x instanceof Api.MessageMediaDocument) { diff --git a/lib/routes/tencent/cloud/developer/column.ts b/lib/routes/tencent/cloud/developer/column.ts new file mode 100644 index 00000000000000..8cf0cbd202d3ba --- /dev/null +++ b/lib/routes/tencent/cloud/developer/column.ts @@ -0,0 +1,81 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +const PAGE = 1; +const PAGE_SIZE = 20; + +export const route: Route = { + path: '/cloud/developer/column/:categoryId?', + categories: ['programming'], + example: '/tencent/cloud/developer/column/1', + parameters: { categoryId: 'categoryId from page url' }, + radar: [ + { + source: ['cloud.tencent.com/developer/column'], + }, + ], + name: '腾讯云开发者社区专栏', + maintainers: ['lyling'], + handler: async (ctx) => { + const categoryId = ctx.req.param('categoryId') ?? 0; + const link = `https://cloud.tencent.com/developer/api/home/article-list`; + const response = await ofetch(link, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: { + classifyId: categoryId, + page: PAGE, + pagesize: PAGE_SIZE, + type: '', + }, + }); + + const items = response.list.map((item) => ({ + // 文章标题 + title: item.title, + // 文章链接 + link: `https://cloud.tencent.com/developer/article/${item.articleId}`, + // 文章正文 + description: item.summary, + // 文章发布日期 + pubDate: parseDate(item.createTime * 1000), + // 如果有的话,文章作者 + author: item.author.nickname, + // 如果有的话,文章分类 + category: item.tags.map((tag) => tag.tagName), + })); + + const classify = await findClassifyById(categoryId); + + const title = classify ? classify.name : ''; + const description = `${title} - 腾讯云开发者社区`; + + return { + title, + description, + item: items, + }; + }, +}; + +async function findClassifyById(id) { + const classifylink = 'https://cloud.tencent.com/developer/api/column/get-classify-list-by-scene'; + const response = await cache.tryGet(classifylink, async () => { + const response = await ofetch(classifylink, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: { + scene: 0, + }, + }); + return response; + }); + + return response.list.find((classify) => classify.id === Number(id)); +} diff --git a/lib/routes/tencent/namespace.ts b/lib/routes/tencent/namespace.ts index 00130d612f5e3f..17b78150841b3a 100644 --- a/lib/routes/tencent/namespace.ts +++ b/lib/routes/tencent/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: '王者荣耀', - url: 'mp.weixin.qq.com', + name: '腾讯', + url: 'tencent.com', + lang: 'zh-CN', }; diff --git a/lib/routes/tencent/news/author.ts b/lib/routes/tencent/news/author.ts index bb1169b4d673f9..5d1fae1e6ec13f 100644 --- a/lib/routes/tencent/news/author.ts +++ b/lib/routes/tencent/news/author.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, Data } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); @@ -25,17 +25,19 @@ export const route: Route = { }, radar: [ { - source: ['new.qq.com/omn/author/:mid'], + title: '当前作者文章', + source: ['news.qq.com/omn/author/:mid'], }, ], - name: '更新', + name: '作者', maintainers: ['LogicJake', 'miles170'], handler, }; -async function handler(ctx) { +async function handler(ctx): Promise<Data> { const mid = ctx.req.param('mid'); - const homePageInfoUrl = `https://i.news.qq.com/i/getUserHomepageInfo?chlid=${mid}`; + const userType = /^\d+$/.test(mid) ? 'chlid' : 'guestSuid'; + const homePageInfoUrl = `https://i.news.qq.com/i/getUserHomepageInfo?${userType}=${mid}`; const userInfo = await cache.tryGet(homePageInfoUrl, async () => (await got(homePageInfoUrl)).data.userinfo); const title = userInfo.nick; const description = userInfo.user_desc; @@ -52,44 +54,47 @@ async function handler(ctx) { const author = item.source; const abstract = item.abstract; - return item.articletype === '4' - ? { - title, - description: abstract, - link: itemUrl, - author, - pubDate, - } - : cache.tryGet(itemUrl, async () => { - const response = await got(itemUrl); - const $ = load(response.data); - const data = JSON.parse( - $('script:contains("window.DATA")') - .text() - .match(/window\.DATA = ({.+});/)[1] - ); - const $data = load(data.originContent?.text || '', null, false); - if ($data) { - // Not video page - $data('*') - .contents() - .filter((_, elem) => elem.type === 'comment') - .replaceWith((_, elem) => - art(path.join(__dirname, '../templates/news/image.art'), { - attribute: elem.data.trim(), - originAttribute: data.originAttribute, - }) - ); - } + if (item.articletype === '4' || item.articletype === '118') { + // Video + return { + title, + description: `<a href=${item.url}><img src="${item.articletype === '4' ? item.miniProShareImage : item.miniVideoPic}" style="width: 100%"></a>`, + link: itemUrl, + author, + pubDate, + }; + } - return { - title, - description: $data.html() || abstract, - link: itemUrl, - author, - pubDate, - }; - }); + return cache.tryGet(itemUrl, async () => { + const response = await got(itemUrl); + const $ = load(response.data); + const data = JSON.parse( + $('script:contains("window.DATA")') + .text() + .match(/window\.DATA = ({.+});/)[1] + ); + const $data = load(data.originContent?.text || '', null, false); + if ($data) { + // Not video page + $data('*') + .contents() + .filter((_, elem) => elem.type === 'comment') + .replaceWith((_, elem) => + art(path.join(__dirname, '../templates/news/image.art'), { + attribute: elem.data.trim(), + originAttribute: data.originAttribute, + }) + ); + } + + return { + title, + description: $data.html() || abstract, + link: itemUrl, + author, + pubDate, + }; + }); }) ); @@ -98,5 +103,6 @@ async function handler(ctx) { description, link: `https://new.qq.com/omn/author/${mid}`, item: items, + image: userInfo?.shareImg, }; } diff --git a/lib/routes/tencent/pvp/newsindex.ts b/lib/routes/tencent/pvp/newsindex.ts index 41064e33b824df..37870b2fb50810 100644 --- a/lib/routes/tencent/pvp/newsindex.ts +++ b/lib/routes/tencent/pvp/newsindex.ts @@ -54,8 +54,8 @@ export const route: Route = { maintainers: ['Jeason0228', 'HenryQW'], handler, description: `| 全部 | 热门 | 新闻 | 公告 | 活动 | 赛事 | 优化 | - | ---- | ---- | ---- | ---- | ---- | ---- | ---- | - | all | rm | xw | gg | hd | ss | yh |`, +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| all | rm | xw | gg | hd | ss | yh |`, }; async function handler(ctx) { diff --git a/lib/routes/tesla/cx.ts b/lib/routes/tesla/cx.ts index 63e1c83ad2cfbc..e129181f0df3b8 100644 --- a/lib/routes/tesla/cx.ts +++ b/lib/routes/tesla/cx.ts @@ -26,81 +26,81 @@ export const route: Route = { maintainers: ['simonsmh', 'nczitzk'], handler, description: `| 充电免停 | 酒店 | 美食 | 生活方式 | - | -------- | ---- | ---- | -------- | +| -------- | ---- | ---- | -------- | - :::tip +::: tip 分类为 **充电免停** 时,城市参数不起作用 - ::: +::: - <details> - <summary>可选城市</summary> +<details> +<summary>可选城市</summary> - | 成都 | 深圳 | 洛阳 | 北京 | 南京 | 绍兴 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 成都 | 深圳 | 洛阳 | 北京 | 南京 | 绍兴 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 西安 | 上海 | 阿坝藏族羌族自治州 | 重庆 | 郑州 | 天津 | - | ---- | ---- | ------------------ | ---- | ---- | ---- | +| 西安 | 上海 | 阿坝藏族羌族自治州 | 重庆 | 郑州 | 天津 | +| ---- | ---- | ------------------ | ---- | ---- | ---- | - | 晋中 | 三亚 | 湖州 | 苏州 | 扬州 | 秦皇岛 | - | ---- | ---- | ---- | ---- | ---- | ------ | +| 晋中 | 三亚 | 湖州 | 苏州 | 扬州 | 秦皇岛 | +| ---- | ---- | ---- | ---- | ---- | ------ | - | 长沙 | 武汉 | 安阳 | 温州 | 瑞安 | 石家庄 | - | ---- | ---- | ---- | ---- | ---- | ------ | +| 长沙 | 武汉 | 安阳 | 温州 | 瑞安 | 石家庄 | +| ---- | ---- | ---- | ---- | ---- | ------ | - | 佛山 | 广州 | 杭州 | 烟台 | 沧州 | 张家港 | - | ---- | ---- | ---- | ---- | ---- | ------ | +| 佛山 | 广州 | 杭州 | 烟台 | 沧州 | 张家港 | +| ---- | ---- | ---- | ---- | ---- | ------ | - | 金华 | 临沧 | 大理 | 南昌 | 贵阳 | 信阳 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 金华 | 临沧 | 大理 | 南昌 | 贵阳 | 信阳 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 张家口 | 铜仁 | 沈阳 | 合肥 | 黔东 | 高邮 | - | ------ | ---- | ---- | ---- | ---- | ---- | +| 张家口 | 铜仁 | 沈阳 | 合肥 | 黔东 | 高邮 | +| ------ | ---- | ---- | ---- | ---- | ---- | - | 三河 | 安顺 | 莆田 | 阳江 | 南宁 | 台州 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 三河 | 安顺 | 莆田 | 阳江 | 南宁 | 台州 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 余姚 | 淄博 | 三明 | 中山 | 宁波 | 厦门 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 余姚 | 淄博 | 三明 | 中山 | 宁波 | 厦门 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 永康 | 慈溪 | 台山 | 福州 | 无锡 | 宜昌 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 永康 | 慈溪 | 台山 | 福州 | 无锡 | 宜昌 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 泉州 | 肇庆 | 太仓 | 珠海 | 邢台 | 衡水 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 泉州 | 肇庆 | 太仓 | 珠海 | 邢台 | 衡水 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 温岭 | 宜兴 | 东莞 | 威海 | 南通 | 舟山 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 温岭 | 宜兴 | 东莞 | 威海 | 南通 | 舟山 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 都匀 | 长治 | 江阴 | 云浮 | 常州 | 唐山 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 都匀 | 长治 | 江阴 | 云浮 | 常州 | 唐山 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 平湖 | 商丘 | 保定 | 泰州 | 青岛 | 龙口 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 平湖 | 商丘 | 保定 | 泰州 | 青岛 | 龙口 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 泰安 | 岳阳 | 惠州 | 徐州 | 哈尔滨 | 潍坊 | - | ---- | ---- | ---- | ---- | ------ | ---- | +| 泰安 | 岳阳 | 惠州 | 徐州 | 哈尔滨 | 潍坊 | +| ---- | ---- | ---- | ---- | ------ | ---- | - | 大同 | 嘉兴 | 毕节 | 临汾 | 江门 | 诸暨 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 大同 | 嘉兴 | 毕节 | 临汾 | 江门 | 诸暨 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 儋州 | 衢州 | 大连 | 昆山 | 靖江 | 常熟 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 儋州 | 衢州 | 大连 | 昆山 | 靖江 | 常熟 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 罗定 | 丽江 | 晋江 | 乐清 | 茂名 | 福清 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 罗定 | 丽江 | 晋江 | 乐清 | 茂名 | 福清 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 廊坊 | 兰溪 | 汕尾 | 滨州 | 昆明 | 玉环 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 廊坊 | 兰溪 | 汕尾 | 滨州 | 昆明 | 玉环 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 绵阳 | 漳州 | 德州 | 聊城 | 龙岩 | 临沂 | - | ---- | ---- | ---- | ---- | ---- | ---- | +| 绵阳 | 漳州 | 德州 | 聊城 | 龙岩 | 临沂 | +| ---- | ---- | ---- | ---- | ---- | ---- | - | 新沂 | 桐乡 | 迪庆藏族自治州 | 汕头 | 潮州 | 驻马店 | - | ---- | ---- | -------------- | ---- | ---- | ------ | +| 新沂 | 桐乡 | 迪庆藏族自治州 | 汕头 | 潮州 | 驻马店 | +| ---- | ---- | -------------- | ---- | ---- | ------ | - | 曲阜 | 郴州 | 济源 | 兴义 | - | ---- | ---- | ---- | ---- | - </details>`, +| 曲阜 | 郴州 | 济源 | 兴义 | +| ---- | ---- | ---- | ---- | +</details>`, }; async function handler(ctx) { @@ -127,14 +127,14 @@ async function handler(ctx) { }, }); - const categoryObject = categoryResponse.data.filter((c) => c.name === category).pop(); + const categoryObject = categoryResponse.data.findLast((c) => c.name === category); const { data: response } = await got(apiUrl, { searchParams: { pageSize: limit, pageNumber: 0, benefitCategoryId: categoryObject?.id ?? undefined, - category: categoryObject ? undefined : category === '充电免停' ? 2 : undefined, + category: categoryObject ? undefined : (category === '充电免停' ? 2 : undefined), city, }, }); diff --git a/lib/routes/tesla/namespace.ts b/lib/routes/tesla/namespace.ts index 166ec1bcb03b12..8daaaa8242dddb 100644 --- a/lib/routes/tesla/namespace.ts +++ b/lib/routes/tesla/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '特斯拉中国', url: 'tesla.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/test/index.ts b/lib/routes/test/index.ts index 2990917bd7bcd8..1752e2ab9465d4 100644 --- a/lib/routes/test/index.ts +++ b/lib/routes/test/index.ts @@ -11,7 +11,7 @@ let cacheIndex = 0; export const route: Route = { path: '/:id/:params?', - name: 'Unknown', + name: 'Test', maintainers: ['DIYgod', 'NeverBehave'], handler, }; @@ -32,7 +32,12 @@ async function handler(ctx) { if (ctx.req.param('id') === 'invalid-parameter-error') { throw new InvalidParameterError('Test invalid parameter error'); } + if (ctx.req.param('id') === 'redirect') { + ctx.set('redirect', '/test/1'); + return; + } let item: DataItem[] = []; + let image: string | null = null; switch (ctx.req.param('id')) { case 'filter': item = [ @@ -141,6 +146,7 @@ async function handler(ctx) { break; } case 'complicated': + image = 'https://mock.com/DIYgod/RSSHub.png'; item.push( { title: `Complicated Title`, @@ -166,15 +172,38 @@ async function handler(ctx) { pubDate: new Date(`2019-3-1`).toUTCString(), link: `https://mock.com/DIYgod/RSSHub`, author: `DIYgod`, + }, + { + title: `Complicated Title`, + description: `<a href="/DIYgod/RSSHub"></a> +<img src="/DIYgod/RSSHub.jpg"> +<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==">`, + pubDate: new Date(`2019-3-1`).toUTCString(), + link: `//mock.com/DIYgod/RSSHub`, + author: `DIYgod`, + enclosure_url: 'https://mock.com/DIYgod/RSSHub.png', + enclosure_type: 'image/png', + itunes_item_image: 'https://mock.com/DIYgod/RSSHub.gif', + }, + { + title: `Complicated Title`, + description: `<a href="/DIYgod/RSSHub"></a> +<img src="/DIYgod/RSSHub.jpg"> +<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==">`, + pubDate: new Date(`2019-3-1`).toUTCString(), + link: `//mock.com/DIYgod/RSSHub`, + author: `DIYgod`, + image: 'https://mock.com/DIYgod/RSSHub.jpg', } ); break; case 'multimedia': - item.push({ - title: `Multimedia Title`, - description: `<img src="/DIYgod/RSSHub.jpg"> + item.push( + { + title: `Multimedia Title`, + description: `<img src="/DIYgod/RSSHub.jpg"> <video src="/DIYgod/RSSHub.mp4"></video> <video poster="/DIYgod/RSSHub.jpg"> <source src="/DIYgod/RSSHub.mp4" type="video/mp4"> @@ -182,10 +211,21 @@ async function handler(ctx) { </video> <audio src="/DIYgod/RSSHub.mp3"></audio> <iframe src="/DIYgod/RSSHub.html"></iframe>`, - pubDate: new Date(`2019-3-1`).toUTCString(), - link: `https://mock.com/DIYgod/RSSHub`, - author: `DIYgod`, - }); + pubDate: new Date(`2019-3-1`).toUTCString(), + link: `https://mock.com/DIYgod/RSSHub`, + author: `DIYgod`, + }, + { + title: `Multimedia Title`, + description: `<img src="/DIYgod/RSSHub.jpg"> +<video src="/DIYgod/RSSHub.mp4"></video>`, + pubDate: new Date(`2019-3-1`).toUTCString(), + link: `https://mock.com/DIYgod/RSSHub`, + author: `DIYgod`, + enclosure_url: 'https://mock.com/DIYgod/RSSHub.mp4', + enclosure_type: 'video/mp4', + } + ); break; @@ -299,42 +339,12 @@ async function handler(ctx) { break; case 'gpt': - item.push( - { - title: 'Title0', - description: 'Description0', - pubDate: new Date(`2019-3-1`).toUTCString(), - link: 'https://github.com/DIYgod/RSSHub/issues/0', - }, - { - title: 'Title1', - description: - '快速开始\n' + - '如果您在使用 RSSHub 过程中遇到了问题或者有建议改进,我们很乐意听取您的意见!您可以通过 Pull Request 来提交您的修改。无论您对 Pull Request 的使用是否熟悉,我们都欢迎不同经验水平的开发者参与贡献。如果您不懂编程,也可以通过 报告错误 的方式来帮助我们。\n' + - '\n' + - '参与讨论\n' + - 'Telegram 群组 GitHub Issues GitHub 讨论\n' + - '\n' + - '开始之前\n' + - '要制作一个 RSS 订阅,您需要结合使用 Git、HTML、JavaScript、jQuery 和 Node.js。\n' + - '\n' + - '如果您对它们不是很了解,但想要学习它们,以下是一些好的资源:\n' + - '\n' + - 'MDN Web Docs 上的 JavaScript 指南\n' + - 'W3Schools\n' + - 'Codecademy 上的 Git 课程\n' + - '如果您想查看其他开发人员如何使用这些技术来制作 RSS 订阅的示例,您可以查看 我们的代码库 中的一些代码。\n' + - '\n' + - '提交新的 RSSHub 规则\n' + - '如果您发现一个网站没有提供 RSS 订阅,您可以使用 RSSHub 制作一个 RSS 规则。RSS 规则是一个短小的 Node.js 程序代码(以下简称 “路由”),它告诉 RSSHub 如何从网站中提取内容并生成 RSS 订阅。通过制作新的 RSS 路由,您可以帮助让您喜爱的网站的内容被更容易访问和关注。\n' + - '\n' + - '在您开始编写 RSS 路由之前,请确保源站点没有提供 RSS。一些网页会在 HTML 头部中包含一个 type 为 application/atom+xml 或 application/rss+xml 的 link 元素来指示 RSS 链接。\n' + - '\n' + - '这是在 HTML 头部中看到 RSS 链接可能会长成这样:<link rel="alternate" type="application/rss+xml" href="http://example.com/rss.xml" />。如果您看到这样的链接,这意味着这个网站已经有了一个 RSS 订阅,您不需要为它制作一个新的 RSS 路由。', - pubDate: new Date(`2019-3-1`).toUTCString(), - link: 'https://github.com/DIYgod/RSSHub/issues/1', - } - ); + item.push({ + title: 'Title0', + description: 'Description0', + pubDate: new Date(`2019-3-1`).toUTCString(), + link: 'https://github.com/DIYgod/RSSHub/issues/0', + }); break; @@ -376,6 +386,10 @@ async function handler(ctx) { await wait(1000); } + if (ctx.req.param('id') === 'slow4') { + await wait(4000); + } + if (ctx.req.query('mode') === 'fulltext') { item = [ { @@ -395,12 +409,13 @@ async function handler(ctx) { } return { + image, title: `Test ${ctx.req.param('id')}`, itunes_author: ctx.req.param('id') === 'enclosure' ? 'DIYgod' : null, link: 'https://github.com/DIYgod/RSSHub', item, allowEmpty: ctx.req.param('id') === 'allow_empty', description: - ctx.req.param('id') === 'complicated' ? '<img src="http://mock.com/DIYgod/DIYgod/RSSHub">' : ctx.req.param('id') === 'multimedia' ? '<video src="http://mock.com/DIYgod/DIYgod/RSSHub"></video>' : 'A test route for RSSHub', + ctx.req.param('id') === 'complicated' ? '<img src="http://mock.com/DIYgod/DIYgod/RSSHub">' : (ctx.req.param('id') === 'multimedia' ? '<video src="http://mock.com/DIYgod/DIYgod/RSSHub"></video>' : 'A test route for RSSHub'), }; } diff --git a/lib/routes/test/namespace.ts b/lib/routes/test/namespace.ts index f236265dcc7ddf..a6db48305f9deb 100644 --- a/lib/routes/test/namespace.ts +++ b/lib/routes/test/namespace.ts @@ -1,5 +1,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Unknown', + name: 'RSSHub Test', + lang: 'en', }; diff --git a/lib/routes/tfc-taiwan/common.ts b/lib/routes/tfc-taiwan/common.ts new file mode 100644 index 00000000000000..09076850a2191b --- /dev/null +++ b/lib/routes/tfc-taiwan/common.ts @@ -0,0 +1,38 @@ +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { baseUrl, parseList, parseItems } from './utils'; +import { getSubPath } from '@/utils/common-utils'; + +export async function handler(ctx) { + const requestPath = getSubPath(ctx); + const isTopic = requestPath.startsWith('/topic/'); + let link = baseUrl; + + if (isTopic) { + link += `/topic/${ctx.req.param('id')}`; + } else if (requestPath === '/') { + link += `/articles/report`; + } else { + link += `/articles${requestPath}`; + } + + const { data: response } = await got(link); + const $ = load(response); + + const list = $(`${isTopic ? '.view-grouping' : '.pane-clone-of-article'} .views-row-inner`) + .toArray() + .map((item) => parseList($(item))); + + const items = await parseItems(list, cache.tryGet); + + return { + title: $('head title').text(), + description: $('head meta[name="description"]').attr('content'), + image: $('head meta[property="og:image"]').attr('content'), + logo: $('head link[rel="shortcut icon"]').attr('href'), + icon: $('head link[rel="shortcut icon"]').attr('href'), + link, + item: items, + }; +} diff --git a/lib/routes/tfc-taiwan/index.ts b/lib/routes/tfc-taiwan/index.ts index cac1cf3581ac79..4b1653194803c8 100644 --- a/lib/routes/tfc-taiwan/index.ts +++ b/lib/routes/tfc-taiwan/index.ts @@ -1,47 +1,17 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import { baseUrl, parseList, parseItems } from './utils'; +import { handler } from './common'; export const route: Route = { - path: ['/', '/category/:id{.+}', '/info', '/report', '/topic/:id'], - name: 'Unknown', + name: '最新相關資訊 / 最新查核報告', maintainers: ['TonyRL'], + example: '/tfc-taiwan', + path: '/:type?', + parameters: { + type: '分類,見下表,預設為 `report`', + }, handler, url: 'tfc-taiwan.org.tw/articles/report', - url: 'tfc-taiwan.org.tw/articles/info', + description: `| 最新相關資訊 | 最新查核報告 | +| ------------ | ------------ | +| info | report |`, }; - -async function handler(ctx) { - const requestPath = ctx.req.path; - const isTopic = requestPath.startsWith('/topic/'); - let link = baseUrl; - - if (isTopic) { - link += `/topic/${ctx.req.param('id')}`; - } else if (requestPath === '/') { - link += `/articles/report`; - } else { - link += `/articles${requestPath}`; - } - - const { data: response } = await got(link); - const $ = load(response); - - const list = $(`${isTopic ? '.view-grouping' : '.pane-clone-of-article'} .views-row-inner`) - .toArray() - .map((item) => parseList($(item))); - - const items = await parseItems(list, cache.tryGet); - - return { - title: $('head title').text(), - description: $('head meta[name="description"]').attr('content'), - image: $('head meta[property="og:image"]').attr('content'), - logo: $('head link[rel="shortcut icon"]').attr('href'), - icon: $('head link[rel="shortcut icon"]').attr('href'), - link, - item: items, - }; -} diff --git a/lib/routes/tfc-taiwan/namespace.ts b/lib/routes/tfc-taiwan/namespace.ts index 7704954e3a5589..962c3e3aed7e49 100644 --- a/lib/routes/tfc-taiwan/namespace.ts +++ b/lib/routes/tfc-taiwan/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '台灣事實查核中心', url: 'tfc-taiwan.org.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/tfc-taiwan/topic.ts b/lib/routes/tfc-taiwan/topic.ts new file mode 100644 index 00000000000000..ba1badc3d5c236 --- /dev/null +++ b/lib/routes/tfc-taiwan/topic.ts @@ -0,0 +1,17 @@ +import { Route } from '@/types'; +import { handler } from './common'; + +export const route: Route = { + name: '專題 / 重點專區', + maintainers: ['TonyRL'], + example: '/tfc-taiwan/category/242', + path: '/:type/:id{.+}', + parameters: { + type: '分類,見下表,預設為 `report`', + }, + handler, + url: 'tfc-taiwan.org.tw/articles/report', + description: `| 專題 | 重點專區 | +| -------- | -------- | +| category | topic |`, +}; diff --git a/lib/routes/tgbus/list.ts b/lib/routes/tgbus/list.ts index 136ddb2af03546..4b26806493a31c 100644 --- a/lib/routes/tgbus/list.ts +++ b/lib/routes/tgbus/list.ts @@ -33,8 +33,8 @@ export const route: Route = { maintainers: ['Xzonn'], handler, description: `| 最新资讯 | 游戏评测 | 游戏视频 | 巴士首页特稿 | 硬件资讯 | - | -------- | -------- | -------- | ------------ | -------- | - | news | review | video | special | hardware |`, +| -------- | -------- | -------- | ------------ | -------- | +| news | review | video | special | hardware |`, }; async function handler(ctx) { diff --git a/lib/routes/tgbus/namespace.ts b/lib/routes/tgbus/namespace.ts index f8b1f1cef145e6..8359b833c3d514 100644 --- a/lib/routes/tgbus/namespace.ts +++ b/lib/routes/tgbus/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '电玩巴士 TGBUS', url: 'tgbus.com', + lang: 'zh-CN', }; diff --git a/lib/routes/the/index.ts b/lib/routes/the/index.ts new file mode 100644 index 00000000000000..f7747887575c20 --- /dev/null +++ b/lib/routes/the/index.ts @@ -0,0 +1,214 @@ +import { Route } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +import { apiSlug, bakeFilterSearchParams, bakeFiltersWithPair, bakeUrl, fetchData, getFilterParamsForUrl, parseFilterStr } from './util'; +import timezone from '@/utils/timezone'; + +export const handler = async (ctx) => { + const { filter } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 40; + + const rootUrl = 'https://the.bi/s'; + const filters = parseFilterStr(filter); + const filtersWithPair = await bakeFiltersWithPair(filters, rootUrl); + + const searchParams = bakeFilterSearchParams(filters, 'name', false); + const apiSearchParams = bakeFilterSearchParams(filtersWithPair, 'id', true); + + apiSearchParams.append('_embed', 'true'); + apiSearchParams.append('per_page', String(limit)); + apiSearchParams.append('page', '1'); + + const apiUrl = bakeUrl(`${apiSlug}/posts`, rootUrl, apiSearchParams); + const currentUrl = bakeUrl(getFilterParamsForUrl(filtersWithPair) ?? '', rootUrl, searchParams); + + const { data: response } = await got(apiUrl); + + const items = response.slice(0, limit).map((item) => { + const terminologies = item._embedded['wp:term']; + const guid = item.guid?.rendered ?? item.guid; + + const $$ = load(item.content?.rendered ?? item.content); + + const publication = $$("a[id='publication']").text(); // Must be obtained before being removed + + const image = $$('img#poster').prop('data-srcset'); + + $$('figure.graf').each((_, el) => { + el = $$(el); + + const imgEl = el.find('img'); + + el.replaceWith( + art(path.join(__dirname, 'templates/description.art'), { + images: imgEl + ? [ + { + src: imgEl.prop('src'), + width: imgEl.prop('width'), + height: imgEl.prop('height'), + }, + ] + : undefined, + }) + ); + }); + + const title = $$('h1').text(); + const intro = $$('h2').text(); + + $$('h1').parent().remove(); + + const description = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + }, + ] + : undefined, + intro, + description: $$.html(), + }); + + return { + title: item.title?.rendered ?? item.title ?? title, + description, + pubDate: timezone(parseDate(item.date_gmt), 0), + updated: timezone(parseDate(item.modified_gmt), 0), + link: item.link, + category: [...new Set(terminologies.flat().map((c) => c.name))], + author: [...item._embedded.author, { name: publication }], + guid, + id: guid, + content: { + html: description, + text: $$.text(), + }, + }; + }); + + const data = await fetchData(currentUrl, rootUrl); + + return { + ...data, + item: items, + }; +}; + +export const route: Route = { + path: '/:filter{.+}?', + name: '分类', + url: 'the.bi', + maintainers: ['nczitzk'], + handler, + example: '/the', + parameters: { filter: '过滤器,见下方描述' }, + description: `::: tip + 如果你想订阅特定类别或标签,可以在路由中填写 filter 参数。\`/category/rawmw7dsta2jew\` 可以实现订阅 [剩余价值](https://the.bi/s/rawmw7dsta2jew) 类别。此时,路由是 [\`/the/category/rawmw7dsta2jew/\`](https://rsshub.app/the/category/rawmw7dsta2jew). + + 你还可以订阅多个类别。\`/category/rawmw7dsta2jew,rawbcvxkktdkq8/\` 可以实现同时订阅 [剩余价值](https://the.bi/s/rawmw7dsta2jew) 和 [打江山](https://the.bi/s/rawbcvxkktdkq8) 两个类别。此时,路由是 [\`/the/category/rawmw7dsta2jew,rawbcvxkktdkq8\`](https://rsshub.app/the/category/rawmw7dsta2jew,rawbcvxkktdkq8). + + 类别和标签也可以合并订阅。\`/category/rawmw7dsta2jew/tag/raweekl3na8trq\` 订阅 [剩余价值](https://the.bi/s/rawmw7dsta2jew) 类别和 [动物](https://the.bi/s/raweekl3na8trq) 标签。此时,路由是 [\`/the/category/rawmw7dsta2jew/tag/raweekl3na8trq\`](https://rsshub.app/the/category/rawmw7dsta2jew/tag/raweekl3na8trq). + + 你还可以搜索关键字。\`/search/中国\` 搜索关键字 [中国](https://the.bi/s/?s=中国)。在这种情况下,路径是 [\`/the/search/中国\`](https://rsshub.app/the/search/中国). +::: + +| 分类 | ID | +| ---------------------------------------------- | ---------------------------------------------------------------- | +| [时局图](https://the.bi/s/rawj7o4ypewv94) | [rawj7o4ypewv94](https://rsshub.app/the/category/rawj7o4ypewv94) | +| [剩余价值](https://the.bi/s/rawmw7dsta2jew) | [rawmw7dsta2jew](https://rsshub.app/the/category/rawmw7dsta2jew) | +| [打江山](https://the.bi/s/rawbcvxkktdkq8) | [rawbcvxkktdkq8](https://rsshub.app/the/category/rawbcvxkktdkq8) | +| [中国经济](https://the.bi/s/raw4krvx85dh27) | [raw4krvx85dh27](https://rsshub.app/the/category/raw4krvx85dh27) | +| [水深火热](https://the.bi/s/rawtn8jpsc6uvv) | [rawtn8jpsc6uvv](https://rsshub.app/the/category/rawtn8jpsc6uvv) | +| [东升西降](https://the.bi/s/rawai5kd4z15il) | [rawai5kd4z15il](https://rsshub.app/the/category/rawai5kd4z15il) | +| [大局 & 大棋](https://the.bi/s/raw2efkzejrsx8) | [raw2efkzejrsx8](https://rsshub.app/the/category/raw2efkzejrsx8) | +| [境外势力](https://the.bi/s/rawmpalhnlphuc) | [rawmpalhnlphuc](https://rsshub.app/the/category/rawmpalhnlphuc) | +| [副刊](https://the.bi/s/rawxght2jr2u5z) | [rawxght2jr2u5z](https://rsshub.app/the/category/rawxght2jr2u5z) | +| [天高地厚](https://the.bi/s/rawrsnh9zakqdx) | [rawrsnh9zakqdx](https://rsshub.app/the/category/rawrsnh9zakqdx) | +| [Oyster](https://the.bi/s/rawdhl9hugdfn9) | [rawdhl9hugdfn9](https://rsshub.app/the/category/rawdhl9hugdfn9) | + `, + categories: ['new-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['the.bi/s/:category?'], + target: (params) => { + const category = params.category; + + return `/the${category ? `/category/${category}` : ''}`; + }, + }, + { + title: '时局图', + source: ['the.bi/s/rawj7o4ypewv94'], + target: '/category/rawj7o4ypewv94', + }, + { + title: '剩余价值', + source: ['the.bi/s/rawmw7dsta2jew'], + target: '/category/rawmw7dsta2jew', + }, + { + title: '打江山', + source: ['the.bi/s/rawbcvxkktdkq8'], + target: '/category/rawbcvxkktdkq8', + }, + { + title: '中国经济', + source: ['the.bi/s/raw4krvx85dh27'], + target: '/category/raw4krvx85dh27', + }, + { + title: '水深火热', + source: ['the.bi/s/rawtn8jpsc6uvv'], + target: '/category/rawtn8jpsc6uvv', + }, + { + title: '东升西降', + source: ['the.bi/s/rawai5kd4z15il'], + target: '/category/rawai5kd4z15il', + }, + { + title: '大局 & 大棋', + source: ['the.bi/s/raw2efkzejrsx8'], + target: '/category/raw2efkzejrsx8', + }, + { + title: '境外势力', + source: ['the.bi/s/rawmpalhnlphuc'], + target: '/category/rawmpalhnlphuc', + }, + { + title: '副刊', + source: ['the.bi/s/rawxght2jr2u5z'], + target: '/category/rawxght2jr2u5z', + }, + { + title: '天高地厚', + source: ['the.bi/s/rawrsnh9zakqdx'], + target: '/category/rawrsnh9zakqdx', + }, + { + title: 'Oyster', + source: ['the.bi/s/rawdhl9hugdfn9'], + target: '/category/rawdhl9hugdfn9', + }, + ], +}; diff --git a/lib/routes/the/namespace.ts b/lib/routes/the/namespace.ts new file mode 100644 index 00000000000000..7e1491bc10353c --- /dev/null +++ b/lib/routes/the/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'The.bi', + url: 'the.bi', + categories: ['new-media'], + description: '', + lang: 'en', +}; diff --git a/lib/routes/the/templates/description.art b/lib/routes/the/templates/description.art new file mode 100644 index 00000000000000..cd725d1f54a204 --- /dev/null +++ b/lib/routes/the/templates/description.art @@ -0,0 +1,27 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} + <figure> + <img + {{ if image.alt }} + alt="{{ image.alt }}" + {{ /if }} + {{ if image.width }} + alt="{{ image.width }}" + {{ /if }} + {{ if image.height }} + alt="{{ image.height }}" + {{ /if }} + src="{{ image.src }}"> + </figure> + {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} + <blockquote>{{ intro }}</blockquote> +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/the/util.ts b/lib/routes/the/util.ts new file mode 100644 index 00000000000000..fbbdc853fa946a --- /dev/null +++ b/lib/routes/the/util.ts @@ -0,0 +1,307 @@ +import got from '@/utils/got'; +import { load } from 'cheerio'; + +const apiSlug = 'wp-json/wp/v2'; + +interface Filter { + id: string; + name: string; + slug: string; +} + +const filterKeys: Record<string, string> = { + search: 's', +}; + +const filterApiKeys: Record<string, string | undefined> = { + category: 'categories', + tag: 'tags', + search: undefined, +}; + +const filterApiKeysWithNoId = new Set(['search']); + +/** + * Bake filter search parameters. + * + * @param filterPairs - The filter pairs object. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + * @param pairKey - The filter pair key. + * e.g. `{ id: ..., name: ..., slug: ... }`. + * @param isApi - Indicates if the search parameters are for API. + * @returns The baked filter search parameters. + */ +const bakeFilterSearchParams = (filterPairs: Record<string, Filter[] | string[]>, pairKey: string, isApi: boolean = false): URLSearchParams => { + /** + * Bake filters recursively. + * + * @param filterPairs - The filter pairs object. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + * @param filterSearchParams - The filter search parameters. + * e.g. `category=a,b&tag=c`. + * @returns The baked filter search parameters. + * e.g. `category=a,b&tag=c`. + */ + const bakeFilters = (filterPairs: Record<string, Filter[] | string[]>, filterSearchParams: URLSearchParams): URLSearchParams => { + const keys = Object.keys(filterPairs).filter((key) => filterPairs[key]?.length > 0 && (isApi ? Object.hasOwn(filterApiKeys, key) : Object.hasOwn(filterKeys, key))); + + if (keys.length === 0) { + return filterSearchParams; + } + + const key = keys[0]; + const pairs = filterPairs[key]; + + const originalFilters = { ...filterPairs }; + delete originalFilters[key]; + + const filterKey = getFilterKeyForSearchParams(key, isApi); + const pairValues = pairs.map((pair) => (Object.hasOwn(pair, pairKey) ? pair[pairKey] : pair)); + + if (filterKey) { + filterSearchParams.append(filterKey, pairValues.join(',')); + } + + return bakeFilters(originalFilters, filterSearchParams); + }; + + return bakeFilters(filterPairs, new URLSearchParams()); +}; + +/** + * Bake filters with pair. + * + * @param filters - The filters object. + * e.g. `{ category: [ a, b ], tag: [ c ] }`. + * @returns The baked filters. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + */ +const bakeFiltersWithPair = async (filters: Record<string, string[]>, rootUrl: string) => { + /** + * Bake keywords recursively. + * + * @param key - The key. + * e.g. `category` or `tag`. + * @param keywords - The keywords. + * e.g. `[ a, b ]`. + * @returns The baked keywords. + * e.g. `[ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ]`. + */ + const bakeKeywords = async (key: string, keywords: string[]) => { + if (keywords.length === 0) { + return []; + } + + const [keyword, ...rest] = keywords; + + const filter = await getFilterByKeyAndKeyword(key, keyword, rootUrl); + + return [ + ...(filter?.id && filter?.slug + ? [ + { + id: filter.id, + name: filter.name, + slug: filter.slug, + }, + ] + : []), + ...(await bakeKeywords(key, rest)), + ]; + }; + + /** + * Bake filters recursively. + * + * @param filters - The filters object. + * e.g. `{ category: [ a, b ], tag: [ c ] }`. + * @param filtersWithPair - The filters with pairs. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + * @returns The baked filters. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + */ + const bakeFilters = async (filters: Record<string, string[]>, filtersWithPair: Record<string, Filter[]>) => { + const keys = Object.keys(filters); + + if (keys.length === 0) { + return filtersWithPair; + } + + const key = keys[0]; + const keywords = filters[key]; + + const originalFilters = { ...filters }; + delete originalFilters[key]; + + return bakeFilters(originalFilters, { + ...filtersWithPair, + [key]: filterApiKeysWithNoId.has(key) ? keywords : await bakeKeywords(key, keywords), + }); + }; + + return await bakeFilters(filters, {}); +}; + +/** + * Bake URL with search parameters. + * + * @param url - The URL. + * @param rootUrl - The root URL. + * @param searchParams - The search parameters. + * @returns The baked URL. + */ +const bakeUrl = (url: string, rootUrl: string, searchParams: URLSearchParams = new URLSearchParams()): string => { + const searchParamsStr = searchParams.toString(); + const searchParamsSuffix = searchParamsStr ? `?${searchParamsStr}` : ''; + + return `${rootUrl}/${url}${searchParamsSuffix}`; +}; + +/** + * Fetch data from the specified URL. + * + * @param url - The URL to fetch data from. + * @param rootUrl - The root URL. + * @returns A promise that resolves to an object containing the fetched data to be added into `ctx.state.data`. + */ +const fetchData = async (url: string, rootUrl: string): Promise<object> => { + /** + * Request URLs recursively. + * + * @param urls - The URLs to request. + * @returns A promise that resolves to the response data or undefined if no response is available. + */ + const requestUrls = async (urls: string[]): Promise<string | undefined> => { + if (urls.length === 0) { + return; + } + + const [currentUrl, ...remainingUrls] = urls; + try { + const { data: response } = await got.get(currentUrl); + return response; + } catch { + return requestUrls(remainingUrls); + } + }; + + const response = await requestUrls([url, rootUrl]); + + if (!response) { + return {}; + } + + const $ = load(response); + + const title = $('title').first().text(); + const image = new URL('wp-content/uploads/site_logo.png', rootUrl).href; + + return { + title, + description: $('meta[property="og:description"]').attr('content') || $('meta[name="description"]').attr('content'), + link: url, + allowEmpty: true, + image, + author: $('meta[property="og:site_name"]').attr('content'), + language: $('html').attr('lang'), + }; +}; + +/** + * Get filter by key and keyword. + * + * @param key - The key. + * e.g. `category` or `tag`. + * @param keyword - The keywords. + * e.g. `keyword1`. + * @returns A promise that resolves to the filter object if found, or undefined if not found. + */ +const getFilterByKeyAndKeyword = async (key: string, keyword: string, rootUrl: string): Promise<Filter | undefined> => { + const apiFilterUrl = `${rootUrl}/${apiSlug}/${getFilterKeyForSearchParams(key, true)}`; + + const { data: response } = await got(apiFilterUrl, { + searchParams: { + search: keyword, + }, + }); + + return response.length > 0 ? response[0] : undefined; +}; + +/** + * Get filter key for search parameters. + * + * @param key - The key. e.g. `category` or `tag`. + * @param isApi - Indicates whether the key is for the API. + * @returns The filter key for search parameters, or undefined if not found. + * e.g. `categories` or `tags`. + */ +const getFilterKeyForSearchParams = (key: string, isApi: boolean = false): string | undefined => { + const keys = isApi ? filterApiKeys : filterKeys; + + return Object.hasOwn(keys, key) ? (keys[key] ?? key) : undefined; +}; + +/** + * Get filter parameters for URL. + * + * @param filterPairs - The filter pairs object. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + * @returns The filter parameters for the URL, or undefined if no filters are available. + */ +const getFilterParamsForUrl = (filterPairs: Record<string, Filter[]>): string | undefined => { + const keys = Object.keys(filterPairs).filter((key) => filterPairs[key].length > 0 && !Object.hasOwn(filterKeys, key)); + + if (keys.length === 0) { + return; + } + + const key = keys[0]; + + return filterPairs[key].map((pair) => pair.slug).join('/'); +}; + +/** + * Parses a filter string into a filters object. + * + * @param filterStr - The filter string to parse. + * e.g. `category/a,b/tag/c`. + * @returns The parsed filters object. + * e.g. `{ category: [ 'a', 'b' ], tag: [ 'c' ] }`. + */ +const parseFilterStr = (filterStr: string | undefined): Record<string, string[]> => { + /** + * Recursively parses a filter string. + * + * @param remainingStr - The remaining filter string to parse. + * e.g. `category/a,b/tag/c`. + * @param filters - The accumulated filters object. + * e.g. `{ category: [ a, b ], tag: [ c ] }`. + * @param currentKey - The current filter key. + * e.g. `category` or `tag`. + * @returns The parsed filters object. + */ + const parseStr = (remainingStr: string | undefined, filters: Record<string, string[]> = {}, currentKey?: string): Record<string, string[]> => { + if (!remainingStr) { + return filters; + } + + const [word, ...rest] = remainingStr.split(/\/|,/); + + const isKey = Object.hasOwn(filterApiKeys, word); + const key = isKey ? word : currentKey; + + const newFilters = key + ? { + ...filters, + [key]: [...(filters[key] || []), ...(isKey ? [] : [word])], + } + : filters; + + return parseStr(rest.join('/'), newFilters, key); + }; + + return parseStr(filterStr, {}); +}; + +export { apiSlug, bakeFilterSearchParams, bakeFiltersWithPair, bakeUrl, fetchData, getFilterParamsForUrl, parseFilterStr }; diff --git a/lib/routes/theatlantic/namespace.ts b/lib/routes/theatlantic/namespace.ts index 02a59a6066dab8..3323d1a1aa1f62 100644 --- a/lib/routes/theatlantic/namespace.ts +++ b/lib/routes/theatlantic/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The Atlantic', url: 'www.theatlantic.com', + lang: 'en', }; diff --git a/lib/routes/theatlantic/news.ts b/lib/routes/theatlantic/news.ts index eab56646aeccd4..8052363a5ca67f 100644 --- a/lib/routes/theatlantic/news.ts +++ b/lib/routes/theatlantic/news.ts @@ -1,5 +1,5 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import { getArticleDetails } from './utils'; export const route: Route = { @@ -21,11 +21,11 @@ export const route: Route = { }, ], name: 'News', - maintainers: ['EthanWng97'], + maintainers: ['EthanWng97', 'pseudoyu'], handler, description: `| Popular | Latest | Politics | Technology | Business | - | ------------ | ------ | -------- | ---------- | -------- | - | most-popular | latest | politics | technology | business | +| ------------ | ------ | -------- | ---------- | -------- | +| most-popular | latest | politics | technology | business | More categories (except photo) can be found within the navigation bar at [https://www.theatlantic.com](https://www.theatlantic.com)`, }; @@ -34,11 +34,8 @@ async function handler(ctx) { const host = 'https://www.theatlantic.com'; const category = ctx.req.param('category'); const url = `${host}/${category}/`; - const response = await got({ - method: 'get', - url, - }); - const $ = load(response.data); + const response = await ofetch(url); + const $ = load(response); const contents = JSON.parse($('script#__NEXT_DATA__').text()).props.pageProps.urqlState; const keyWithContent = Object.keys(contents).filter((key) => contents[key].data.includes(category)); const data = JSON.parse(contents[keyWithContent].data); diff --git a/lib/routes/theatlantic/utils.ts b/lib/routes/theatlantic/utils.ts index f0dc4c2da9b079..cba3daa8d28f4a 100644 --- a/lib/routes/theatlantic/utils.ts +++ b/lib/routes/theatlantic/utils.ts @@ -3,7 +3,7 @@ const __dirname = getCurrentPath(import.meta.url); import cache from '@/utils/cache'; import { load } from 'cheerio'; -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'; @@ -16,20 +16,18 @@ const getArticleDetails = async (items) => { items.map((item) => cache.tryGet(item.link, async () => { const url = item.link; - const response = await got({ - url, - method: 'get', + const html = await ofetch(url, { headers: { 'User-Agent': UA, }, }); - const html = response.data; const $ = load(html); let data = JSON.parse($('script#__NEXT_DATA__').text()); const list = data.props.pageProps.urqlState; const keyWithContent = Object.keys(list).filter((key) => list[key].data.includes('content')); data = JSON.parse(list[keyWithContent].data).article; + item.title = data.shareTitle; item.category = data.categories.map((category) => category.slug); for (const channel of data.channels) { @@ -37,9 +35,13 @@ const getArticleDetails = async (items) => { } item.content = data.content.filter((item) => item.innerHtml !== undefined && item.innerHtml !== ''); item.caption = data.dek; - item.imgUrl = data.leadArt.image?.url; - item.imgAlt = data.leadArt.image?.altText; - item.imgCaption = data.leadArt.image?.attributionText; + + if (data.leadArt) { + item.imgUrl = data.leadArt.image?.url; + item.imgAlt = data.leadArt.image?.altText; + item.imgCaption = data.leadArt.image?.attributionText; + } + item.description = art(path.join(__dirname, 'templates/article-description.art'), { item, }); diff --git a/lib/routes/theblock/index.ts b/lib/routes/theblock/index.ts new file mode 100644 index 00000000000000..e34b53c83e1264 --- /dev/null +++ b/lib/routes/theblock/index.ts @@ -0,0 +1,142 @@ +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'; + +export const route: Route = { + path: '/category/:category', + categories: ['finance'], + example: '/theblock/category/crypto-ecosystems', + parameters: { category: 'News category' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Category', + maintainers: ['pseudoyu'], + handler, + radar: [ + { + source: ['theblock.co/category/:category'], + target: '/category/:category', + }, + ], + description: 'Get latest news from TheBlock by category. Note that due to website limitations, only article summaries may be available.', +}; + +async function handler(ctx): Promise<Data> { + const category = ctx.req.param('category'); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10; + + const apiUrl = `https://www.theblock.co/api/category/${category}`; + + try { + const response = await ofetch(apiUrl); + + // Extract articles from the nested data structure + const articles = response.data?.articles || []; + + if (!articles.length) { + throw new Error(`No articles found for category: ${category}`); + } + + const items = await Promise.all( + articles.slice(0, limit).map((article) => + cache.tryGet(`theblock:article:${article.url}`, async () => { + try { + // Try to get the full article + const articleResponse = await ofetch(`https://www.theblock.co/api/post/${article.id}/`); + + const post = articleResponse.post; + const $ = load(post.body, null, false); + + // If we successfully got the article content + if (post.body.length) { + // Remove unwanted elements + $('.copyright').remove(); + + let fullText = ''; + + if (article.thumbnail) { + fullText += `<p><img src="${post.thumbnail}" alt="${article.title}"></p>`; + } + fullText += post.intro + $.html(); + + if (fullText) { + return { + title: article.title, + link: article.url, + pubDate: parseDate(post.published), + description: fullText, + author: article.authors?.map((a) => a.name).join(', ') || 'TheBlock', + category: [...new Set([post.categories.name, ...post.categories.map((cat) => cat.name), ...post.tags.map((tag) => tag.name)])], + guid: article.url, + image: article.thumbnail, + }; + } + } + + // If we couldn't extract specific content, fall back to a summary-based approach + logger.info(`Using summary-based approach for article: ${article.url}`); + return createSummaryItem(article); + } catch (error: any) { + // If we got a 403 error or any other error, use summary approach + logger.warn(`Couldn't fetch full content for ${article.url}: ${error.message}`); + return createSummaryItem(article); + } + }) + ) + ); + + return { + title: `TheBlock - ${category.charAt(0).toUpperCase() + category.slice(1).replaceAll('-', ' ')}`, + link: `https://www.theblock.co/category/${category}`, + item: items, + description: `Latest articles from TheBlock in the ${category} category`, + language: 'en', + } as Data; + } catch (error: any) { + logger.error(`Error in TheBlock handler: ${error.message}`); + throw error; + } +} + +// Helper function to create a summary-based item when full content isn't available +function createSummaryItem(article: any) { + let description = ''; + + // Add thumbnail if available + if (article.thumbnail) { + description += `<p><img src="${article.thumbnail}" alt="${article.title}"></p>`; + } + + // Add subheading if available + if (article.subheading) { + description += `<p><strong>${article.subheading}</strong></p>`; + } + + // Add preview if available + if (article.preview) { + description += `<p>${article.preview}</p>`; + } + + // Add link to original article + description += `<p><a href="${article.url}">Read the full article at TheBlock</a></p>`; + + return { + title: article.title, + link: article.url, + pubDate: parseDate(article.publishedFormatted, 'MMMM D, YYYY, h:mmA [EST]'), + description, + author: article.authors?.map((a) => a.name).join(', ') || 'TheBlock', + category: article.primaryCategory?.name || [], + guid: article.url, + image: article.thumbnail, + }; +} diff --git a/lib/routes/theblock/namespace.ts b/lib/routes/theblock/namespace.ts new file mode 100644 index 00000000000000..034e10142bf684 --- /dev/null +++ b/lib/routes/theblock/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'TheBlock', + url: 'theblock.co', + lang: 'en', +}; diff --git a/lib/routes/theblockbeats/index.ts b/lib/routes/theblockbeats/index.ts index 0ffd888e12de32..ad8a0a69969c9d 100644 --- a/lib/routes/theblockbeats/index.ts +++ b/lib/routes/theblockbeats/index.ts @@ -1,67 +1,123 @@ -import { Route } from '@/types'; +import { Route, ViewType } 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'; +import path from 'node:path'; +import { art } from '@/utils/render'; +import { getCurrentPath } from '@/utils/helpers'; + +const __dirname = getCurrentPath(import.meta.url); const domain = 'theblockbeats.info'; const rootUrl = `https://www.${domain}`; const apiBase = `https://api.${domain}`; +const render = (data) => { + const html = art(path.join(__dirname, 'templates/description.art'), data); + const $ = load(html, null, false); + + $('img').each((_, e) => { + const $e = $(e); + const src = $e.attr('src'); + $e.attr('src', src?.split('?')[0]); + }); + + return $.html(); +}; + const channelMap = { newsflash: { title: '快讯', link: `${rootUrl}/newsflash`, - api: `${apiBase}/v5/newsflash/select`, + api: `${apiBase}/v2/newsflash/list`, }, article: { title: '文章', link: `${rootUrl}/article`, - api: `${apiBase}/v5/Information/newsall`, + api: `${apiBase}/v2/article/list`, }, }; export const route: Route = { - path: '/:channel?', - categories: ['finance'], + path: '/:channel?/:original?', + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/theblockbeats/newsflash', - parameters: { channel: '类型,见下表,默认为快讯' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, + parameters: { + channel: { + description: '类型', + options: [ + { value: 'newsflash', label: '快讯' }, + { value: 'article', label: '文章' }, + ], + default: 'newsflash', + }, + original: { + description: '文章类型,仅 `channel` 为 `article` 时有效', + options: [ + { value: '0', label: '全部' }, + { value: '1', label: '深度' }, + { value: '2', label: '精选' }, + { value: '3', label: '热点追踪' }, + ], + default: '0', + }, }, name: '新闻快讯', maintainers: ['Fatpandac', 'jameshih'], handler, + radar: [ + { + title: '文章', + source: ['www.theblockbeats.info/article'], + target: '/article', + }, + { + title: '快讯', + source: ['www.theblockbeats.info/newsflash'], + target: '/newsflash', + }, + ], description: `| 快讯 | 文章 | - | :-------: | :-----: | - | newsflash | article |`, +| :-------: | :-----: | +| newsflash | article | + +| 全部 | 深度 | 精选 | 热点追踪 | +| :--: | :--: | :--: | :---: | +| | -2 | 1 | 2 |`, }; async function handler(ctx) { - const { channel = 'newsflash' } = ctx.req.param(); + const { channel = 'newsflash', original } = ctx.req.param(); - const { data: response } = await got(channelMap[channel].api); + const response = await ofetch(channelMap[channel].api, { + query: { + limit: ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20, + original: channel === 'article' ? original : undefined, + }, + }); - const { data } = channel === 'newsflash' ? response.data : response; - let list = data.map((item) => ({ + let list = response.data.list.map((item) => ({ title: item.title, - link: `${rootUrl}/${channel === 'newsflash' ? 'flash' : 'news'}/${item.id}`, - description: item.content ?? item.im_abstract, + link: `${rootUrl}/${channel === 'newsflash' ? 'flash' : 'news'}/${item.article_id}`, + description: item.content ?? item.abstract, pubDate: parseDate(item.add_time, 'X'), + author: item.author?.nickname, + category: item.tag_list, + imgUrl: item.img_url, })); if (channel !== 'newsflash') { list = await Promise.all( list.map((item) => - cache.tryGet(item.link, async () => { - const { data: response } = await got(item.link); + cache.tryGet(`theblockbeats:${item.link}`, async () => { + const response = await ofetch(item.link); const $ = load(response); - item.description = $('div.news-content').html(); + item.description = render({ + image: item.imgUrl, + description: $('div.news-content').html(), + }); return item; }) ) diff --git a/lib/routes/theblockbeats/namespace.ts b/lib/routes/theblockbeats/namespace.ts index da29bc6823579e..04a460220ed3b1 100644 --- a/lib/routes/theblockbeats/namespace.ts +++ b/lib/routes/theblockbeats/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: '律动', - url: 'rszhaopin.theblockbeats.info', + name: '律动 BlockBeats', + url: 'www.theblockbeats.info', + lang: 'zh-CN', }; diff --git a/lib/routes/theblockbeats/templates/description.art b/lib/routes/theblockbeats/templates/description.art new file mode 100644 index 00000000000000..35898eb4cb0fd5 --- /dev/null +++ b/lib/routes/theblockbeats/templates/description.art @@ -0,0 +1,5 @@ +{{ if image }} + <img src="{{ image }}" /><br> +{{ /if }} + +{{@ description }} diff --git a/lib/routes/thecover/channel.ts b/lib/routes/thecover/channel.ts index 162338c0c7700d..210b3ddfee9b67 100644 --- a/lib/routes/thecover/channel.ts +++ b/lib/routes/thecover/channel.ts @@ -41,8 +41,8 @@ export const route: Route = { maintainers: ['yuxinliu-alex'], handler, description: `| 天下 | 四川 | 辟谣 | 国际 | 云招考 | 30 秒 | 拍客 | 体育 | 国内 | 帮扶铁军 | 文娱 | 宽窄 | 商业 | 千面 | 封面号 | - | ---- | ---- | ---- | ---- | ------ | ----- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | ------ | - | 3892 | 3560 | 3909 | 3686 | 11 | 3902 | 3889 | 3689 | 1 | 4002 | 12 | 46 | 4 | 21 | 17 |`, +| ---- | ---- | ---- | ---- | ------ | ----- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | ------ | +| 3892 | 3560 | 3909 | 3686 | 11 | 3902 | 3889 | 3689 | 1 | 4002 | 12 | 46 | 4 | 21 | 17 |`, }; async function handler(ctx) { diff --git a/lib/routes/thecover/namespace.ts b/lib/routes/thecover/namespace.ts index 536da3af111b1e..db73c480c127bf 100644 --- a/lib/routes/thecover/namespace.ts +++ b/lib/routes/thecover/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '封面新闻', url: 'thecover.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/thegradient/index.ts b/lib/routes/thegradient/index.ts new file mode 100644 index 00000000000000..f255f7ad54064d --- /dev/null +++ b/lib/routes/thegradient/index.ts @@ -0,0 +1,80 @@ +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: '/posts', + categories: ['blog'], + example: '/thegradient/posts', + radar: [ + { + source: ['thegradient.pub/'], + }, + ], + url: 'thegradient.pub/', + name: 'Posts', + maintainers: ['liyaozhong'], + handler, + description: 'The Gradient Blog Posts', +}; + +async function handler() { + const rootUrl = 'https://thegradient.pub'; + const currentUrl = rootUrl; + + const response = await got(currentUrl); + const $ = load(response.data); + + let items = $('.c-post-card-wrap') + .toArray() + .map((item) => { + const $item = $(item); + const $link = $item.find('.c-post-card__title-link').first(); + const $meta = $item.find('.c-post-card__meta'); + + const href = $link.attr('href'); + const title = $link.text().trim(); + const dateStr = $meta.find('time').attr('datetime'); + + if (!href || !title || !dateStr) { + return null; + } + + const link = new URL(href, rootUrl).href; + const pubDate = parseDate(dateStr); + + 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); + + item.description = $detail('.c-content').html() || ''; + + return item as DataItem; + } catch { + return item; + } + }) + ) + ) + ).filter((item): item is DataItem => item !== null); + + return { + title: 'The Gradient Blog', + link: rootUrl, + item: items, + }; +} diff --git a/lib/routes/thegradient/namespace.ts b/lib/routes/thegradient/namespace.ts new file mode 100644 index 00000000000000..fa3d32ebc51e9a --- /dev/null +++ b/lib/routes/thegradient/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'The Gradient', + url: 'thegradient.pub', + lang: 'en', +}; diff --git a/lib/routes/thehindu/namespace.ts b/lib/routes/thehindu/namespace.ts index bc44ae8634a0a0..d40e034df84487 100644 --- a/lib/routes/thehindu/namespace.ts +++ b/lib/routes/thehindu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The Hindu', url: 'thehindu.com', + lang: 'en', }; diff --git a/lib/routes/theinitium/app.ts b/lib/routes/theinitium/app.ts new file mode 100644 index 00000000000000..f5b5bdcf19aeab --- /dev/null +++ b/lib/routes/theinitium/app.ts @@ -0,0 +1,192 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load, type CheerioAPI, type Element } from 'cheerio'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { config } from '@/config'; +import { getCurrentPath } from '@/utils/helpers'; + +const __dirname = getCurrentPath(import.meta.url); +const appUrl = 'https://app.theinitium.com/'; +const userAgent = 'PugpigBolt v4.1.8 (iPhone, iOS 18.2.1) on phone (model iPhone15,2)'; + +export const route: Route = { + path: '/app/:category?', + categories: ['new-media', 'popular'], + example: '/theinitium/app', + parameters: { + category: 'Category, see below, latest_sc by default', + }, + features: { + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'App', + maintainers: ['quiniapiezoelectricity'], + radar: [ + { + source: ['app.theinitium.com/t/latest/:category'], + target: '/app/:category', + }, + ], + handler, + description: `抓取[The Initium App](https://app.theinitium.com/)的文章列表 + +::: warning +此路由暂不支持登陆认证 +::: + +Category 栏目: + +| ----- | 简体中文 | 繁體中文 | +| ----- | ----------------- | ---------------- | +| 最新 | latest_sc | latest_tc | +| 日报 | daily_brief_sc | daily_brief_tc | +| 速递 | whats_new_sc | whats_new_tc | +| 专题 | report_sc | report_tc | +| 评论 | opinion_sc | opinion_tc | +| 国际 | international_sc | international_tc | +| 大陆 | mainland_sc | mainland_tc | +| 香港 | hongkong_sc | hongkong_tc | +| 台湾 | taiwan_sc | taiwan_tc | +| 播客 | article_audio_sc | article_audio_tc |`, +}; + +const resolveRelativeLink = ($: CheerioAPI, elem: Element, attr: string, appUrl?: string) => { + // code from @/middleware/paratmeter.ts + const $elem = $(elem); + + if (appUrl) { + try { + const oldAttr = $elem.attr(attr); + if (oldAttr) { + // e.g. <video><source src="https://example.com"></video> should leave <video> unchanged + $elem.attr(attr, new URL(oldAttr, appUrl).href); + } + } catch { + // no-empty + } + } +}; + +async function getUA(url: string) { + return await got({ + method: 'get', + url, + headers: { + 'User-Agent': userAgent, + }, + }); +} + +async function fetchAppPage(url: URL) { + const response = await getUA(url.href); + const $ = load(response.data); + // resolve relative links with app.theinitium.com + // code from @/middleware/paratmeter.ts + $('a, area').each((_, elem) => { + resolveRelativeLink($, elem, 'href', appUrl); + // $(elem).attr('rel', 'noreferrer'); // currently no such a need + }); + // https://www.w3schools.com/tags/att_src.asp + $('img, video, audio, source, iframe, embed, track').each((_, elem) => { + resolveRelativeLink($, elem, 'src', appUrl); + $(elem).removeAttr('srcset'); + }); + $('video[poster]').each((_, elem) => { + resolveRelativeLink($, elem, 'poster', appUrl); + }); + const article = $('.pp-article__body'); + article.find('.block-related-articles').remove(); + article.find('figure.wp-block-pullquote').children().unwrap(); + article.find('div.block-explanation-note').wrapInner('<blockquote></blockquote>'); + article.find('div.wp-block-tcc-author-note').wrapInner('<em></em>').after('<hr>'); + article.find('p.has-small-font-size').wrapInner('<small></small>'); + return art(path.join(__dirname, 'templates/description.art'), { + standfirst: $('.pp-header-group__standfirst').html(), + coverImage: $('.pp-media__image').attr('src'), + coverCaption: $('.pp-media__caption').html(), + article: article.html(), + copyright: $('.copyright').html(), + }); +} + +async function fetchWebPage(url: URL) { + const response = await got(url.href); + const $ = load(response.data); + const article = $('.entry-content'); + article.find('.block-related-articles').remove(); + article.find('figure.wp-block-pullquote').children().unwrap(); + article.find('div.block-explanation-note').wrapInner('<blockquote></blockquote>'); + article.find('div.wp-block-tcc-author-note').wrapInner('<em></em>').after('<hr>'); + article.find('p.has-small-font-size').wrapInner('<small></small>'); + return art(path.join(__dirname, 'templates/description.art'), { + standfirst: $('span.caption1').html(), + coverImage: $('.wp-post-image').attr('src'), + coverCaption: $('.image-caption').html(), + article: article.html(), + copyright: $('.entry-copyright').html(), + }); +} + +async function handler(ctx) { + const category = ctx.req.param('category') ?? 'latest_sc'; + + const feeds = await cache.tryGet(new URL('timelines.json', appUrl).href, async () => await getUA(new URL('timelines.json', appUrl).href), config.cache.routeExpire, false); + const metadata = feeds.data.timelines.find((timeline) => timeline.id === category); + const response = await getUA(new URL(metadata.feed, appUrl).href); + const feed = response.data.stories.filter((item) => item.type === 'article'); + + const items = await Promise.all( + feed.map((item) => + cache.tryGet(item.shareurl, async () => { + const url = new URL(item.shareurl); + item.link = url.href; + item.description = item.summary; + item.pubDate = item.published; + item.category = []; + if (item.section) { + item.category = [...item.category, item.section]; + } + if (item.taxonomy) { + if (item.taxonomy.collection_tag) { + item.category = [...item.category, ...item.taxonomy.collection_tag]; + } + if (item.taxonomy.sections) { + item.category = [...item.category, ...item.taxonomy.sections]; + } + } + item.category = [...new Set(item.category)]; + switch (url.hostname) { + case 'app.theinitium.com': + item.description = await fetchAppPage(url); + break; + case 'theinitium.com': + item.description = await fetchWebPage(url); + break; + default: + break; + } + return item; + }) + ) + ); + + let lang = 'zh-hans'; + let name = '端传媒'; + if (metadata.timeline_sets[0] === 'chinese-traditional') { + lang = 'zh-hant'; + name = '端傳媒'; + } + + return { + title: `${name} - ${metadata.title}`, + link: `https://app.theinitium.com/t/latest/${category}/`, + language: lang, + item: items, + }; +} diff --git a/lib/routes/theinitium/author.ts b/lib/routes/theinitium/author.ts index a1d5dbc60055b7..c395397903c886 100644 --- a/lib/routes/theinitium/author.ts +++ b/lib/routes/theinitium/author.ts @@ -19,5 +19,5 @@ export const route: Route = { ], handler, example: '/theinitium/author/ninghuilulu/zh-hans', - categories: ['new-media'], + categories: ['new-media', 'popular'], }; diff --git a/lib/routes/theinitium/channel.ts b/lib/routes/theinitium/channel.ts index b418b09216906b..d87c3d0b05dad9 100644 --- a/lib/routes/theinitium/channel.ts +++ b/lib/routes/theinitium/channel.ts @@ -6,7 +6,7 @@ const handler = (ctx) => processFeed('channel', ctx); export const route: Route = { path: '/channel/:type?/:language?', name: '专题・栏目', - maintainers: ['prnake'], + maintainers: ['prnake', 'mintyfrankie'], parameters: { type: '栏目,缺省为最新', language: '语言,简体`zh-hans`,繁体`zh-hant`,缺省为简体', @@ -19,10 +19,10 @@ export const route: Route = { ], handler, example: '/theinitium/channel/latest/zh-hans', - categories: ['new-media'], + categories: ['new-media', 'popular'], description: `Type 栏目: - | 最新 | 深度 | What’s New | 广场 | 科技 | 风物 | 特约 | ... | - | ------ | ------- | ---------- | ----------------- | ---------- | ------- | -------- | --- | - | latest | feature | news-brief | notes-and-letters | technology | culture | pick_up | ... |`, +| 最新 | 深度 | What’s New | 广场 | 科技 | 风物 | 特约 | ... | +| ------ | ------- | ---------- | ----------------- | ---------- | ------- | -------- | --- | +| latest | feature | news-brief | notes-and-letters | technology | culture | pick_up | ... |`, }; diff --git a/lib/routes/theinitium/follow.ts b/lib/routes/theinitium/follow.ts index 84fd599f3ab193..6c4d1d4d2a36b1 100644 --- a/lib/routes/theinitium/follow.ts +++ b/lib/routes/theinitium/follow.ts @@ -20,7 +20,7 @@ export const route: Route = { handler, example: '/theinitium/author/ninghuilulu/zh-hans', categories: ['new-media'], - description: 'Web 版认证 token 和 iOS 内购回执认证 token 只需选择其一填入即可。你也可选择直接在环境设置中填写明文的用户名和密码', + description: '需填入 Web 版认证 token, 也可选择直接在环境设置中填写明文的用户名和密码', features: { requireConfig: [ { @@ -28,11 +28,6 @@ export const route: Route = { optional: true, description: `端传媒 Web 版认证 token。获取方式:登陆后打开端传媒站内任意页面,打开浏览器开发者工具中 “网络”(Network) 选项卡,筛选 URL 找到任一个地址为 \`api.initium.com\` 开头的请求,点击检查其 “消息头”,在 “请求头” 中找到Authorization字段,将其值复制填入配置即可。你的配置应该形如 \`INITIUM_BEARER_TOKEN: 'Bearer eyJxxxx......xx_U8'\`。使用 token 部署的好处是避免占据登陆设备数的额度,但这个 token 一般有效期为两周,因此只可作临时测试使用。`, }, - { - name: 'INITIUM_IAP_RECEIPT', - optional: true, - description: `端传媒 iOS 版内购回执认证 token。获取方式:登陆后打开端传媒 iOS app 内任意页面,打开抓包工具,筛选 URL 找到任一个地址为 \`api.initium.com\` 开头的请求,点击检查其 “消息头”,在 “请求头” 中找到 \`X-IAP-Receipt\` 字段,将其值复制填入配置即可。你的配置应该形如 \`INITIUM_IAP_RECEIPT: ef81dee9e4e2fe084a0af1ea82da2f7b16e75f756db321618a119fa62b52550e\`。`, - }, { name: 'INITIUM_USERNAME', optional: true, diff --git a/lib/routes/theinitium/namespace.ts b/lib/routes/theinitium/namespace.ts index ee7f8b25b51398..6abfdcba1d00e4 100644 --- a/lib/routes/theinitium/namespace.ts +++ b/lib/routes/theinitium/namespace.ts @@ -5,7 +5,8 @@ export const namespace: Namespace = { url: 'theinitium.com', description: `通过提取文章全文,以提供比官方源更佳的阅读体验。 -:::warning +::: warning 付费内容全文可能需要登陆获取,详情见部署页面的配置模块。 :::`, + lang: 'zh-HK', }; diff --git a/lib/routes/theinitium/tags.ts b/lib/routes/theinitium/tags.ts index fd0beb1a284fe3..386c75d8b5f0d7 100644 --- a/lib/routes/theinitium/tags.ts +++ b/lib/routes/theinitium/tags.ts @@ -19,5 +19,5 @@ export const route: Route = { ], handler, example: '/theinitium/tags/2019_10/zh-hans', - categories: ['new-media'], + categories: ['new-media', 'popular'], }; diff --git a/lib/routes/theinitium/templates/description.art b/lib/routes/theinitium/templates/description.art new file mode 100644 index 00000000000000..6a7a902eac38f5 --- /dev/null +++ b/lib/routes/theinitium/templates/description.art @@ -0,0 +1,17 @@ +{{ if standfirst }} + <blockquote><p><em>{{ standfirst }}</em></p></blockquote> +{{ /if }} +{{ if coverImage}} +<figure> + <img src={{ coverImage }}> + {{ if coverCaption }} + <figcaption>{{ coverCaption }}</figcaption> + {{ /if }} +</figure> +{{ /if }} +{{ if article }} + {{@ article }} +{{ /if }} +{{ if copyright }} + <figure><small>{{@ copyright }}</small></figure> +{{ /if }} \ No newline at end of file diff --git a/lib/routes/theinitium/utils.ts b/lib/routes/theinitium/utils.ts index 3a818fcf316064..5ede1da125b4aa 100644 --- a/lib/routes/theinitium/utils.ts +++ b/lib/routes/theinitium/utils.ts @@ -39,7 +39,6 @@ export const processFeed = async (model: string, ctx: Context) => { const key = { email: config.initium.username, password: config.initium.password, - iapReceipt: config.initium.iap_receipt, }; const body = JSON.stringify(key); @@ -59,25 +58,11 @@ export const processFeed = async (model: string, ctx: Context) => { Accept: 'application/json', Connection: 'keep-alive', Authorization: TOKEN, - Origin: `https://theinitium.com/`, - Referer: `https://theinitium.com/`, - 'X-Client-Name': 'Web', }, body, }); - /* - const devices = login.data.access.devices; - - for (const key in devices) { - const device = devices[key]; - if (device.status === 'logged_in' && !device.logout_at && device.platform === 'web') { - token = 'Bearer ' + device.device_id; - break; - } - } - */ - token = 'Bearer ' + login.data.token; + token = 'token ' + login.data.token; cache.set('initium:token', token); } @@ -85,7 +70,6 @@ export const processFeed = async (model: string, ctx: Context) => { Accept: '*/*', Connection: 'keep-alive', Authorization: token, - 'X-IAP-Receipt': key.iapReceipt || '', }; let response; @@ -154,7 +138,7 @@ export const processFeed = async (model: string, ctx: Context) => { const items = await Promise.all( articles .filter((a) => a.article) - .slice(0, token === TOKEN && key.iapReceipt === undefined ? 25 : articles.length) + .slice(0, token === TOKEN ? 25 : articles.length) .map(async (item) => { item.article.date = parseDate(item.article.date); item.article.updated = parseDate(item.article.updated); diff --git a/lib/routes/themoviedb/namespace.ts b/lib/routes/themoviedb/namespace.ts index 1b74e3691c5311..87231101a7a4e1 100644 --- a/lib/routes/themoviedb/namespace.ts +++ b/lib/routes/themoviedb/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The Movie Database', url: 'themoviedb.org', - description: `:::tip + description: `::: tip Refer to [https://developers.themoviedb.org/3/getting-started/languages](https://developers.themoviedb.org/3/getting-started/languages) for the language parameter in the route. :::`, + lang: 'en', }; diff --git a/lib/routes/themoviedb/seasons.ts b/lib/routes/themoviedb/seasons.ts index 0de7c819561974..b5e82f5009d411 100644 --- a/lib/routes/themoviedb/seasons.ts +++ b/lib/routes/themoviedb/seasons.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; import apiKey from './api-key'; @@ -6,7 +6,8 @@ import { handleDescription } from './utils'; export const route: Route = { path: '/tv/:id/seasons/:lang?', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Notifications, example: '/themoviedb/tv/70593/seasons/en-US', parameters: { id: 'TV show ID', lang: 'Language' }, features: { diff --git a/lib/routes/themoviedb/sheet.ts b/lib/routes/themoviedb/sheet.ts index 032df08bcb6cad..9f30be13b6f156 100644 --- a/lib/routes/themoviedb/sheet.ts +++ b/lib/routes/themoviedb/sheet.ts @@ -47,15 +47,15 @@ export const route: Route = { handler, description: `When \`mediaType\` is \`tv\`, \`sheet\` should be: - | Airing Today | On TV | Top Rated | - | ------------ | ---------- | --------- | - | airing-today | on-the-air | top-rated | +| Airing Today | On TV | Top Rated | +| ------------ | ---------- | --------- | +| airing-today | on-the-air | top-rated | When \`mediaType\` is \`movie\`, \`sheet\` should be: - | Now Playing | Upcoming | Top Rated | - | ----------- | -------- | --------- | - | now-playing | upcoming | top-rated |`, +| Now Playing | Upcoming | Top Rated | +| ----------- | -------- | --------- | +| now-playing | upcoming | top-rated |`, }; async function handler(ctx) { diff --git a/lib/routes/thenewslens/namespace.ts b/lib/routes/thenewslens/namespace.ts index 5d9f3c0bd1cad5..d39bf23674ef8e 100644 --- a/lib/routes/thenewslens/namespace.ts +++ b/lib/routes/thenewslens/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The News Lens 關鍵評論', url: 'thenewslens.com', + lang: 'zh-TW', }; diff --git a/lib/routes/thepaper/839studio/category.ts b/lib/routes/thepaper/839studio/category.ts index d742b93b819e62..5ac38f41122dd4 100644 --- a/lib/routes/thepaper/839studio/category.ts +++ b/lib/routes/thepaper/839studio/category.ts @@ -9,7 +9,7 @@ export const route: Route = { source: ['thepaper.cn/'], }, ], - name: 'Unknown', + name: '澎湃美数课作品集 - 分类', maintainers: ['umm233'], handler, url: 'thepaper.cn/', diff --git a/lib/routes/thepaper/839studio/studio.ts b/lib/routes/thepaper/839studio/studio.ts index eba04c4450add7..9cab082d4d57bc 100644 --- a/lib/routes/thepaper/839studio/studio.ts +++ b/lib/routes/thepaper/839studio/studio.ts @@ -4,7 +4,7 @@ import { load } from 'cheerio'; export const route: Route = { path: '/839studio', - name: 'Unknown', + name: '澎湃美数课作品集', maintainers: ['umm233'], handler, }; diff --git a/lib/routes/thepaper/channel.ts b/lib/routes/thepaper/channel.ts index 96fccb26d01aa1..84ff03e9c1c6a3 100644 --- a/lib/routes/thepaper/channel.ts +++ b/lib/routes/thepaper/channel.ts @@ -5,7 +5,7 @@ import got from '@/utils/got'; export const route: Route = { path: '/channel/:id', - categories: ['traditional-media'], + categories: ['new-media', 'popular'], example: '/thepaper/channel/25950', parameters: { id: '频道 id,可在频道页 URL 中找到' }, features: { @@ -20,20 +20,20 @@ export const route: Route = { maintainers: ['xyqfer', 'nczitzk', 'bigfei'], handler, description: `| 频道 ID | 频道名 | - | ------- | ------ | - | 26916 | 视频 | - | 108856 | 战疫 | - | 25950 | 时事 | - | 25951 | 财经 | - | 36079 | 澎湃号 | - | 119908 | 科技 | - | 25952 | 思想 | - | 119489 | 智库 | - | 25953 | 生活 | - | 26161 | 问吧 | - | 122908 | 国际 | - | -21 | 体育 | - | -24 | 评论 |`, +| ------- | ------ | +| 26916 | 视频 | +| 108856 | 战疫 | +| 25950 | 时事 | +| 25951 | 财经 | +| 36079 | 澎湃号 | +| 119908 | 科技 | +| 25952 | 思想 | +| 119489 | 智库 | +| 25953 | 生活 | +| 26161 | 问吧 | +| 122908 | 国际 | +| -21 | 体育 | +| -24 | 评论 |`, }; async function handler(ctx) { diff --git a/lib/routes/thepaper/factpaper.ts b/lib/routes/thepaper/factpaper.ts index 44639088b07e04..e2d59e34f44bad 100644 --- a/lib/routes/thepaper/factpaper.ts +++ b/lib/routes/thepaper/factpaper.ts @@ -11,7 +11,7 @@ import path from 'node:path'; export const route: Route = { path: '/factpaper/:status?', - categories: ['traditional-media'], + categories: ['new-media', 'popular'], example: '/thepaper/factpaper', parameters: { status: '状态 id,可选 `1` 即 有定论 或 `0` 即 核查中,默认为 `1`' }, features: { diff --git a/lib/routes/thepaper/featured.ts b/lib/routes/thepaper/featured.ts index b660d38f4d62a7..1dc10211619d0a 100644 --- a/lib/routes/thepaper/featured.ts +++ b/lib/routes/thepaper/featured.ts @@ -5,7 +5,7 @@ import got from '@/utils/got'; export const route: Route = { path: '/featured', - categories: ['traditional-media'], + categories: ['new-media', 'popular'], example: '/thepaper/featured', parameters: {}, features: { diff --git a/lib/routes/thepaper/list.ts b/lib/routes/thepaper/list.ts index 16dd6f74951319..9bcf0ddeb523af 100644 --- a/lib/routes/thepaper/list.ts +++ b/lib/routes/thepaper/list.ts @@ -5,7 +5,7 @@ import got from '@/utils/got'; export const route: Route = { path: '/list/:id', - categories: ['traditional-media'], + categories: ['new-media', 'popular'], example: '/thepaper/list/25457', parameters: { id: '栏目 id,可在栏目页 URL 中找到' }, features: { @@ -20,104 +20,104 @@ export const route: Route = { maintainers: ['nczitzk', 'bigfei'], handler, description: `| 栏目 ID | 栏目名 | - | ------- | ------------ | - | 26912 | 上直播 | - | 26913 | 七环视频 | - | 26965 | 温度计 | - | 26908 | 一级视场 | - | 27260 | World 湃 | - | 26907 | 湃客科技 | - | 33168 | 纪录湃 | - | 26911 | 围观 | - | 26918 | @所有人 | - | 26906 | 大都会 | - | 26909 | 追光灯 | - | 26910 | 运动装 | - | 26914 | 健寻记 | - | 82188 | AI 播报 | - | 89035 | 眼界 | - | 92278 | 关键帧 | - | 90069 | 战疫 | - | 25462 | 中国政库 | - | 25488 | 中南海 | - | 97924 | 初心之路 | - | 25489 | 舆论场 | - | 25490 | 打虎记 | - | 25423 | 人事风向 | - | 25426 | 法治中国 | - | 25424 | 一号专案 | - | 25463 | 港台来信 | - | 25491 | 长三角政商 | - | 25428 | 直击现场 | - | 68750 | 公益湃 | - | 27604 | 暖闻 | - | 25464 | 澎湃质量报告 | - | 25425 | 绿政公署 | - | 25429 | 澎湃国际 | - | 25481 | 外交学人 | - | 25430 | 澎湃防务 | - | 25678 | 唐人街 | - | 25427 | 澎湃人物 | - | 25422 | 浦江头条 | - | 25487 | 教育家 | - | 25634 | 全景现场 | - | 25635 | 美数课 | - | 25600 | 快看 | - | 25434 | 10% 公司 | - | 25436 | 能见度 | - | 25433 | 地产界 | - | 25438 | 财经上下游 | - | 25435 | 金改实验室 | - | 25437 | 牛市点线面 | - | 119963 | IPO 最前线 | - | 25485 | 澎湃商学院 | - | 25432 | 自贸区连线 | - | 37978 | 进博会在线 | - | 36079 | 湃客 | - | 27392 | 政务 | - | 77286 | 媒体 | - | 27234 | 科学湃 | - | 119445 | 生命科学 | - | 119447 | 未来 2% | - | 119446 | 元宇宙观察 | - | 119448 | 科创 101 | - | 119449 | 科学城邦 | - | 25444 | 社论 | - | 27224 | 澎湃评论 | - | 26525 | 思想湃 | - | 26878 | 上海书评 | - | 25483 | 思想市场 | - | 25457 | 私家历史 | - | 25574 | 翻书党 | - | 25455 | 艺术评论 | - | 26937 | 古代艺术 | - | 25450 | 文化课 | - | 25482 | 逝者 | - | 25536 | 专栏 | - | 26506 | 异次元 | - | 97313 | 海平面 | - | 103076 | 一问三知 | - | 25445 | 澎湃研究所 | - | 25446 | 全球智库 | - | 26915 | 城市漫步 | - | 25456 | 市政厅 | - | 104191 | 世界会客厅 | - | 25448 | 有戏 | - | 26609 | 文艺范 | - | 25942 | 身体 | - | 26015 | 私・奔 | - | 25599 | 运动家 | - | 25842 | 私家地理 | - | 80623 | 非常品 | - | 26862 | 楼市 | - | 25769 | 生活方式 | - | 25990 | 澎湃联播 | - | 26173 | 视界 | - | 26202 | 亲子学堂 | - | 26404 | 赢家 | - | 26490 | 汽车圈 | - | 115327 | IP SH | - | 117340 | 酒业 |`, +| ------- | ------------ | +| 26912 | 上直播 | +| 26913 | 七环视频 | +| 26965 | 温度计 | +| 26908 | 一级视场 | +| 27260 | World 湃 | +| 26907 | 湃客科技 | +| 33168 | 纪录湃 | +| 26911 | 围观 | +| 26918 | @所有人 | +| 26906 | 大都会 | +| 26909 | 追光灯 | +| 26910 | 运动装 | +| 26914 | 健寻记 | +| 82188 | AI 播报 | +| 89035 | 眼界 | +| 92278 | 关键帧 | +| 90069 | 战疫 | +| 25462 | 中国政库 | +| 25488 | 中南海 | +| 97924 | 初心之路 | +| 25489 | 舆论场 | +| 25490 | 打虎记 | +| 25423 | 人事风向 | +| 25426 | 法治中国 | +| 25424 | 一号专案 | +| 25463 | 港台来信 | +| 25491 | 长三角政商 | +| 25428 | 直击现场 | +| 68750 | 公益湃 | +| 27604 | 暖闻 | +| 25464 | 澎湃质量报告 | +| 25425 | 绿政公署 | +| 25429 | 澎湃国际 | +| 25481 | 外交学人 | +| 25430 | 澎湃防务 | +| 25678 | 唐人街 | +| 25427 | 澎湃人物 | +| 25422 | 浦江头条 | +| 25487 | 教育家 | +| 25634 | 全景现场 | +| 25635 | 美数课 | +| 25600 | 快看 | +| 25434 | 10% 公司 | +| 25436 | 能见度 | +| 25433 | 地产界 | +| 25438 | 财经上下游 | +| 25435 | 金改实验室 | +| 25437 | 牛市点线面 | +| 119963 | IPO 最前线 | +| 25485 | 澎湃商学院 | +| 25432 | 自贸区连线 | +| 37978 | 进博会在线 | +| 36079 | 湃客 | +| 27392 | 政务 | +| 77286 | 媒体 | +| 27234 | 科学湃 | +| 119445 | 生命科学 | +| 119447 | 未来 2% | +| 119446 | 元宇宙观察 | +| 119448 | 科创 101 | +| 119449 | 科学城邦 | +| 25444 | 社论 | +| 27224 | 澎湃评论 | +| 26525 | 思想湃 | +| 26878 | 上海书评 | +| 25483 | 思想市场 | +| 25457 | 私家历史 | +| 25574 | 翻书党 | +| 25455 | 艺术评论 | +| 26937 | 古代艺术 | +| 25450 | 文化课 | +| 25482 | 逝者 | +| 25536 | 专栏 | +| 26506 | 异次元 | +| 97313 | 海平面 | +| 103076 | 一问三知 | +| 25445 | 澎湃研究所 | +| 25446 | 全球智库 | +| 26915 | 城市漫步 | +| 25456 | 市政厅 | +| 104191 | 世界会客厅 | +| 25448 | 有戏 | +| 26609 | 文艺范 | +| 25942 | 身体 | +| 26015 | 私・奔 | +| 25599 | 运动家 | +| 25842 | 私家地理 | +| 80623 | 非常品 | +| 26862 | 楼市 | +| 25769 | 生活方式 | +| 25990 | 澎湃联播 | +| 26173 | 视界 | +| 26202 | 亲子学堂 | +| 26404 | 赢家 | +| 26490 | 汽车圈 | +| 115327 | IP SH | +| 117340 | 酒业 |`, }; async function handler(ctx) { diff --git a/lib/routes/thepaper/namespace.ts b/lib/routes/thepaper/namespace.ts index 504d2f77e45f91..a0cda4243511f8 100644 --- a/lib/routes/thepaper/namespace.ts +++ b/lib/routes/thepaper/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: '澎湃新闻', url: 'thepaper.cn', description: `以下所有路由可使用参数\`old\`以采取旧全文获取方法。该方法会另外获取网页中的图片与视频资源。在原始 url 追加\`?old=yes\`以启用.`, + lang: 'zh-CN', }; diff --git a/lib/routes/thepaper/sidebar.ts b/lib/routes/thepaper/sidebar.ts index 9a82e8e8ee28ec..ffe3bf554a23a4 100644 --- a/lib/routes/thepaper/sidebar.ts +++ b/lib/routes/thepaper/sidebar.ts @@ -16,7 +16,10 @@ export const route: Route = { target: '/sidebar', }, ], - name: 'Unknown', + name: '侧边栏', + categories: ['new-media', 'popular'], + example: '/thepaper/sidebar', + parameters: { sec: '侧边栏 id,可选 `hotNews` 即 澎湃热榜、`financialInformationNews` 即 澎湃财讯、`morningEveningNews` 即 早晚报,默认为 `hotNews`' }, maintainers: ['bigfei'], handler, url: 'thepaper.cn/', diff --git a/lib/routes/thepaper/user.ts b/lib/routes/thepaper/user.ts new file mode 100644 index 00000000000000..18e3a5d3008034 --- /dev/null +++ b/lib/routes/thepaper/user.ts @@ -0,0 +1,174 @@ +import { Route } from '@/types'; +import * as cheerio from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/user/:pphId', + categories: ['new-media', 'popular'], + example: '/thepaper/user/4221423', + parameters: { pphId: '澎湃号 id,可在澎湃号页 URL 中找到' }, + name: '澎湃号', + maintainers: ['TonyRL'], + handler, +}; + +interface AuthorInfo { + userId: number; + sname: string; + pic: string; + isAuth: string; + userType: string; + perDesc: string; + mobile: null; + isOrder: string; + sex: string; + area: string; + attentionNum: string; + fansNum: string; + praiseNum: null; + pph: boolean; + normalUser: boolean; + mobForwardType: number; + authInfo: string; + mail: string; + pphImpactNum: string; + wonderfulCommentCount: string; + location: string; + lastLoginDate: null; +} + +interface PPHContentResponse { + code: number; + data: Data; + desc: string; + time: number; +} + +interface Data { + hasNext: boolean; + startTime: number; + list: ListItem[]; + nodeInfo: null; + tagInfo: null; + moreNodeInfo: null; + pageNum: null; + pageSize: null; + pages: null; + total: null; + prevPageNum: null; + nextPageNum: null; + excludeContIds: null; + contCont: null; + filterIds: null; + updateCount: null; +} + +interface ListItem { + contId: string; + isOutForword: string; + isOutForward: string; + forwardType: string; + mobForwardType: number; + interactionNum: string; + praiseTimes: string; + pic: string; + imgCardMode: number; + smallPic: string; + sharePic: string; + pubTime: string; + pubTimeNew: string; + name: string; + closePraise: string; + authorInfo: AuthorInfo; + nodeId: number; + contType: number; + pubTimeLong: number; + specialNodeId: number; + cardMode: string; + dataObjId: number; + closeFrontComment: boolean; + isSupInteraction: boolean; + hideVideoFlag: boolean; + praiseStyle: number; + isSustainedFly: number; + softLocType: number; + closeComment: boolean; + voiceInfo: VoiceInfo; + softAdTypeStr: string; +} + +interface VoiceInfo { + imgSrc: string; + isHaveVoice: string; +} + +async function handler(ctx) { + const { pphId } = ctx.req.param(); + + const mobileBuildId = (await cache.tryGet('thepaper:m:buildId', async () => { + const response = await ofetch('https://m.thepaper.cn'); + const $ = cheerio.load(response); + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + return nextData.buildId; + })) as string; + + const userInfo = (await cache.tryGet(`thepaper:user:${pphId}`, async () => { + const response = await ofetch(`https://api.thepaper.cn/userservice/user/homePage/${pphId}`, { + headers: { + 'Client-Type': '2', + Origin: 'https://m.thepaper.cn', + Referer: 'https://m.thepaper.cn/', + }, + }); + return response.userInfo; + })) as AuthorInfo; + + const response = await ofetch<PPHContentResponse>('https://api.thepaper.cn/contentapi/cont/pph/user', { + method: 'POST', + body: { + pageSize: 10, + pageNum: 1, + contType: 0, + excludeContIds: [], + pphId, + startTime: 0, + }, + }); + + const list = response.data.list.map((item) => ({ + title: item.name, + link: `https://www.thepaper.cn/newsDetail_forward_${item.contId}`, + pubDate: parseDate(item.pubTimeLong), + author: item.authorInfo.sname, + contId: item.contId, + image: item.pic, + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(`https://m.thepaper.cn/_next/data/${mobileBuildId}/detail/${item.contId}.json`, { + query: { + id: item.contId, + }, + }); + + item.description = response.pageProps.detailData.contentDetail.content; + item.updated = parseDate(response.pageProps.detailData.contentDetail.updateTime); + + return item; + }) + ) + ); + + return { + title: userInfo.sname, + description: userInfo.perDesc, + link: `https://www.thepaper.cn/user_${pphId}`, + item: items, + itunes_author: userInfo.sname, + image: userInfo.pic, + }; +} diff --git a/lib/routes/thepetcity/index.ts b/lib/routes/thepetcity/index.ts index fb43599da5a5ae..c191140c60d7de 100644 --- a/lib/routes/thepetcity/index.ts +++ b/lib/routes/thepetcity/index.ts @@ -8,7 +8,7 @@ const baseUrl = 'https://thepetcity.co'; export const route: Route = { path: '/:term?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/thepetcity', parameters: { term: '見下表,留空為全部文章' }, radar: Object.entries(termsMap).map(([key, value]) => ({ @@ -21,12 +21,12 @@ export const route: Route = { handler, url: 'thepetcity.co/', description: `| Column Name | TermID | - | -------------------- | ------ | - | Knowledge飼養大全 | 3 | - | Funny News毛孩趣聞 | 2 | - | Raise Pets 養寵物新手 | 5 | - | Hot Spot 毛孩打卡點 | 4 | - | Pet Staff 毛孩好物 | 1 |`, +| -------------------- | ------ | +| Knowledge飼養大全 | 3 | +| Funny News毛孩趣聞 | 2 | +| Raise Pets 養寵物新手 | 5 | +| Hot Spot 毛孩打卡點 | 4 | +| Pet Staff 毛孩好物 | 1 |`, }; async function handler(ctx) { diff --git a/lib/routes/thepetcity/namespace.ts b/lib/routes/thepetcity/namespace.ts index 83114d6e092cf6..16789d207a6055 100644 --- a/lib/routes/thepetcity/namespace.ts +++ b/lib/routes/thepetcity/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'PetCity 毛孩日常', - url: 'thepetcity.com', + url: 'thepetcity.co', + lang: 'zh-TW', }; diff --git a/lib/routes/theverge/index.ts b/lib/routes/theverge/index.ts index a659a25aebaf57..6d9a6b725ed6d1 100644 --- a/lib/routes/theverge/index.ts +++ b/lib/routes/theverge/index.ts @@ -1,12 +1,17 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import parser from '@/utils/rss-parser'; import { load } from 'cheerio'; +import path from 'node:path'; +import { art } from '@/utils/render'; +import { getCurrentPath } from '@/utils/helpers'; + +const __dirname = getCurrentPath(import.meta.url); export const route: Route = { path: '/:hub?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/theverge', parameters: { hub: 'Hub, see below, All Posts by default' }, features: { @@ -22,27 +27,58 @@ export const route: Route = { source: ['theverge.com/:hub', 'theverge.com/'], }, ], - name: 'The Verge', + name: 'Category', maintainers: ['HenryQW', 'vbali'], handler, description: `| Hub | Hub name | - | ----------- | ------------------- | - | | All Posts | - | android | Android | - | apple | Apple | - | apps | Apps & Software | - | blackberry | BlackBerry | - | culture | Culture | - | gaming | Gaming | - | hd | HD & Home | - | microsoft | Microsoft | - | photography | Photography & Video | - | policy | Policy & Law | - | web | Web & Social | +| ----------- | ------------------- | +| | All Posts | +| android | Android | +| apple | Apple | +| apps | Apps & Software | +| blackberry | BlackBerry | +| culture | Culture | +| gaming | Gaming | +| hd | HD & Home | +| microsoft | Microsoft | +| photography | Photography & Video | +| policy | Policy & Law | +| web | Web & Social | Provides a better reading experience (full text articles) over the official one.`, }; +const renderBlock = (b) => { + switch (b.__typename) { + case 'CoreEmbedBlockType': + return b.embedHtml; + case 'CoreGalleryBlockType': + return b.images.map((i) => `<figure><img src="${i.image.thumbnails.horizontal.url.split('?')[0]}" alt="${i.alt}" /><figcaption>${i.caption.html}</figcaption></figure>`).join(''); + case 'CoreHeadingBlockType': + return `<h${b.level}>${b.contents.html}</h${b.level}>`; + case 'CoreHTMLBlockType': + return b.markup; + case 'CoreImageBlockType': + return `<figure><img src="${b.thumbnail.url.split('?')[0]}" alt="${b.alt}" /><figcaption>${b.caption.html}</figcaption></figure>`; + case 'CoreListBlockType': + return `${b.ordered ? '<ol>' : '<ul>'}${b.items.map((i) => `<li>${i.contents.html}</li>`).join('')}${b.ordered ? '</ol>' : '</ul>'}`; + case 'CoreParagraphBlockType': + return b.contents.html; + case 'CorePullquoteBlockType': + return `<blockquote>${b.contents.html}</blockquote>`; + case 'CoreQuoteBlockType': + return `<blockquote>${b.children.map((child) => renderBlock(child)).join('')}</blockquote>`; + case 'CoreSeparatorBlockType': + return '<hr>'; + case 'HighlightBlockType': + return b.children.map((c) => renderBlock(c)).join(''); + case 'MethodologyAccordionBlockType': + return `<h2>${b.heading.html}</h2>${b.sections.map((s) => `<h3>${s.heading.html}</h3>${s.content.html}`).join('')}`; + default: + throw new Error(`Unsupported block type: ${b.__typename}`); + } +}; + async function handler(ctx) { const link = ctx.req.param('hub') ? `https://www.theverge.com/${ctx.req.param('hub')}/rss/index.xml` : 'https://www.theverge.com/rss/index.xml'; @@ -51,73 +87,48 @@ async function handler(ctx) { const items = await Promise.all( feed.items.map((item) => cache.tryGet(item.link, async () => { - const response = await got(item.link); - - const $ = load(response.data); - - const content = $('#content'); - const body = $('.duet--article--article-body-component-container'); - - // 处理封面图片 - - const cover = $('meta[property="og:image"]'); - - if (cover.length > 0) { - $(`<img src=${cover[0].attribs.content}>`).insertBefore(body[0].childNodes[0]); - } + const response = await ofetch(item.link); - // 处理封面视频 - $('div.l-col__main > div.c-video-embed, div.c-entry-hero > div.c-video-embed').each((i, e) => { - const src = `https://volume.vox-cdn.com/embed/${e.attribs['data-volume-uuid']}?autoplay=false`; + const $ = load(response); - $(`<iframe src="${src}" style="border: 0; top: 0; left: 0; width: 100%; height: 100%; position: absolute;" allowfullscreen scrolling="no"></iframe>`).insertBefore(body[0].childNodes[0]); - }); + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + const node = nextData.props.pageProps.hydration.responses.find((x) => x.operationName === 'PostLayoutQuery' || x.operationName === 'StreamLayoutQuery').data.node; - // 处理封面视频 - $('div.l-col__main > div.c-video-embed--media iframe').each((i, e) => { - $(e).insertBefore(body[0].childNodes[0]); + let description = art(path.join(__dirname, 'templates/header.art'), { + featuredImage: node.featuredImage, + ledeMediaData: node.ledeMediaData, }); - // 处理文章图片 - content.find('figure.e-image').each((i, e) => { - let src, caption; - - // 处理 jpeg, png - if ($(e).find('picture > source').length > 0) { - src = $(e) - .find('picture > img')[0] - .attribs.srcset.match(/(?<=320w,).*?(?=520w)/g)[0] - .trim(); - } else if ($(e).find('img.c-dynamic-image').length > 0) { - // 处理 gif - src = $(e).find('span.e-image__image')[0].attribs['data-original']; - } - - // 处理 caption - if ($(e).find('span.e-image__meta').length > 0) { - caption = $(e).find('span.e-image__meta').text(); - } - - const figure = `<figure><img src=${src}>${caption ? `<br><figcaption>${caption}</figcaption>` : ''}</figure>`; - - $(figure).insertBefore(e); - - $(e).remove(); - }); - - const lede = $('.duet--article--lede h2:first'); - if (lede[0]) { - lede.insertBefore(body[0].childNodes[0]); + description += node.blocks + .filter((b) => b.__typename !== 'NewsletterBlockType' && b.__typename !== 'RelatedPostsBlockType' && b.__typename !== 'ProductBlockType' && b.__typename !== 'TableOfContentsBlockType') + .map((b) => renderBlock(b)) + .join('<br><br>'); + + if (node.__typename === 'StreamResourceType') { + description += node.posts.edges + .map(({ node: n }) => { + let d = + `<h2><a href="${n.permalink}">${n.promo.headline || n.title}</a></h2>` + + art(path.join(__dirname, 'templates/header.art'), { + ledeMediaData: n.ledeMediaData, + }); + switch (n.__typename) { + case 'PostResourceType': + d += n.excerpt.map((e) => e.contents.html).join('<br>'); + break; + case 'QuickPostResourceType': + d += n.blocks.map((b) => renderBlock(b)).join('<br>'); + break; + default: + break; + } + return d; + }) + .join('<br>'); } - // 移除无用 DOM - content.find('.duet--article--comments-join-the-conversation').remove(); - content.find('.duet--recirculation--related-list').remove(); - delete item.content; - delete item.contentSnippet; - delete item.isoDate; - - item.description = body.html(); + item.description = description; + item.category = node.categories; return item; }) diff --git a/lib/routes/theverge/namespace.ts b/lib/routes/theverge/namespace.ts index 6bc9fe21aa2a85..86f56a30d9d70c 100644 --- a/lib/routes/theverge/namespace.ts +++ b/lib/routes/theverge/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The Verge', url: 'theverge.com', + lang: 'en', }; diff --git a/lib/routes/theverge/templates/header.art b/lib/routes/theverge/templates/header.art new file mode 100644 index 00000000000000..e57a1dbf595134 --- /dev/null +++ b/lib/routes/theverge/templates/header.art @@ -0,0 +1,19 @@ +{{ if featuredImage }} + <figure> + <img src="{{ featuredImage.image.originalUrl.split('?')[0] }}" alt="{{ featuredImage.image.originalUrl.alt }}"> + <figcaption>{{ featuredImage.image.title }}</figcaption> + </figure> +{{ /if }} + +{{ if ledeMediaData }} + {{ if ledeMediaData.__typename === 'LedeMediaEmbedType'}} + {{@ ledeMediaData.embedHtml }} + {{ else if ledeMediaData.__typename === 'LedeMediaImageType' && !featuredImage }} + <figure> + <img src="{{ ledeMediaData.image.thumbnails.horizontal.url.split('?')[0] }}" alt="{{ ledeMediaData.image.title }}"> + <figcaption>{{ ledeMediaData.image.credit.plaintext || ledeMediaData.image.title }}</figcaption> + </figure> + {{ else if ledeMediaData.__typename === 'LedeMediaVideoType'}} + <iframe src="https://volume.vox-cdn.com/embed/{{ ledeMediaData.video.volumeUuid }}" allowfullscreen></iframe> + {{ /if }} +{{ /if }} diff --git a/lib/routes/thoughtco/index.ts b/lib/routes/thoughtco/index.ts index bea5e6cc57caa2..39dc18e4097029 100644 --- a/lib/routes/thoughtco/index.ts +++ b/lib/routes/thoughtco/index.ts @@ -27,299 +27,299 @@ export const route: Route = { handler, description: `#### Science, Tech, Math - | category | id | - | ---------------- | -------------------------- | - | Science | science-4132464 | - | Math | math-4133545 | - | Social Sciences | social-sciences-4133522 | - | Computer Science | computer-science-4133486 | - | Animals & Nature | animals-and-nature-4133421 | - - #### Humanities - - | category | id | - | ----------------- | --------------------------- | - | History & Culture | history-and-culture-4133356 | - | Visual Arts | visual-arts-4132957 | - | Literature | literature-4133251 | - | English | english-4688281 | - | Geography | geography-4133035 | - | Philosophy | philosophy-4133025 | - | Issues | issues-4133022 | - - #### Languages - - | category | id | - | ---------------------------- | ---------------- | - | English as a Second Language | esl-4133095 | - | Spanish | spanish-4133085 | - | French | french-4133079 | - | German | german-4133073 | - | Italian | italian-4133069 | - | Japanese | japanese-4133062 | - | Mandarin | mandarin-4133057 | - | Russian | russian-4175265 | - - #### Resources - - | category | id | - | ---------------------- | ---------------------------- | - | For Students & Parents | for-students-parents-4132588 | - | For Educators | for-educators-4132509 | - | For Adult Learners | for-adult-learners-4132469 | - - <details> - <summary>More categories</summary> - - #### Science - - | category | id | - | ----------------- | --------------------------- | - | Chemistry | chemistry-4133594 | - | Biology | biology-4133580 | - | Physics | physics-4133571 | - | Geology | geology-4133564 | - | Astronomy | astronomy-4133558 | - | Weather & Climate | weather-and-climate-4133550 | - - #### Math - - | category | id | - | --------------------- | ------------------------------- | - | Math Tutorials | math-tutorials-4133543 | - | Geometry | geometry-4133540 | - | Arithmetic | arithmetic-4133542 | - | Pre Algebra & Algebra | pre-algebra-and-algebra-4133541 | - | Statistics | statistics-4133539 | - | Exponential Decay | exponential-decay-4133528 | - | Worksheets By Grade | worksheets-by-grade-4133526 | - | Resources | math-resources-4133523 | - - #### Social Sciences - - | category | id | - | ----------- | ------------------- | - | Psychology | psychology-4160512 | - | Sociology | sociology-4133515 | - | Archaeology | archaeology-4133504 | - | Economics | economics-4133521 | - | Ergonomics | ergonomics-4133492 | - - #### Computer Science - - | category | id | - | ---------------------- | -------------------------------- | - | PHP Programming | php-4133485 | - | Perl | perl-4133481 | - | Python | python-4133477 | - | Java Programming | java-programming-4133478 | - | Javascript Programming | javascript-programming-4133476 | - | Delphi Programming | delphi-programming-4133475 | - | C & C++ Programming | c-and-c-plus-programming-4133470 | - | Ruby Programming | ruby-programming-4133469 | - | Visual Basic | visual-basic-4133468 | - - #### Animals and Nature - - | category | id | - | ---------------- | ------------------------ | - | Amphibians | amphibians-4133418 | - | Birds | birds-4133416 | - | Habitat Profiles | habitat-profiles-4133412 | - | Mammals | mammals-4133411 | - | Reptiles | reptiles-4133408 | - | Insects | insects-4133406 | - | Marine Life | marine-life-4133393 | - | Forestry | forestry-4133386 | - | Dinosaurs | dinosaurs-4133376 | - | Evolution | evolution-4133366 | - - #### History and Culture - - | category | id | - | ------------------------------ | ---------------------------------------- | - | American History | american-history-4133354 | - | African American History | african-american-history-4133344 | - | African History | african-history-4133338 | - | Ancient History and Culture | ancient-history-4133336 | - | Asian History | asian-history-4133325 | - | European History | european-history-4133316 | - | Genealogy | genealogy-4133308 | - | Inventions | inventions-4133303 | - | Latin American History | latin-american-history-4133296 | - | Medieval & Renaissance History | medieval-and-renaissance-history-4133289 | - | Military History | military-history-4133285 | - | The 20th Century | 20th-century-4133273 | - | Women's History | womens-history-4133260 | - - #### Visual Arts - - | category | id | - | ------------- | -------------------- | - | Art & Artists | art-4132956 | - | Architecture | architecture-4132953 | - - #### Literature - - | category | id | - | ------------------ | -------------------------- | - | Best Sellers | best-sellers-4133250 | - | Classic Literature | classic-literature-4133245 | - | Plays & Drama | plays-and-drama-4133239 | - | Poetry | poetry-4133232 | - | Quotations | quotations-4133229 | - | Shakespeare | shakespeare-4133223 | - | Short Stories | short-stories-4133217 | - | Children's Books | childrens-books-4133216 | - - #### English - - | category | id | - | --------------- | ----------------------- | - | English Grammar | english-grammar-4133049 | - | Writing | writing-4133048 | - - #### Geography - - | category | id | - | ------------------------ | ---------------------------------- | - | Basics | geography-basics-4133034 | - | Physical Geography | physical-geography-4133032 | - | Political Geography | political-geography-4133033 | - | Population | population-4133031 | - | Country Information | country-information-4133030 | - | Key Figures & Milestones | key-figures-and-milestones-4133029 | - | Maps | maps-4133027 | - | Urban Geography | urban-geography-4133026 | - - #### Philosophy - - | category | id | - | ------------------------------ | ---------------------------------------- | - | Philosophical Theories & Ideas | philosophical-theories-and-ideas-4133024 | - | Major Philosophers | major-philosophers-4133023 | - - #### Issues - - | category | id | - | --------------------------------- | -------------------------------- | - | The U. S. Government | us-government-4133021 | - | U.S. Foreign Policy | us-foreign-policy-4133010 | - | U.S. Liberal Politics | us-liberal-politics-4133009 | - | U.S. Conservative Politics | us-conservative-politics-4133006 | - | Women's Issues | womens-issues-4133002 | - | Civil Liberties | civil-liberties-4132996 | - | The Middle East | middle-east-4132989 | - | Race Relations | race-relations-4132982 | - | Immigration | immigration-4132977 | - | Crime & Punishment | crime-and-punishment-4132972 | - | Canadian Government | canadian-government-4132959 | - | Understanding Types of Government | types-of-government-5179107 | - - #### English as a Second Language - - | category | id | - | ---------------------------- | ------------------------------------------ | - | Pronunciation & Conversation | esl-pronunciation-and-conversation-4133093 | - | Vocabulary | esl-vocabulary-4133092 | - | Writing Skills | esl-writing-skills-4133091 | - | Reading Comprehension | esl-reading-comprehension-4133090 | - | Grammar | esl-grammar-4133089 | - | Business English | esl-business-english-4133088 | - | Resources for Teachers | resources-for-esl-teachers-4133087 | - - #### Spanish - - | category | id | - | ----------------- | ----------------------------------- | - | History & Culture | spanish-history-and-culture-4133084 | - | Pronunciation | spanish-pronunciation-4133083 | - | Vocabulary | spanish-vocabulary-4133082 | - | Writing Skills | spanish-writing-skills-4133081 | - | Grammar | spanish-grammar-4133080 | - - #### French - - | category | id | - | ---------------------------- | -------------------------------------------- | - | Pronunciation & Conversation | french-pronunciation-4133075 | - | Vocabulary | french-vocabulary-4133076 | - | Grammar | french-grammar-4133074 | - | Resources For Teachers | french-resources-for-french-teachers-4133077 | - - #### German - - | category | id | - | ---------------------------- | ---------------------------------- | - | History & Culture | german-history-and-culture-4133071 | - | Pronunciation & Conversation | german-pronunciation-4133070 | - | Vocabulary | german-vocabulary-4133068 | - | Grammar | german-grammar-4133067 | - - #### Italian - - | category | id | - | ----------------- | ----------------------------------- | - | History & Culture | italian-history-and-culture-4133065 | - | Vocabulary | italian-vocabulary-4133061 | - | Grammar | italian-grammar-4133063 | - - #### Japanese - - | category | id | - | ----------------------------- | ------------------------------------ | - | History & Culture | japanese-history-and-culture-4133058 | - | Essential Japanese Vocabulary | japanese-vocabulary-4133060 | - | Japanese Grammar | japanese-grammar-4133056 | - - #### Mandarin - - | category | id | - | -------------------------------- | ---------------------------------------- | - | Mandarin History and Culture | mandarin-history-and-culture-4133054 | - | Pronunciation | mandarin-pronunciation-4133053 | - | Vocabulary | mandarin-vocabulary-4133052 | - | Understanding Chinese Characters | understanding-chinese-characters-4133051 | - - #### Russian - - | category | id | - | -------- | --------------- | - | Russian | russian-4175265 | - - #### For Students & Parents - - | category | id | - | ------------------ | -------------------------- | - | Homework Help | homework-help-4132587 | - | Private School | private-school-4132514 | - | Test Prep | test-prep-4132578 | - | College Admissions | college-admissions-4132565 | - | College Life | college-life-4132553 | - | Graduate School | graduate-school-4132543 | - | Business School | business-school-4132536 | - | Law School | law-school-4132527 | - | Distance Learning | distance-learning-4132521 | - - #### For Educators - - | category | id | - | -------------------- | ----------------------------- | - | Becoming A Teacher | becoming-a-teacher-4132510 | - | Assessments & Tests | assessments-and-tests-4132508 | - | Elementary Education | elementary-education-4132507 | - | Secondary Education | secondary-education-4132504 | - | Special Education | special-education-4132499 | - | Teaching | teaching-4132488 | - | Homeschooling | homeschooling-4132480 | - - #### For Adult Learners - - | category | id | - | ----------------------- | ------------------------------- | - | Tips For Adult Students | tips-for-adult-students-4132468 | - | Getting Your Ged | getting-your-ged-4132466 | - </details>`, +| category | id | +| ---------------- | -------------------------- | +| Science | science-4132464 | +| Math | math-4133545 | +| Social Sciences | social-sciences-4133522 | +| Computer Science | computer-science-4133486 | +| Animals & Nature | animals-and-nature-4133421 | + +#### Humanities + +| category | id | +| ----------------- | --------------------------- | +| History & Culture | history-and-culture-4133356 | +| Visual Arts | visual-arts-4132957 | +| Literature | literature-4133251 | +| English | english-4688281 | +| Geography | geography-4133035 | +| Philosophy | philosophy-4133025 | +| Issues | issues-4133022 | + +#### Languages + +| category | id | +| ---------------------------- | ---------------- | +| English as a Second Language | esl-4133095 | +| Spanish | spanish-4133085 | +| French | french-4133079 | +| German | german-4133073 | +| Italian | italian-4133069 | +| Japanese | japanese-4133062 | +| Mandarin | mandarin-4133057 | +| Russian | russian-4175265 | + +#### Resources + +| category | id | +| ---------------------- | ---------------------------- | +| For Students & Parents | for-students-parents-4132588 | +| For Educators | for-educators-4132509 | +| For Adult Learners | for-adult-learners-4132469 | + +<details> +<summary>More categories</summary> + +#### Science + +| category | id | +| ----------------- | --------------------------- | +| Chemistry | chemistry-4133594 | +| Biology | biology-4133580 | +| Physics | physics-4133571 | +| Geology | geology-4133564 | +| Astronomy | astronomy-4133558 | +| Weather & Climate | weather-and-climate-4133550 | + +#### Math + +| category | id | +| --------------------- | ------------------------------- | +| Math Tutorials | math-tutorials-4133543 | +| Geometry | geometry-4133540 | +| Arithmetic | arithmetic-4133542 | +| Pre Algebra & Algebra | pre-algebra-and-algebra-4133541 | +| Statistics | statistics-4133539 | +| Exponential Decay | exponential-decay-4133528 | +| Worksheets By Grade | worksheets-by-grade-4133526 | +| Resources | math-resources-4133523 | + +#### Social Sciences + +| category | id | +| ----------- | ------------------- | +| Psychology | psychology-4160512 | +| Sociology | sociology-4133515 | +| Archaeology | archaeology-4133504 | +| Economics | economics-4133521 | +| Ergonomics | ergonomics-4133492 | + +#### Computer Science + +| category | id | +| ---------------------- | -------------------------------- | +| PHP Programming | php-4133485 | +| Perl | perl-4133481 | +| Python | python-4133477 | +| Java Programming | java-programming-4133478 | +| Javascript Programming | javascript-programming-4133476 | +| Delphi Programming | delphi-programming-4133475 | +| C & C++ Programming | c-and-c-plus-programming-4133470 | +| Ruby Programming | ruby-programming-4133469 | +| Visual Basic | visual-basic-4133468 | + +#### Animals and Nature + +| category | id | +| ---------------- | ------------------------ | +| Amphibians | amphibians-4133418 | +| Birds | birds-4133416 | +| Habitat Profiles | habitat-profiles-4133412 | +| Mammals | mammals-4133411 | +| Reptiles | reptiles-4133408 | +| Insects | insects-4133406 | +| Marine Life | marine-life-4133393 | +| Forestry | forestry-4133386 | +| Dinosaurs | dinosaurs-4133376 | +| Evolution | evolution-4133366 | + +#### History and Culture + +| category | id | +| ------------------------------ | ---------------------------------------- | +| American History | american-history-4133354 | +| African American History | african-american-history-4133344 | +| African History | african-history-4133338 | +| Ancient History and Culture | ancient-history-4133336 | +| Asian History | asian-history-4133325 | +| European History | european-history-4133316 | +| Genealogy | genealogy-4133308 | +| Inventions | inventions-4133303 | +| Latin American History | latin-american-history-4133296 | +| Medieval & Renaissance History | medieval-and-renaissance-history-4133289 | +| Military History | military-history-4133285 | +| The 20th Century | 20th-century-4133273 | +| Women's History | womens-history-4133260 | + +#### Visual Arts + +| category | id | +| ------------- | -------------------- | +| Art & Artists | art-4132956 | +| Architecture | architecture-4132953 | + +#### Literature + +| category | id | +| ------------------ | -------------------------- | +| Best Sellers | best-sellers-4133250 | +| Classic Literature | classic-literature-4133245 | +| Plays & Drama | plays-and-drama-4133239 | +| Poetry | poetry-4133232 | +| Quotations | quotations-4133229 | +| Shakespeare | shakespeare-4133223 | +| Short Stories | short-stories-4133217 | +| Children's Books | childrens-books-4133216 | + +#### English + +| category | id | +| --------------- | ----------------------- | +| English Grammar | english-grammar-4133049 | +| Writing | writing-4133048 | + +#### Geography + +| category | id | +| ------------------------ | ---------------------------------- | +| Basics | geography-basics-4133034 | +| Physical Geography | physical-geography-4133032 | +| Political Geography | political-geography-4133033 | +| Population | population-4133031 | +| Country Information | country-information-4133030 | +| Key Figures & Milestones | key-figures-and-milestones-4133029 | +| Maps | maps-4133027 | +| Urban Geography | urban-geography-4133026 | + +#### Philosophy + +| category | id | +| ------------------------------ | ---------------------------------------- | +| Philosophical Theories & Ideas | philosophical-theories-and-ideas-4133024 | +| Major Philosophers | major-philosophers-4133023 | + +#### Issues + +| category | id | +| --------------------------------- | -------------------------------- | +| The U. S. Government | us-government-4133021 | +| U.S. Foreign Policy | us-foreign-policy-4133010 | +| U.S. Liberal Politics | us-liberal-politics-4133009 | +| U.S. Conservative Politics | us-conservative-politics-4133006 | +| Women's Issues | womens-issues-4133002 | +| Civil Liberties | civil-liberties-4132996 | +| The Middle East | middle-east-4132989 | +| Race Relations | race-relations-4132982 | +| Immigration | immigration-4132977 | +| Crime & Punishment | crime-and-punishment-4132972 | +| Canadian Government | canadian-government-4132959 | +| Understanding Types of Government | types-of-government-5179107 | + +#### English as a Second Language + +| category | id | +| ---------------------------- | ------------------------------------------ | +| Pronunciation & Conversation | esl-pronunciation-and-conversation-4133093 | +| Vocabulary | esl-vocabulary-4133092 | +| Writing Skills | esl-writing-skills-4133091 | +| Reading Comprehension | esl-reading-comprehension-4133090 | +| Grammar | esl-grammar-4133089 | +| Business English | esl-business-english-4133088 | +| Resources for Teachers | resources-for-esl-teachers-4133087 | + +#### Spanish + +| category | id | +| ----------------- | ----------------------------------- | +| History & Culture | spanish-history-and-culture-4133084 | +| Pronunciation | spanish-pronunciation-4133083 | +| Vocabulary | spanish-vocabulary-4133082 | +| Writing Skills | spanish-writing-skills-4133081 | +| Grammar | spanish-grammar-4133080 | + +#### French + +| category | id | +| ---------------------------- | -------------------------------------------- | +| Pronunciation & Conversation | french-pronunciation-4133075 | +| Vocabulary | french-vocabulary-4133076 | +| Grammar | french-grammar-4133074 | +| Resources For Teachers | french-resources-for-french-teachers-4133077 | + +#### German + +| category | id | +| ---------------------------- | ---------------------------------- | +| History & Culture | german-history-and-culture-4133071 | +| Pronunciation & Conversation | german-pronunciation-4133070 | +| Vocabulary | german-vocabulary-4133068 | +| Grammar | german-grammar-4133067 | + +#### Italian + +| category | id | +| ----------------- | ----------------------------------- | +| History & Culture | italian-history-and-culture-4133065 | +| Vocabulary | italian-vocabulary-4133061 | +| Grammar | italian-grammar-4133063 | + +#### Japanese + +| category | id | +| ----------------------------- | ------------------------------------ | +| History & Culture | japanese-history-and-culture-4133058 | +| Essential Japanese Vocabulary | japanese-vocabulary-4133060 | +| Japanese Grammar | japanese-grammar-4133056 | + +#### Mandarin + +| category | id | +| -------------------------------- | ---------------------------------------- | +| Mandarin History and Culture | mandarin-history-and-culture-4133054 | +| Pronunciation | mandarin-pronunciation-4133053 | +| Vocabulary | mandarin-vocabulary-4133052 | +| Understanding Chinese Characters | understanding-chinese-characters-4133051 | + +#### Russian + +| category | id | +| -------- | --------------- | +| Russian | russian-4175265 | + +#### For Students & Parents + +| category | id | +| ------------------ | -------------------------- | +| Homework Help | homework-help-4132587 | +| Private School | private-school-4132514 | +| Test Prep | test-prep-4132578 | +| College Admissions | college-admissions-4132565 | +| College Life | college-life-4132553 | +| Graduate School | graduate-school-4132543 | +| Business School | business-school-4132536 | +| Law School | law-school-4132527 | +| Distance Learning | distance-learning-4132521 | + +#### For Educators + +| category | id | +| -------------------- | ----------------------------- | +| Becoming A Teacher | becoming-a-teacher-4132510 | +| Assessments & Tests | assessments-and-tests-4132508 | +| Elementary Education | elementary-education-4132507 | +| Secondary Education | secondary-education-4132504 | +| Special Education | special-education-4132499 | +| Teaching | teaching-4132488 | +| Homeschooling | homeschooling-4132480 | + +#### For Adult Learners + +| category | id | +| ----------------------- | ------------------------------- | +| Tips For Adult Students | tips-for-adult-students-4132468 | +| Getting Your Ged | getting-your-ged-4132466 | +</details>`, }; async function handler(ctx) { diff --git a/lib/routes/thoughtco/namespace.ts b/lib/routes/thoughtco/namespace.ts index 8cb061f8327725..f9daab2fa5779b 100644 --- a/lib/routes/thoughtco/namespace.ts +++ b/lib/routes/thoughtco/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ThoughtCo', url: 'thoughtco.com', + lang: 'en', }; diff --git a/lib/routes/thoughtworks/index.ts b/lib/routes/thoughtworks/index.ts new file mode 100644 index 00000000000000..daa652eec15f97 --- /dev/null +++ b/lib/routes/thoughtworks/index.ts @@ -0,0 +1,71 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/blog', + categories: ['programming'], + example: '/thoughtworks/blog', + radar: [ + { + source: ['www.thoughtworks.com/zh-cn/insights/blog'], + }, + ], + name: 'Inside Blog', + maintainers: ['Hyvi'], + handler, +}; +async function handler() { + // https://www.thoughtworks.com/rest/search/config 里的 BLOG_SEARCH_TOKEN + const tokenData = await ofetch('https://www.thoughtworks.com/rest/search/config', { + headers: { + 'content-type': 'application/json', + origin: 'https://www.thoughtworks.com', + referer: 'https://www.thoughtworks.com/', + }, + }); + + // 'Bearer ' + token + const bearerToken = 'Bearer ' + tokenData.BLOG_SEARCH_TOKEN; + + const data = await ofetch('https://platform-eu.cloud.coveo.com/rest/search/v2?organizationId=thoughtworksproductionhcqoag0q', { + method: 'POST', + headers: { + authorization: bearerToken, + 'content-type': 'application/json', + origin: 'https://www.thoughtworks.com', + referer: 'https://www.thoughtworks.com/', + }, + body: { + context: { countryLocale: 'zh-cn' }, + fieldsToInclude: ['author', 'language', 'objecttype', 'collection', 'source', 'tw_content_type', 'tw_topic', 'tw_published_date'], + sortCriteria: '@tw_published_date descending', + numberOfResults: 10, + firstResult: 0, + }, + }); + // 从 API 响应中提取相关数据 + const items = data.results.map((item) => ({ + // 文章标题 + title: item.title, + // 文章链接 + link: item.uri, + // 文章正文 + description: item.excerpt, + // 文章发布日期 + pubDate: parseDate(item.raw.tw_published_date), + // 如果有的话,文章作者 + author: item.raw.sysauthor, + // 如果有的话,文章分类 + // category: item.labels.map((label) => label.name), + })); + + return { + // 源标题 + title: 'ThoughtWorks Blog', + // 源链接 + link: 'https://www.thoughtworks.com/zh-cn/insights/blog', + // 源文章 + item: items, + }; +} diff --git a/lib/routes/thoughtworks/namespace.ts b/lib/routes/thoughtworks/namespace.ts new file mode 100644 index 00000000000000..77e17cc6009ed9 --- /dev/null +++ b/lib/routes/thoughtworks/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'ThoughtWorks', + url: 'www.thoughtworks.com/zh-cn/insights/blog', + lang: 'zh-CN', +}; diff --git a/lib/routes/threads/index.ts b/lib/routes/threads/index.ts index 3db1c6e0c93857..00c91a95809c22 100644 --- a/lib/routes/threads/index.ts +++ b/lib/routes/threads/index.ts @@ -1,45 +1,44 @@ -import { Route } from '@/types'; -import ofetch from '@/utils/ofetch'; +import { Route, ViewType } from '@/types'; import { parseDate } from '@/utils/parse-date'; -import { REPLIES_QUERY, THREADS_QUERY, apiUrl, threadUrl, profileUrl, extractTokens, makeHeader, getUserId, buildContent } from './utils'; -import { destr } from 'destr'; -import cache from '@/utils/cache'; -import { config } from '@/config'; +import { threadUrl, profileUrl, extractTokens, getUserId, buildContent } from './utils'; +import { JSDOM } from 'jsdom'; +import { JSONPath } from 'jsonpath-plus'; +import ofetch from '@/utils/ofetch'; export const route: Route = { path: '/:user/:routeParams?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/threads/zuck', - parameters: { user: 'Username', routeParams: 'Extra parameters, see the table below' }, + parameters: { + user: 'Username', + routeParams: { + description: `Extra parameters, see the table below +Specify options (in the format of query string) in parameter \`routeParams\` to control some extra features for threads + +| Key | Description | Accepts | Defaults to | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------- | +| \`showAuthorInTitle\` | Show author name in title | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | +| \`showAuthorInDesc\` | Show author name in description (RSS body) | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | +| \`showQuotedAuthorAvatarInDesc\` | Show avatar of quoted author in description (RSS body) (Not recommended if your RSS reader extracts images from description) | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | +| \`showAuthorAvatarInDesc\` | Show avatar of author in description (RSS body) (Not recommended if your RSS reader extracts images from description) | \`0\`/\`1\`/\`true\`/\`false\` | \`falseP\` | +| \`showEmojiForQuotesAndReply\` | Use "🔁" instead of "QT", "↩️" instead of "Re" | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | +| \`showQuotedInTitle\` | Show quoted tweet in title | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | +| \`replies\` | Show replies | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` |`, + }, + }, name: 'User timeline', - maintainers: ['ninboy'], + maintainers: ['ninboy', 'pseudoyu'], handler, - description: `Specify options (in the format of query string) in parameter \`routeParams\` to control some extra features for threads - - | Key | Description | Accepts | Defaults to | - | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------- | - | \`showAuthorInTitle\` | Show author name in title | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | - | \`showAuthorInDesc\` | Show author name in description (RSS body) | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | - | \`showQuotedAuthorAvatarInDesc\` | Show avatar of quoted author in description (RSS body) (Not recommended if your RSS reader extracts images from description) | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | - | \`showAuthorAvatarInDesc\` | Show avatar of author in description (RSS body) (Not recommended if your RSS reader extracts images from description) | \`0\`/\`1\`/\`true\`/\`false\` | \`falseP\` | - | \`showEmojiForQuotesAndReply\` | Use "🔁" instead of "QT", "↩️" instead of "Re" | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | - | \`showQuotedInTitle\` | Show quoted tweet in title | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | - | \`replies\` | Show replies | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | - - Specify different option values than default values to improve readability. The URL - - \`\`\` - https://rsshub.app/threads/zuck/showAuthorInTitle=1&showAuthorInDesc=1&showQuotedAuthorAvatarInDesc=1&showAuthorAvatarInDesc=1&showEmojiForQuotesAndReply=1&showQuotedInTitle=1 - \`\`\``, }; async function handler(ctx) { const { user, routeParams } = ctx.req.param(); const { lsd } = await extractTokens(user); - const userId = await getUserId(user, lsd); + const userId = await getUserId(user); const params = new URLSearchParams(routeParams); - const debugJson = { + const debugJson: any = { params: routeParams, lsd, }; @@ -54,48 +53,59 @@ async function handler(ctx) { replies: params.get('replies') ?? false, }; - const threadsResponse = await cache.tryGet( - `threads:${userId}:${options.replies}`, - () => - ofetch(apiUrl, { - method: 'POST', - headers: { - ...makeHeader(user, lsd), - 'content-type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - lsd, - variables: JSON.stringify({ userID: userId }), - doc_id: String(options.replies ? REPLIES_QUERY : THREADS_QUERY), - }).toString(), - parseResponse: (txt) => destr(txt), - }), - config.cache.routeExpire, - false - ); + const response = await ofetch(profileUrl(user), { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, br', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'Cache-Control': 'no-cache', + Pragma: 'no-cache', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + }, + }); + + const dom = new JSDOM(response); + + let threadsData: ThreadItem[] | null = null; + for (const el of dom.window.document.querySelectorAll('script[data-sjs]')) { + try { + const data = JSONPath({ + path: '$..thread_items[0]', + json: JSON.parse(el.textContent || ''), + }); + + if (data?.length > 0) { + threadsData = data as ThreadItem[]; + break; + } + } catch { + // Skip invalid JSON + } + } + + if (!threadsData) { + throw new Error('Failed to fetch thread data'); + } debugJson.profileId = userId; - debugJson.response = { - response: threadsResponse, - }; + debugJson.response = { response: threadsData }; + + const userData: ThreadUser = threadsData[0]?.post?.user || { username: user, profile_pic_url: '' }; - const threads = threadsResponse?.data?.mediaData?.threads || []; - const userData = threadsResponse?.data?.mediaData?.threads?.[0]?.thread_items?.[0]?.post?.user || {}; - - const items = threads.flatMap((thread) => - thread.thread_items - .filter((item) => user === item.post.user?.username) - .map((item) => { - const { title, description } = buildContent(item, options); - return { - author: user, - title, - description, - pubDate: parseDate(item.post.taken_at, 'X'), - link: threadUrl(item.post.code), - }; - }) - ); + const items = threadsData + .filter((item) => user === item.post.user?.username) + .map((item) => ({ + author: user, + title: buildContent(item, options).title, + description: buildContent(item, options).description, + pubDate: parseDate(item.post.taken_at, 'X'), + link: threadUrl(item.post.code), + })); debugJson.items = items; ctx.set('json', debugJson); @@ -104,7 +114,22 @@ async function handler(ctx) { title: `${user} (@${user}) on Threads`, link: profileUrl(user), image: userData?.profile_pic_url, - // description: userData.biography, item: items, }; } + +interface ThreadUser { + username: string; + profile_pic_url: string; +} + +interface ThreadItem { + post: { + user?: ThreadUser; + taken_at: number; + code: string; + caption?: { + text: string; + }; + }; +} diff --git a/lib/routes/threads/namespace.ts b/lib/routes/threads/namespace.ts index 63d33a4787d368..f3f83b8e4599b7 100644 --- a/lib/routes/threads/namespace.ts +++ b/lib/routes/threads/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Threads', url: 'threads.net', + lang: 'en', }; diff --git a/lib/routes/threads/utils.ts b/lib/routes/threads/utils.ts index 4dbad9f8f00a72..f121fbf46a51c9 100644 --- a/lib/routes/threads/utils.ts +++ b/lib/routes/threads/utils.ts @@ -1,79 +1,91 @@ -import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import dayjs from 'dayjs'; import cache from '@/utils/cache'; -import { destr } from 'destr'; import NotFoundError from '@/errors/types/not-found'; +import ofetch from '@/utils/ofetch'; +import { JSDOM } from 'jsdom'; +import { JSONPath } from 'jsonpath-plus'; const profileUrl = (user: string) => `https://www.threads.net/@${user}`; const threadUrl = (code: string) => `https://www.threads.net/t/${code}`; -const apiUrl = 'https://www.threads.net/api/graphql'; -// const PROFILE_QUERY = 23_996_318_473_300_828; // no longer works -const THREADS_QUERY = 6_232_751_443_445_612; -const REPLIES_QUERY = 6_307_072_669_391_286; -const USER_AGENT = 'Barcelona 289.0.0.77.109 Android'; -const appId = '238260118697367'; -const asbdId = '129477'; +const USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1'; const extractTokens = async (user): Promise<{ lsd: string }> => { const response = await ofetch(profileUrl(user), { headers: { 'User-Agent': USER_AGENT, - 'X-IG-App-ID': appId, + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, br', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'Cache-Control': 'no-cache', + Pragma: 'no-cache', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', }, }); - const $ = load(response); + const $ = load(response); const data = $('script:contains("LSD"):first').text(); - const lsd = data.match(/"LSD",\[],{"token":"([\w@-]+)"},/)?.[1]; + if (!lsd) { throw new NotFoundError('LSD token not found'); } - // const userId = data.match(/{"user_id":"(\d+)"},/)?.[1]; - - const ret = { lsd }; - return ret; + return { lsd }; }; -const makeHeader = (user: string, lsd: string) => ({ - Accept: '*/*', - Host: 'www.threads.net', - Origin: 'https://www.threads.net', - Referer: profileUrl(user), - 'User-Agent': USER_AGENT, - 'X-FB-LSD': lsd, - 'X-IG-App-ID': appId, - 'Sec-Fetch-Site': 'same-origin', -}); - -const getUserId = (user: string, lsd: string): Promise<string> => - cache.tryGet(`threads:userId:${user}`, async () => { - const pathName = `/@${user}`; - const payload: any = { - 'route_urls[0]': pathName, - __a: '1', - __comet_req: '29', - lsd, - }; - const response = await ofetch('https://www.threads.net/ajax/bulk-route-definitions/', { - method: 'POST', - headers: { - ...makeHeader(user, lsd), - 'content-type': 'application/x-www-form-urlencoded', - 'X-ASBD-ID': asbdId, - }, - body: new URLSearchParams(payload).toString(), - parseResponse: (txt) => destr(txt.slice(9)), // remove "for (;;);" - }); +const getUserId = (user: string): Promise<string> => + cache + .tryGet(`threads:userId:${user}`, async () => { + const response = await ofetch(profileUrl(user), { + headers: { + 'User-Agent': USER_AGENT, + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, br', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'Cache-Control': 'no-cache', + Pragma: 'no-cache', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + }, + }); + + const dom = new JSDOM(response); + + for (const el of dom.window.document.querySelectorAll('script[data-sjs]')) { + try { + const data = JSONPath({ + path: '$..user_id', + json: JSON.parse(el.textContent || ''), + }); + + if (data?.[0]) { + return data[0]; + } + } catch { + // Skip invalid JSON + } + } - const userId = response.payload.payloads[pathName].result.exports.rootView.props.user_id; - return userId; - }); + throw new NotFoundError('User ID not found'); + }) + .then((result): string => { + if (!result || typeof result !== 'string') { + throw new TypeError('Invalid user ID type'); + } + return result; + }); const hasMedia = (post) => post.image_versions2 || post.carousel_media || post.video_versions; + const buildMedia = (post) => { let html = ''; @@ -81,25 +93,13 @@ const buildMedia = (post) => { for (const media of post.carousel_media) { const firstImage = media.image_versions2?.candidates[0]; const firstVideo = media.video_versions?.[0]; - if (firstVideo) { - html += `<video controls autoplay loop poster="${firstImage.url}">`; - html += `<source src="${firstVideo.url}"/>`; - html += '</video>'; - } else { - html += `<img src="${firstImage.url}"/>`; - } + html += firstVideo ? `<video controls autoplay loop poster="${firstImage.url}"><source src="${firstVideo.url}"/></video>` : `<img src="${firstImage.url}"/>`; } } else { const mainImage = post.image_versions2?.candidates?.[0]; const mainVideo = post.video_versions?.[0]; if (mainImage) { - if (mainVideo) { - html += `<video controls autoplay loop poster="${mainImage.url}">`; - html += `<source src="${mainVideo.url}"/>`; - html += '</video>'; - } else { - html += `<img src="${mainImage.url}"/>`; - } + html += mainVideo ? `<video controls autoplay loop poster="${mainImage.url}"><source src="${mainVideo.url}"/></video>` : `<img src="${mainImage.url}"/>`; } } @@ -163,4 +163,4 @@ const buildContent = (item, options) => { return { title, description }; }; -export { apiUrl, profileUrl, threadUrl, THREADS_QUERY, REPLIES_QUERY, USER_AGENT, extractTokens, getUserId, makeHeader, hasMedia, buildMedia, buildContent }; +export { profileUrl, threadUrl, extractTokens, getUserId, buildContent }; diff --git a/lib/routes/thwiki/namespace.ts b/lib/routes/thwiki/namespace.ts index 3a7ad2a04d153c..c79ea69b2e3736 100644 --- a/lib/routes/thwiki/namespace.ts +++ b/lib/routes/thwiki/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'THBWiki', url: 'thwiki.cc', + lang: 'zh-CN', }; diff --git a/lib/routes/tiddlywiki/namespace.ts b/lib/routes/tiddlywiki/namespace.ts new file mode 100644 index 00000000000000..b8b7742b8babd5 --- /dev/null +++ b/lib/routes/tiddlywiki/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'TiddlyWiki', + url: 'tiddlywiki.com', + description: '', + lang: 'en', +}; diff --git a/lib/routes/tiddlywiki/releases.ts b/lib/routes/tiddlywiki/releases.ts new file mode 100644 index 00000000000000..cbc9174a602817 --- /dev/null +++ b/lib/routes/tiddlywiki/releases.ts @@ -0,0 +1,84 @@ +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: '/releases', + categories: ['program-update'], + example: '/tiddlywiki/releases', + url: 'tiddlywiki.com', + name: 'Releases', + maintainers: ['p3psi-boo'], + radar: [ + { + source: ['github.com/TiddlyWiki/TiddlyWiki5'], + target: '/releases', + }, + { + source: ['tiddlywiki.com'], + target: '/releases', + }, + ], + handler, +}; + +async function handler() { + const tagListUrl = 'https://github.com/TiddlyWiki/TiddlyWiki5/releases.atom'; + + const response = await got({ + method: 'get', + url: tagListUrl, + }); + + const $ = load(response.data); + + const alist = $('entry'); + + const versionList = alist + .toArray() + .map((item) => { + item = $(item); + const text = item.find('title').text(); + const date = item.find('updated').text(); + // 使用正则提取 v5.3.6 格式 + const version = text.match(/v\d+\.\d+\.\d+/)?.[0]; + return { + version, + pubDate: parseDate(date), + }; + }) + .filter((item) => item.version); + + const items = await Promise.all( + versionList.map((item) => { + const _version = item.version.slice(1); + const url = `https://tiddlywiki.com/static/Release%2520${_version}.html`; + return cache.tryGet(url, async () => { + const response = await got({ + method: 'get', + url, + }); + + const $ = load(response.data); + + const description = $('.tc-tiddler-body').html(); + + return { + title: item.version, + link: url, + pubDate: item.pubDate, + description, + }; + }); + }) + ); + + return { + title: 'TiddlyWiki Releases', + link: 'https://tiddlywiki.com/static/Releases.html', + item: items, + }; +} diff --git a/lib/routes/tiktok/namespace.ts b/lib/routes/tiktok/namespace.ts index b754c6bd674933..1476995cb4f1a9 100644 --- a/lib/routes/tiktok/namespace.ts +++ b/lib/routes/tiktok/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'TikTok', url: 'tiktok.com', + lang: 'en', }; diff --git a/lib/routes/tiktok/user.ts b/lib/routes/tiktok/user.ts index 5fac463f237074..c54bd80f72b92f 100644 --- a/lib/routes/tiktok/user.ts +++ b/lib/routes/tiktok/user.ts @@ -53,7 +53,7 @@ async function handler(ctx) { waitUntil: 'networkidle0', }); const SIGI_STATE = await page.evaluate(() => window.SIGI_STATE); - browser.close(); + await browser.close(); const lang = SIGI_STATE.AppContext.lang; const SharingMetaState = SIGI_STATE.SharingMetaState; diff --git a/lib/routes/timednews/namespace.ts b/lib/routes/timednews/namespace.ts index 9bd4901999001e..1728fcf62ee667 100644 --- a/lib/routes/timednews/namespace.ts +++ b/lib/routes/timednews/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '时刻新闻', url: 'timednews.com', + lang: 'zh-CN', }; diff --git a/lib/routes/timednews/news.ts b/lib/routes/timednews/news.ts index 2ad750eb78a65a..da5f2febbbcd93 100644 --- a/lib/routes/timednews/news.ts +++ b/lib/routes/timednews/news.ts @@ -55,7 +55,7 @@ const PATH_LIST = { export const route: Route = { path: '/news/:type?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/timednews/news', parameters: { type: '子分类,见下表,默认为全部' }, features: { @@ -71,9 +71,9 @@ export const route: Route = { handler, description: `子分类 - | 全部 | 时政 | 财经 | 科技 | 社会 | 体娱 | 国际 | 美国 | 中国 | 欧洲 | 评论 | - | ---- | -------------- | ------- | ---------- | ------ | ------ | ------------- | ---- | ---- | ------ | -------- | - | all | currentAffairs | finance | technology | social | sports | international | usa | cn | europe | comments |`, +| 全部 | 时政 | 财经 | 科技 | 社会 | 体娱 | 国际 | 美国 | 中国 | 欧洲 | 评论 | +| ---- | -------------- | ------- | ---------- | ------ | ------ | ------------- | ---- | ---- | ------ | -------- | +| all | currentAffairs | finance | technology | social | sports | international | usa | cn | europe | comments |`, }; async function handler(ctx) { diff --git a/lib/routes/tingshuitz/changsha.ts b/lib/routes/tingshuitz/changsha.ts index aa4fa2ab198fe5..98ed6d5b9b6d15 100644 --- a/lib/routes/tingshuitz/changsha.ts +++ b/lib/routes/tingshuitz/changsha.ts @@ -22,10 +22,10 @@ export const route: Route = { handler, description: `可能仅限于中国大陆服务器访问,以实际情况为准。 - | channelId | 分类 | - | --------- | -------- | - | 78 | 计划停水 | - | 157 | 抢修停水 |`, +| channelId | 分类 | +| --------- | -------- | +| 78 | 计划停水 | +| 157 | 抢修停水 |`, }; async function handler(ctx) { diff --git a/lib/routes/tingshuitz/namespace.ts b/lib/routes/tingshuitz/namespace.ts index bce8ce8dab8d1c..016538b517c005 100644 --- a/lib/routes/tingshuitz/namespace.ts +++ b/lib/routes/tingshuitz/namespace.ts @@ -3,5 +3,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '停水通知', url: 'swj.dl.gov.cn', - description: `配合 [IFTTT](https://ifttt.com/) Applets [邮件通知](https://ifttt.com/applets/SEvmDVKY-) 使用实现自动通知效果.`, + description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/tingtingfm/namespace.ts b/lib/routes/tingtingfm/namespace.ts index 2fb66670e9d828..18bf41f046190f 100644 --- a/lib/routes/tingtingfm/namespace.ts +++ b/lib/routes/tingtingfm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '听听 FM', url: 'mobile.tingtingfm.com', + lang: 'zh-CN', }; diff --git a/lib/routes/tingtingfm/program.ts b/lib/routes/tingtingfm/program.ts index d39fcdeb665315..d6cb883a7c4994 100644 --- a/lib/routes/tingtingfm/program.ts +++ b/lib/routes/tingtingfm/program.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); @@ -12,7 +12,8 @@ import { getClientVal, sign } from './utils'; export const route: Route = { path: '/program/:programId', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Audios, example: '/tingtingfm/program/M7VJv6Jj4R', parameters: { programId: '节目 ID,可以在 URL 中找到' }, features: { diff --git a/lib/routes/tisi/namespace.ts b/lib/routes/tisi/namespace.ts index 6ea4ab10ca0e46..1789c8b3841dd4 100644 --- a/lib/routes/tisi/namespace.ts +++ b/lib/routes/tisi/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '腾讯研究院', url: 'tisi.org', + lang: 'zh-CN', }; diff --git a/lib/routes/tju/cic/index.ts b/lib/routes/tju/cic/index.ts index bbecef32cd34dd..a96297fa6e3d47 100644 --- a/lib/routes/tju/cic/index.ts +++ b/lib/routes/tju/cic/index.ts @@ -30,15 +30,15 @@ export const route: Route = { supportScihub: false, }, name: 'College of Intelligence and Computing', - maintainers: ['SuperPung'], + maintainers: ['AlanZeng423', 'SuperPung'], handler, description: `| College News | Notification | TJU Forum for CIC | - | :----------: | :----------: | :---------------: | - | news | notification | forum |`, +| :----------: | :----------: | :---------------: | +| news | notification | forum |`, }; async function handler(ctx) { - const type = ctx.params && ctx.req.param('type'); + const type = ctx.req.param('type'); let path, subtitle; switch (type) { diff --git a/lib/routes/tju/namespace.ts b/lib/routes/tju/namespace.ts index 797cb1abaa514b..c9653c9bb7a10d 100644 --- a/lib/routes/tju/namespace.ts +++ b/lib/routes/tju/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Tianjin University 天津大学', url: 'cic.tju.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/tju/news/index.ts b/lib/routes/tju/news/index.ts index d7e0d54b76725f..eb4d5c1efa8b17 100644 --- a/lib/routes/tju/news/index.ts +++ b/lib/routes/tju/news/index.ts @@ -32,15 +32,15 @@ export const route: Route = { supportScihub: false, }, name: 'News', - maintainers: ['SuperPung'], + maintainers: ['AlanZeng423', 'SuperPung'], handler, description: `| Focus on TJU | General News | Internal News | Media Report | Pictures of TJU | - | :----------: | :----------: | :-----------: | :----------: | :-------------: | - | focus | general | internal | media | picture |`, +| :----------: | :----------: | :-----------: | :----------: | :-------------: | +| focus | general | internal | media | picture |`, }; async function handler(ctx) { - const type = ctx.params && ctx.req.param('type'); + const type = ctx.req.param('type'); let path, subtitle; switch (type) { diff --git a/lib/routes/tju/oaa/index.ts b/lib/routes/tju/oaa/index.ts index 4b58b9f9bcf048..21dc91093cad22 100644 --- a/lib/routes/tju/oaa/index.ts +++ b/lib/routes/tju/oaa/index.ts @@ -37,15 +37,15 @@ export const route: Route = { supportScihub: false, }, name: 'The Office of Academic Affairs', - maintainers: ['AmosChenYQ', 'SuperPung'], + maintainers: ['AlanZeng423', 'AmosChenYQ', 'SuperPung'], handler, description: `| News | Notification | - | :--: | :----------: | - | news | notification |`, +| :--: | :----------: | +| news | notification |`, }; async function handler(ctx) { - const type = ctx.params && ctx.req.param('type'); + const type = ctx.req.param('type'); let path, subtitle; switch (type) { diff --git a/lib/routes/tju/yzb/index.ts b/lib/routes/tju/yzb/index.ts index 013fdc0d1773bb..09e4abadc193b0 100644 --- a/lib/routes/tju/yzb/index.ts +++ b/lib/routes/tju/yzb/index.ts @@ -36,8 +36,8 @@ export const route: Route = { maintainers: ['SuperPung'], handler, description: `| School-level Notice | Master | Doctor | On-the-job Degree | - | :-----------------: | :----: | :----: | :---------------: | - | notice | master | doctor | job |`, +| :-----------------: | :----: | :----: | :---------------: | +| notice | master | doctor | job |`, }; async function handler(ctx) { diff --git a/lib/routes/tkww/index.ts b/lib/routes/tkww/index.ts new file mode 100644 index 00000000000000..4a00ab114a33f0 --- /dev/null +++ b/lib/routes/tkww/index.ts @@ -0,0 +1,83 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { config } from '@/config'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +export const route: Route = { + path: '/:column{.+}?', + categories: ['traditional-media'], + example: '/tkww/hong_kong', + parameters: { + column: '欄目,默認為 home (首頁)', + }, + features: { + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '新聞', + maintainers: ['quiniapiezoelectricity'], + radar: [ + { + source: ['www.tkww.hk/:column'], + target: '/:column', + }, + ], + handler, + description: ` +::: tip +欄目可用\`名稱\`或對應網頁的\`path\`, +如 \`https://www.tkww.hk/hong_kong\` 的欄目可以填\`香港\`或是\`hong_kong\` +而 \`https://www.tkww.hk/china/shanghai\` 的欄目則需填\`china/shanghai\` +:::`, +}; + +async function handler(ctx) { + const column = ctx.req.param('column') ?? 'home'; + + const columns = await cache.tryGet('https://www.tkww.hk/columns.json', async () => await got('https://www.tkww.hk/columns.json'), config.cache.routeExpire, false); + + let metadata; + let scope = columns.data.data; + for (const segment of column.split('/').filter((item) => typeof item === 'string')) { + metadata = scope.find((item) => item.name === segment || item.dirname === segment); + scope = metadata?.children ?? []; + } + + if (metadata === undefined) { + throw new InvalidParameterError(`Invalid Column: ${column}`); + } + + const stories = await got(`https://www.tkww.hk/columns/${metadata.uuid}/tkww/app/stories.json`); + + const items = await Promise.all( + stories.data.data.stories.map((item) => + cache.tryGet(item.url, async () => { + item.link = item.url; + item.description = item.summary; + item.pubDate = item.publishTime; + item.category = []; + if (item.keywords) { + item.category = [...item.category, ...item.keywords]; + } + if (item.tags) { + item.category = [...item.category, ...item.tags]; + } + item.category = [...new Set(item.category)]; + const response = await got(item.jsonUrl); + item.description = response.data.data.content; + return item; + }) + ) + ); + + return { + title: metadata.seoTitle, + description: metadata.seoDescription, + link: metadata.url, + item: items, + }; +} diff --git a/lib/routes/tkww/namespace.ts b/lib/routes/tkww/namespace.ts new file mode 100644 index 00000000000000..aaa33226334c8d --- /dev/null +++ b/lib/routes/tkww/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '大公文匯網', + url: 'www.tkww.hk', + lang: 'zh-HK', +}; diff --git a/lib/routes/tmtpost/column.ts b/lib/routes/tmtpost/column.ts new file mode 100644 index 00000000000000..344a584adcf6f0 --- /dev/null +++ b/lib/routes/tmtpost/column.ts @@ -0,0 +1,298 @@ +import { type Data, type Route, ViewType } from '@/types'; + +import { type Context } from 'hono'; + +import { baseUrl, apiBaseUrl, processItems } from './util'; + +export const handler = async (ctx: Context): Promise<Data> => { + const { id } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`column/${id}`, baseUrl).href; + const listApiUrl: string = new URL('v1/categories/multi_content/list', apiBaseUrl).href; + + const query = { + subtype: 'post', + category_guid: id, + }; + + return await processItems(limit, query, listApiUrl, targetUrl); +}; + +export const route: Route = { + path: '/column/:id', + name: '最新', + url: 'www.tmtpost.com', + maintainers: ['nczitzk'], + handler, + example: '/tmtpost/column/6916385', + parameters: { + id: { + description: '专栏 id,可在对应专栏页 URL 中找到', + options: [ + { + label: 'AGI', + value: '6916385', + }, + { + label: '出海', + value: '6998081', + }, + { + label: '创新场景', + value: '3882035', + }, + { + label: '钛度号', + value: '6100587', + }, + { + label: '深度', + value: '3189960', + }, + { + label: '焦点', + value: '6043895', + }, + { + label: '创投', + value: '5994956', + }, + { + label: '汽车', + value: '2573550', + }, + { + label: '3C', + value: '3615534', + }, + { + label: '消费', + value: '3882530', + }, + { + label: '大健康', + value: '3882507', + }, + { + label: '金融', + value: '3882486', + }, + { + label: '钛智宏观', + value: '4277188', + }, + { + label: '产业研究', + value: '5506730', + }, + { + label: '地产', + value: '3882499', + }, + { + label: '大公司', + value: '2446153', + }, + { + label: 'IPO', + value: '6043750', + }, + { + label: '钛度图闻', + value: '5750087', + }, + { + label: '城视', + value: '6998636', + }, + { + label: '创业家', + value: '4273329', + }, + { + label: '人文', + value: '6252390', + }, + { + label: '新职业研究所', + value: '5750104', + }, + { + label: '科普', + value: '5714992', + }, + { + label: '文娱', + value: '2446157', + }, + ], + }, + }, + description: `:::tip +若订阅 [AGI](https://www.tmtpost.com/column/6916385),网址为 \`https://www.tmtpost.com/column/6916385\`,请截取 \`https://www.tmtpost.com/column\` 到末尾的部分 \`6916385\` 作为 \`id\` 参数填入,此时目标路由为 [\`/tmtpost/column/6916385\`](https://rsshub.app/tmtpost/column/6916385)。 +::: + +<details> + <summary>更多分类</summary> + + | [AGI](https://www.tmtpost.com/column/6916385) | [出海](https://www.tmtpost.com/column/6998081) | [创新场景](https://www.tmtpost.com/column/3882035) | [钛度号](https://www.tmtpost.com/column/6100587) | [深度](https://www.tmtpost.com/column/3189960) | + | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | + | [6916385](https://rsshub.app/tmtpost/column/6916385) | [6998081](https://rsshub.app/tmtpost/column/6998081) | [3882035](https://rsshub.app/tmtpost/column/3882035) | [6100587](https://rsshub.app/tmtpost/column/6100587) | [3189960](https://rsshub.app/tmtpost/column/3189960) | + + | [焦点](https://www.tmtpost.com/column/6043895) | [创投](https://www.tmtpost.com/column/5994956) | [汽车](https://www.tmtpost.com/column/2573550) | [3C](https://www.tmtpost.com/column/3615534) | [消费](https://www.tmtpost.com/column/3882530) | + | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | + | [6043895](https://rsshub.app/tmtpost/column/6043895) | [5994956](https://rsshub.app/tmtpost/column/5994956) | [2573550](https://rsshub.app/tmtpost/column/2573550) | [3615534](https://rsshub.app/tmtpost/column/3615534) | [3882530](https://rsshub.app/tmtpost/column/3882530) | + + | [大健康](https://www.tmtpost.com/column/3882507) | [金融](https://www.tmtpost.com/column/3882486) | [钛智宏观](https://www.tmtpost.com/column/4277188) | [产业研究](https://www.tmtpost.com/column/5506730) | [地产](https://www.tmtpost.com/column/3882499) | + | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | + | [3882507](https://rsshub.app/tmtpost/column/3882507) | [3882486](https://rsshub.app/tmtpost/column/3882486) | [4277188](https://rsshub.app/tmtpost/column/4277188) | [5506730](https://rsshub.app/tmtpost/column/5506730) | [3882499](https://rsshub.app/tmtpost/column/3882499) | + + | [大公司](https://www.tmtpost.com/column/2446153) | [IPO](https://www.tmtpost.com/column/6043750) | [钛度图闻](https://www.tmtpost.com/column/5750087) | [城视](https://www.tmtpost.com/column/6998636) | [创业家](https://www.tmtpost.com/column/4273329) | + | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | + | [2446153](https://rsshub.app/tmtpost/column/2446153) | [6043750](https://rsshub.app/tmtpost/column/6043750) | [5750087](https://rsshub.app/tmtpost/column/5750087) | [6998636](https://rsshub.app/tmtpost/column/6998636) | [4273329](https://rsshub.app/tmtpost/column/4273329) | + + | [人文](https://www.tmtpost.com/column/6252390) | [新职业研究所](https://www.tmtpost.com/column/5750104) | [科普](https://www.tmtpost.com/column/5714992) | [文娱](https://www.tmtpost.com/column/2446157) | + | ---------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------- | ---------------------------------------------------- | + | [6252390](https://rsshub.app/tmtpost/column/6252390) | [5750104](https://rsshub.app/tmtpost/column/5750104) | [5714992](https://rsshub.app/tmtpost/column/5714992) | [2446157](https://rsshub.app/tmtpost/column/2446157) | + +</details> +`, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.tmtpost.com/column/:id'], + target: '/column/:id', + }, + { + title: 'AGI', + source: ['www.tmtpost.com/column/6916385'], + target: '/column/6916385', + }, + { + title: '出海', + source: ['www.tmtpost.com/column/6998081'], + target: '/column/6998081', + }, + { + title: '创新场景', + source: ['www.tmtpost.com/column/3882035'], + target: '/column/3882035', + }, + { + title: '钛度号', + source: ['www.tmtpost.com/column/6100587'], + target: '/column/6100587', + }, + { + title: '深度', + source: ['www.tmtpost.com/column/3189960'], + target: '/column/3189960', + }, + { + title: '焦点', + source: ['www.tmtpost.com/column/6043895'], + target: '/column/6043895', + }, + { + title: '创投', + source: ['www.tmtpost.com/column/5994956'], + target: '/column/5994956', + }, + { + title: '汽车', + source: ['www.tmtpost.com/column/2573550'], + target: '/column/2573550', + }, + { + title: '3C', + source: ['www.tmtpost.com/column/3615534'], + target: '/column/3615534', + }, + { + title: '消费', + source: ['www.tmtpost.com/column/3882530'], + target: '/column/3882530', + }, + { + title: '大健康', + source: ['www.tmtpost.com/column/3882507'], + target: '/column/3882507', + }, + { + title: '金融', + source: ['www.tmtpost.com/column/3882486'], + target: '/column/3882486', + }, + { + title: '钛智宏观', + source: ['www.tmtpost.com/column/4277188'], + target: '/column/4277188', + }, + { + title: '产业研究', + source: ['www.tmtpost.com/column/5506730'], + target: '/column/5506730', + }, + { + title: '地产', + source: ['www.tmtpost.com/column/3882499'], + target: '/column/3882499', + }, + { + title: '大公司', + source: ['www.tmtpost.com/column/2446153'], + target: '/column/2446153', + }, + { + title: 'IPO', + source: ['www.tmtpost.com/column/6043750'], + target: '/column/6043750', + }, + { + title: '钛度图闻', + source: ['www.tmtpost.com/column/5750087'], + target: '/column/5750087', + }, + { + title: '城视', + source: ['www.tmtpost.com/column/6998636'], + target: '/column/6998636', + }, + { + title: '创业家', + source: ['www.tmtpost.com/column/4273329'], + target: '/column/4273329', + }, + { + title: '人文', + source: ['www.tmtpost.com/column/6252390'], + target: '/column/6252390', + }, + { + title: '新职业研究所', + source: ['www.tmtpost.com/column/5750104'], + target: '/column/5750104', + }, + { + title: '科普', + source: ['www.tmtpost.com/column/5714992'], + target: '/column/5714992', + }, + { + title: '文娱', + source: ['www.tmtpost.com/column/2446157'], + target: '/column/2446157', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/tmtpost/namespace.ts b/lib/routes/tmtpost/namespace.ts new file mode 100644 index 00000000000000..e7dc5f4c24e36f --- /dev/null +++ b/lib/routes/tmtpost/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '钛媒体', + url: 'tmtpost.com', + categories: ['new-media'], + description: '钛媒体是一家专注于新媒体领域的科技媒体', + lang: 'zh-CN', +}; diff --git a/lib/routes/tmtpost/new.ts b/lib/routes/tmtpost/new.ts new file mode 100644 index 00000000000000..aa29f2212f6a61 --- /dev/null +++ b/lib/routes/tmtpost/new.ts @@ -0,0 +1,42 @@ +import { type Data, type Route, ViewType } from '@/types'; + +import { type Context } from 'hono'; + +import { baseUrl, apiBaseUrl, processItems } from './util'; + +export const handler = async (ctx: Context): Promise<Data> => { + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL('new', baseUrl).href; + const listApiUrl: string = new URL('v1/lists/new', apiBaseUrl).href; + + return await processItems(limit, {}, listApiUrl, targetUrl); +}; + +export const route: Route = { + path: '/new', + name: '最新', + url: 'www.tmtpost.com', + maintainers: ['nczitzk'], + handler, + example: '/tmtpost/new', + parameters: undefined, + description: undefined, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.tmtpost.com'], + target: '/new', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/tmtpost/nictation.ts b/lib/routes/tmtpost/nictation.ts new file mode 100644 index 00000000000000..0b8f4204f788b8 --- /dev/null +++ b/lib/routes/tmtpost/nictation.ts @@ -0,0 +1,59 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/nictation', + categories: ['new-media'], + example: '/tmtpost/word', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: { + source: ['www.tmtpost.com'], + }, + name: '快报', + maintainers: ['defp'], + handler, + url: 'www.tmtpost.com/nictation', +}; + +async function handler() { + const currentTime = Math.floor(Date.now() / 1000); + const oneHourAgo = currentTime - 3600; + const url = 'https://api.tmtpost.com/v1/word/list'; + + const response = await got({ + method: 'get', + url, + searchParams: { + time_start: oneHourAgo, + time_end: currentTime, + limit: 40, + fields: ['share_description', 'share_image', 'word_comments', 'stock_list', 'is_important', 'duration', 'word_classify', 'share_link'].join(';'), + }, + headers: { + 'app-version': 'web1.0', + }, + }); + + const data = response.data.data; + + return { + title: '钛媒体 - 快报', + link: 'https://www.tmtpost.com/nictation', + item: data.map((item) => ({ + title: item.title, + description: item.detail, + pubDate: parseDate(item.time_published, 'X'), + link: item.share_link || `https://www.tmtpost.com/nictation/${item.guid}.html`, + author: item.author_name, + })), + }; +} diff --git a/lib/routes/tmtpost/templates/description.art b/lib/routes/tmtpost/templates/description.art new file mode 100644 index 00000000000000..57498ab45a9d86 --- /dev/null +++ b/lib/routes/tmtpost/templates/description.art @@ -0,0 +1,7 @@ +{{ if intro }} + <blockquote>{{ intro }}</blockquote> +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/tmtpost/util.ts b/lib/routes/tmtpost/util.ts new file mode 100644 index 00000000000000..ba28139bb4b28c --- /dev/null +++ b/lib/routes/tmtpost/util.ts @@ -0,0 +1,207 @@ +import { type Data, type DataItem } 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, load } from 'cheerio'; +import path from 'node:path'; + +const __dirname = getCurrentPath(import.meta.url); + +const baseUrl: string = 'https://www.tmtpost.com'; +const apiBaseUrl: string = 'https://api.tmtpost.com'; +const postApiUrl: string = new URL('v1/posts/', apiBaseUrl).href; + +const headers = { + 'app-version': 'web1.0', +}; + +const processItems = async (limit: number, query: Record<string, any>, apiUrl: string, targetUrl: string): Promise<Data> => { + const response = await ofetch(apiUrl, { + query: { + limit, + ...query, + }, + headers, + }); + + const targetResponse = await ofetch(targetUrl); + const $: CheerioAPI = load(targetResponse); + const language = $('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + items = response.data.slice(0, limit).map((item): DataItem => { + const title: string = item.title; + const description: string = art(path.join(__dirname, 'templates/description.art'), { + intro: item.summary, + }); + const pubDate: number | string = item.time_published; + const linkUrl: string | undefined = item.share_link; + const guid: string = item.guid; + const image: string | undefined = item.thumb_image?.original?.url; + const updated: number | string = item.updated ?? pubDate; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDate ? parseDate(pubDate, 'X') : undefined, + link: linkUrl, + guid, + id: guid, + content: { + html: description, + text: item.content ?? description, + }, + image, + banner: image, + updated: updated ? parseDate(updated, 'X') : undefined, + language, + }; + + return processedItem; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailApiUrl: string = new URL(item.guid as string, postApiUrl).href; + + const detailResponse = await ofetch(detailApiUrl, { + query: { + fields: ['authors', 'tags', 'featured_image', 'categories', 'stock_list', 'big_plate', 'concept_plate', 'plate', 'plate_list', 'share_link'].join(';'), + }, + headers, + }); + const data = detailResponse.data; + + if (!data) { + return item; + } + + const title: string = data.title; + const description: string = art(path.join(__dirname, 'templates/description.art'), { + intro: data.summary, + description: data.main, + }); + const pubDate: number | string = data.time_published; + const linkUrl: string | undefined = data.share_link; + const categories: string[] = [ + ...new Set( + ( + [...(data.categories ?? []), ...(data.stock_list ?? []), ...(data.big_plate ?? []), ...(data.concept_plate ?? []), ...(data.plate ?? []), ...(data.plate_list ?? []), ...(data.tags ?? [])].map( + (c) => c.title ?? c.name ?? c.tag + ) as string[] + ).filter(Boolean) + ), + ]; + const authors: DataItem['author'] = data.authors?.map((author) => ({ + name: author.username, + url: new URL(`user/${author.guid}`, baseUrl).href, + avatar: author.avatar, + })); + const guid: string = `tmtpost-${data.post_guid}`; + const image: string | undefined = data.images?.[0]?.url; + const updated: number | string = data.time_updated; + + let processedItem: DataItem = { + title, + description, + pubDate: pubDate ? parseDate(pubDate, 'X') : undefined, + link: linkUrl ?? item.link, + category: categories, + author: authors, + guid, + id: guid, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: updated ? parseDate(updated, 'X') : undefined, + language, + }; + + const enclosureUrl: string | undefined = data.audio; + + if (enclosureUrl) { + const enclosureType: string = `audio/${enclosureUrl.split(/\./).pop()}`; + const itunesDuration: string | number | undefined = data.duration; + + processedItem = { + ...processedItem, + enclosure_url: enclosureUrl, + enclosure_type: enclosureType, + enclosure_title: title, + enclosure_length: undefined, + itunes_duration: itunesDuration, + itunes_item_image: image, + }; + } + + const medias: Record<string, Record<string, string>> = {}; + + if (data.full_size_images ?? data.images) { + const images = data.full_size_images ?? data.images; + for (const media of images) { + const url: string | undefined = media.url ?? media; + + if (!url) { + continue; + } + + const medium: string = 'image'; + const count: number = Object.values(medias).filter((m) => m.medium === medium).length + 1; + const key: string = `${medium}${count}`; + + medias[key] = { + url, + medium, + title, + thumbnail: media.thumbnail ?? url, + }; + } + } + + processedItem = { + ...processedItem, + media: medias, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const title: string = $('title').text(); + const author: string | undefined = title.split(/-/).pop(); + + return { + title, + description: $('meta[property="og:description"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('meta[property="og:image"]').attr('content'), + author: title.split(/-/).pop(), + language, + itunes_author: author, + itunes_category: 'Technology', + id: targetUrl, + }; +}; + +export { baseUrl, apiBaseUrl, processItems }; diff --git a/lib/routes/tokeninsight/namespace.ts b/lib/routes/tokeninsight/namespace.ts index b8a9eec04624fb..6eb09aaecedc3e 100644 --- a/lib/routes/tokeninsight/namespace.ts +++ b/lib/routes/tokeninsight/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'TokenInsight', url: 'tokeninsight.com', - description: `:::tip + description: `::: tip TokenInsight also provides official RSS, you can take a look at [https://api.tokeninsight.com/reference/rss](https://api.tokeninsight.com/reference/rss). :::`, + lang: 'en', }; diff --git a/lib/routes/tokeninsight/report.ts b/lib/routes/tokeninsight/report.ts index e4182e4c725727..68eee16f9bd13a 100644 --- a/lib/routes/tokeninsight/report.ts +++ b/lib/routes/tokeninsight/report.ts @@ -31,9 +31,9 @@ export const route: Route = { handler, description: `Language: - | Chinese | English | - | ------- | ------- | - | zh | en |`, +| Chinese | English | +| ------- | ------- | +| zh | en |`, }; async function handler(ctx) { diff --git a/lib/routes/tongji/gs.ts b/lib/routes/tongji/gs.ts new file mode 100644 index 00000000000000..4d1917344ce157 --- /dev/null +++ b/lib/routes/tongji/gs.ts @@ -0,0 +1,66 @@ +import { Route } 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: '/gs', + categories: ['university'], + example: '/tongji/gs', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['gs.tongji.edu.cn/tzgg.htm', 'gs.tongji.edu.cn/'], + }, + ], + name: '研究生院通知公告', + maintainers: ['sitdownkevin'], + handler, + url: 'gs.tongji.edu.cn/tzgg.htm', +}; + +async function getNoticeContent(item) { + const response = await got(item.link); + const $ = load(response.body); + const content = $('#vsb_content').html(); + item.description = content; + return item; +} + +async function handler() { + const baseUrl = 'https://gs.tongji.edu.cn'; + const response = await got(`${baseUrl}/tzgg.htm`); + const $ = load(response.body); + const container = $('body > div > div.con_list.ma0a > div > div.list_content_right > div.list_list > ul'); + const items = container + .find('li') + .toArray() + .map((item) => { + const title = $(item).find('a').attr('title'); + const linkRaw = $(item).find('a').attr('href'); + const link = linkRaw.startsWith('http') ? linkRaw : `${baseUrl}/${linkRaw}`; + const pubDate = $(item).find('span').text(); + return { title, link, pubDate: parseDate(pubDate, 'YYYY-MM-DD') }; + }); + + const itemsWithContent = await Promise.all(items.map((item) => cache.tryGet(item.link, () => getNoticeContent(item)))); + + return { + title: '同济大学研究生院', + link: baseUrl, + description: '同济大学研究生院通知公告', + image: 'https://upload.wikimedia.org/wikipedia/zh/f/f8/Tongji_University_Emblem.svg', + icon: 'https://upload.wikimedia.org/wikipedia/zh/f/f8/Tongji_University_Emblem.svg', + logo: 'https://upload.wikimedia.org/wikipedia/zh/f/f8/Tongji_University_Emblem.svg', + item: itemsWithContent, + }; +} diff --git a/lib/routes/tongji/namespace.ts b/lib/routes/tongji/namespace.ts index b96d5051b91305..07b2d4a7364423 100644 --- a/lib/routes/tongji/namespace.ts +++ b/lib/routes/tongji/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '同济大学', url: 'bksy.tongji.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/tongji/sem/_utils.ts b/lib/routes/tongji/sem/_utils.ts new file mode 100644 index 00000000000000..090d1fce946d39 --- /dev/null +++ b/lib/routes/tongji/sem/_utils.ts @@ -0,0 +1,65 @@ +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { config } from '@/config'; + +export async function getNotifByPage(url): Promise<{ title: string; link: string; pubDate: Date }[]> { + const pageUrl: string = url; + + try { + const response = await got.get(pageUrl, { + headers: { + 'User-Agent': config.ua, + }, + }); + + const html = response.body; + const $ = load(html); + + const notifListElements = $('#page-wrap > div.maim_pages > div > div.leftmain_page > div > ul > li'); + + return notifListElements.toArray().map((Element) => { + const aTagFirst = $(Element).find('a.bt'); + const aTagSecond = $(Element).find('a.time'); + + const title = aTagFirst.attr('title'); + const href = aTagFirst.attr('href'); + const time = aTagSecond.text().trim(); + + return { + title, + link: href, + pubDate: parseDate(time, 'YYYY-MM-DD'), + }; + }); + } catch { + // console.error(error); + } + return []; +} + +export async function getArticle(item) { + const articleUrl: string = item.link; + + if (articleUrl.includes('sem.tongji.edu.cn/semch')) { + // console.log(articleUrl); + + try { + const response = await got.get(articleUrl, { + headers: { + 'User-Agent': config.ua, + }, + }); + + const html = response.body; + const $ = load(html); + + const articleContentElement = $('#page-wrap > div.maim_pages > div > div.leftmain_page > div'); + item.description = articleContentElement ? articleContentElement.html() : ''; + } catch { + // console.error(error); + } + } + + return item; +} diff --git a/lib/routes/tongji/sem/notice.ts b/lib/routes/tongji/sem/notice.ts new file mode 100644 index 00000000000000..790e67ce159ba5 --- /dev/null +++ b/lib/routes/tongji/sem/notice.ts @@ -0,0 +1,58 @@ +// Warning: The author still knows nothing about javascript! +import { Route } from '@/types'; +import { getNotifByPage, getArticle } from './_utils'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/sem/:type?', + categories: ['university'], + example: '/tongji/sem/notice', + parameters: { type: '通知类型,默认为 `notice`' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '经济与管理学院通知', + maintainers: ['sitdownkevin'], + url: 'sem.tongji.edu.cn/semch', + handler, + description: `| 学院通知 | 招生通知 | 学术观点 | 新闻 | 活动 | 视点 | 教师与行政人员招聘 | +| -------- | -------------- | ------------------ | ---- | ---------- | --------- | ------------------ | +| notice | enrollment | academic-paper | news | events | focus | collegerecruitment | +`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type') || 'notice'; + const subType = new Set(['enrollment', 'academic-paper', 'news', 'events', 'focus', 'collegerecruitment']); + const subTypeName = { + notice: '学院通知', + enrollment: '招生通知', + 'academic-paper': '学术观点', + news: '新闻', + events: '活动', + focus: '视点', + collegerecruitment: '教师与行政人员招聘', + }; + + const url = `https://sem.tongji.edu.cn/semch/category/frontpage/${subType.has(type) ? type : 'notice'}`; + + const results: { title: string; link: string; pubDate: Date }[] = await getNotifByPage(url); + + const resultsWithContent = await Promise.all(results.map((item) => cache.tryGet(item.link, () => getArticle(item)))); + + // feed the data to rss + return { + title: '同济大学经济与管理学院', + description: String(subType.has(type) ? subTypeName[type] : '学院通知'), + image: 'https://upload.wikimedia.org/wikipedia/zh/f/f8/Tongji_University_Emblem.svg', + icon: 'https://upload.wikimedia.org/wikipedia/zh/f/f8/Tongji_University_Emblem.svg', + logo: 'https://upload.wikimedia.org/wikipedia/zh/f/f8/Tongji_University_Emblem.svg', + link: 'https://sem.tongji.edu.cn/semch', + item: resultsWithContent, + }; +} diff --git a/lib/routes/tongji/sse/notice.ts b/lib/routes/tongji/sse/notice.ts index 920441bba5392f..87beaf7b79b81a 100644 --- a/lib/routes/tongji/sse/notice.ts +++ b/lib/routes/tongji/sse/notice.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['sgqy'], handler, description: `| 本科生通知 | 研究生通知 | 教工通知 | 全体通知 | 学院通知 | 学院新闻 | 学院活动 | - | ---------- | ---------- | -------- | -------- | -------- | -------- | -------- | - | bkstz | yjstz | jgtz | qttz | xytz | xyxw | xyhd | +| ---------- | ---------- | -------- | -------- | -------- | -------- | -------- | +| bkstz | yjstz | jgtz | qttz | xytz | xyxw | xyhd | 注意: \`qttz\` 与 \`xytz\` 在原网站等价.`, }; diff --git a/lib/routes/tongji/yjs.ts b/lib/routes/tongji/yjs.ts index 63985774533852..afdfb0a013da70 100644 --- a/lib/routes/tongji/yjs.ts +++ b/lib/routes/tongji/yjs.ts @@ -2,6 +2,7 @@ import { Route } 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: '/yjs', @@ -21,32 +22,45 @@ export const route: Route = { source: ['yz.tongji.edu.cn/zsxw/ggtz.htm', 'yz.tongji.edu.cn/'], }, ], - name: '研究生院通知公告', - maintainers: ['shengmaosu'], + name: '研究生招生网通知公告', + maintainers: ['shengmaosu', 'sitdownkevin'], handler, url: 'yz.tongji.edu.cn/zsxw/ggtz.htm', }; -async function handler() { - const link = 'https://yz.tongji.edu.cn/zsxw/ggtz.htm'; - const response = await got(link); +async function getNoticeContent(item) { + const response = await got(item.link); const $ = load(response.data); - const list = $('.list_main_content li'); + const content = $('#vsb_content').html(); + item.description = content; + return item; +} + +async function handler() { + const baseUrl = 'https://yz.tongji.edu.cn'; + const response = await got(`${baseUrl}/zsxw/ggtz.htm`); + const $ = load(response.body); + const container = $('#content-box > div.content > div.list_main_content > ul'); + const items = container + .find('li') + .toArray() + .map((item) => { + const title = $(item).find('a').attr('title'); + const linkRaw = $(item).find('a').attr('href'); + const link = linkRaw.startsWith('http') ? linkRaw : new URL(linkRaw, `${baseUrl}/zsxw`).toString(); + const pubDate = $(item).find('span').text(); + return { title, link, pubDate: parseDate(pubDate, 'YYYY-MM-DD') }; + }); + + const itemsWithContent = await Promise.all(items.map((item) => cache.tryGet(item.link, () => getNoticeContent(item)))); return { - title: '同济大学研究生院', - link, - description: '同济大学研究生院通知公告', - item: - list && - list.toArray().map((item) => { - item = $(item); - const a = item.find('a'); - return { - title: a.attr('title'), - link: new URL(a.attr('href'), link).href, - pubDate: parseDate(item.find('span').text(), 'YYYY-MM-DD'), - }; - }), + title: '同济大学研究生招生网', + link: baseUrl, + description: '同济大学研究生招生网通知公告', + image: 'https://upload.wikimedia.org/wikipedia/zh/f/f8/Tongji_University_Emblem.svg', + icon: 'https://upload.wikimedia.org/wikipedia/zh/f/f8/Tongji_University_Emblem.svg', + logo: 'https://upload.wikimedia.org/wikipedia/zh/f/f8/Tongji_University_Emblem.svg', + item: itemsWithContent, }; } diff --git a/lib/routes/tongli/namespace.ts b/lib/routes/tongli/namespace.ts index 659eef18926060..ced81f990ef597 100644 --- a/lib/routes/tongli/namespace.ts +++ b/lib/routes/tongli/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '東立出版社', url: 'tongli.com.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/toodaylab/namespace.ts b/lib/routes/toodaylab/namespace.ts index be0d4305fd9039..7bbf73e3c18abb 100644 --- a/lib/routes/toodaylab/namespace.ts +++ b/lib/routes/toodaylab/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '理想生活实验室', url: 'toodaylab.com', + lang: 'zh-CN', }; diff --git a/lib/routes/tophub/index.ts b/lib/routes/tophub/index.ts index e98709d68b778c..05afc6f9b6b6bb 100644 --- a/lib/routes/tophub/index.ts +++ b/lib/routes/tophub/index.ts @@ -5,7 +5,7 @@ import { config } from '@/config'; export const route: Route = { path: '/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/tophub/Om4ejxvxEN', parameters: { id: '榜单id,可在 URL 中找到' }, features: { diff --git a/lib/routes/tophub/list.ts b/lib/routes/tophub/list.ts index 975c21e79333aa..48eac902b3bd1e 100644 --- a/lib/routes/tophub/list.ts +++ b/lib/routes/tophub/list.ts @@ -10,7 +10,7 @@ import { art } from '@/utils/render'; export const route: Route = { path: '/list/:id', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/tophub/list/Om4ejxvxEN', parameters: { id: '榜单id,可在 URL 中找到' }, features: { @@ -35,7 +35,7 @@ export const route: Route = { name: '榜单列表', maintainers: ['akynazh'], handler, - description: `:::tip + description: `::: tip 将榜单条目集合到一个列表中,可避免推送大量条目,更符合阅读习惯且有热度排序,推荐使用。 :::`, }; diff --git a/lib/routes/tophub/namespace.ts b/lib/routes/tophub/namespace.ts index 02f7a70864a889..82d3aedf062456 100644 --- a/lib/routes/tophub/namespace.ts +++ b/lib/routes/tophub/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '今日热榜', url: 'tophub.today', - description: `:::warning + description: `::: warning 由于需要登录后的 Cookie 值才能获取原始链接,所以需要自建,需要在环境变量中配置 \`TOPHUB_COOKIE\`,详情见部署页面的配置模块。 :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/topys/index.ts b/lib/routes/topys/index.ts index 2fb933d31bc463..1428fb3d91e170 100644 --- a/lib/routes/topys/index.ts +++ b/lib/routes/topys/index.ts @@ -26,7 +26,7 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 创意 | 设计 | 商业 | 艺术 | 文化 | 科技 | - | ---- | ---- | ---- | ---- | ---- | ---- |`, +| ---- | ---- | ---- | ---- | ---- | ---- |`, }; async function handler(ctx) { diff --git a/lib/routes/topys/namespace.ts b/lib/routes/topys/namespace.ts index 64869c7be9f2d4..9be0bee330ed2a 100644 --- a/lib/routes/topys/namespace.ts +++ b/lib/routes/topys/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'TOPYS', url: 'topys.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/toranoana/namespace.ts b/lib/routes/toranoana/namespace.ts new file mode 100644 index 00000000000000..3d48496571ca86 --- /dev/null +++ b/lib/routes/toranoana/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'とらのあな', + url: 'toranoana.jp', + lang: 'ja', +}; diff --git a/lib/routes/toranoana/news.ts b/lib/routes/toranoana/news.ts new file mode 100644 index 00000000000000..404c1f513f5a54 --- /dev/null +++ b/lib/routes/toranoana/news.ts @@ -0,0 +1,110 @@ +import { Route, Data, DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/news/:category?', + categories: ['anime'], + example: '/toranoana/news/toragen', + parameters: { category: 'category' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Category', + maintainers: ['Tsuyumi25'], + handler, + radar: [ + { + title: '総合新着記事', + source: ['news.toranoana.jp'], + target: '/news', + }, + { + title: '女性向け', + source: ['news.toranoana.jp/joshi'], + target: '/news/joshi', + }, + { + title: 'イラスト展', + source: ['news.toranoana.jp/exhibitions'], + target: '/news/exhibition', + }, + { + source: ['news.toranoana.jp/category/:category'], + target: '/news/:category', + }, + ], + description: ` +::: warning TIP +[総合新着記事](https://news.toranoana.jp)→\`/toranoana/news\` +[女性向け](https://news.toranoana.jp/joshi)→\`/toranoana/news/joshi\` +[イラスト展](https://news.toranoana.jp/exhibitions)→\`/toranoana/news/exhibition\` +[\`https://news.toranoana.jp/category/media\`](https://news.toranoana.jp/category/media)→\`/toranoana/news/media\` +:::`, +}; + +async function handler(ctx): Promise<Data> { + const { category = '' } = ctx.req.param(); + let apiUrl = 'https://news.toranoana.jp/wp-json/wp/v2/posts'; + + if (category) { + const categoryResponse = await ofetch(`https://news.toranoana.jp/wp-json/wp/v2/categories?slug=${category}`); + if (categoryResponse && categoryResponse.length > 0) { + apiUrl += `?categories=${categoryResponse[0].id}`; + } + } else { + // exclude category-joshi to get result of general + apiUrl += `?categories_exclude=1598`; + } + + const posts = await ofetch(apiUrl, { + query: { + per_page: 20, + _embed: 'wp:featuredmedia', + }, + }); + + if (!posts || !posts.length) { + throw new Error('No posts found'); + } + + const items = posts.map((post) => { + const $ = load(post.content.rendered); + + // remove unnecessary title + $('h1').first().remove(); + $('h2').first().remove(); + + let thumbnail = ''; + if (post._embedded && post._embedded['wp:featuredmedia'][0].source_url) { + thumbnail = post._embedded['wp:featuredmedia'][0].source_url; + } + + if (thumbnail) { + $('body').prepend(`<img src="${thumbnail}" alt="${post.title.rendered}" />`); + } + + return { + title: post.title.rendered, + link: post.link, + description: $.html(), + pubDate: parseDate(post.date_gmt), + guid: post.link, + author: 'とらのあな', + }; + }); + + return { + title: category ? `とらのあな総合インフォメーション - ${category}` : 'とらのあな総合インフォメーション', + link: category ? `https://news.toranoana.jp/category/${category}` : 'https://news.toranoana.jp/', + description: 'とらのあなの最新情報をお届け!同人誌、書籍、コミック、店舗フェア、イラスト展、とらのあな限定版、キャンペーンなど…スペシャルでお得な情報をいち早くチェック!', + item: items.filter(Boolean) as DataItem[], + language: 'ja', + }; +} diff --git a/lib/routes/toutiao/a-bogus.ts b/lib/routes/toutiao/a-bogus.ts new file mode 100644 index 00000000000000..5f13a59d4581b2 --- /dev/null +++ b/lib/routes/toutiao/a-bogus.ts @@ -0,0 +1,540 @@ +/* eslint-disable unicorn/prefer-spread */ +/* eslint-disable unicorn/prefer-math-trunc */ +// @ts-nocheck + +// Credits: +// https://github.com/NearHuiwen/TiktokDouyinCrawler/blob/main/utils/a_bogus.js +// https://github.com/110Art/a-bogus/blob/main/a_bogus.js +// https://github.com/ShilongLee/Crawler/blob/main/lib/js/douyin.js + +// Reference: +// https://github.com/Endy-c/gm-crypt/blob/87bfc13f4b234c538d56798ed2457da16bc006ac/src/sm3.js + +import logger from '@/utils/logger'; + +function rc4_encrypt(plaintext, key) { + const s: number[] = []; + for (let i = 0; i < 256; i++) { + s[i] = i; + } + for (let i = 0, j = 0; i < 256; i++) { + j = (j + s[i] + key.codePointAt(i % key.length)) % 256; + const temp = s[i]; + s[i] = s[j]; + s[j] = temp; + } + + const cipher: string[] = []; + for (let i = 0, j = 0, k = 0; k < plaintext.length; k++) { + i = (i + 1) % 256; + j = (j + s[i]) % 256; + const temp = s[i]; + s[i] = s[j]; + s[j] = temp; + const t = (s[i] + s[j]) % 256; + cipher.push(String.fromCodePoint(s[t] ^ plaintext.codePointAt(k))); + } + return cipher.join(''); +} + +function rotateLeft32(e, r) { + return ((e << (r %= 32)) | (e >>> (32 - r))) >>> 0; +} + +function T(j) { + if (0 <= j && j < 16) { + return 0x79_CC_45_19; + } else if (16 <= j && j < 64) { + return 0x7A_87_9D_8A; + } else { + logger.error('invalid j for constant Tj'); + } +} + +function FF(j, x, y, z) { + if (0 <= j && j < 16) { + return (x ^ y ^ z) >>> 0; + } else if (16 <= j && j < 64) { + return ((x & y) | (x & z) | (y & z)) >>> 0; + } else { + logger.error('invalid j for bool function FF'); + return 0; + } +} + +function GG(j, x, y, z) { + if (0 <= j && j < 16) { + return (x ^ y ^ z) >>> 0; + } else if (16 <= j && j < 64) { + return ((x & y) | (~x & z)) >>> 0; + } else { + logger.error('invalid j for bool function GG'); + return 0; + } +} + +function reset(this: any) { + this.reg[0] = 0x73_80_16_6F; + this.reg[1] = 0x49_14_B2_B9; + this.reg[2] = 0x17_24_42_D7; + this.reg[3] = 0xDA_8A_06_00; + this.reg[4] = 0xA9_6F_30_BC; + this.reg[5] = 0x16_31_38_AA; + this.reg[6] = 0xE3_8D_EE_4D; + this.reg[7] = 0xB0_FB_0E_4E; + this.chunk = []; + this.size = 0; +} + +function strToBytes(str) { + const n = encodeURIComponent(str).replaceAll(/%([0-9A-F]{2})/g, (e, r) => String.fromCodePoint('0x' + r)); + const a = Array.from({ length: n.length }); + Array.prototype.forEach.call(n, (e, r) => { + a[r] = e.codePointAt(0); + }); + return a; +} + +function write(this: any, message) { + const a = typeof message === 'string' ? strToBytes(message) : message; + this.size += a.length; + let f = 64 - this.chunk.length; + if (a.length < f) { + this.chunk = this.chunk.concat(a); + } else { + this.chunk = this.chunk.concat(a.slice(0, f)); + while (this.chunk.length >= 64) { + this._compress(this.chunk); + this.chunk = f < a.length ? a.slice(f, Math.min(f + 64, a.length)) : []; + f += 64; + } + } +} + +function sum(this: any, message, encoding) { + if (message) { + this.reset(); + this.write(message); + } + this._fill(); + for (let f = 0; f < this.chunk.length; f += 64) { + this._compress(this.chunk.slice(f, f + 64)); + } + let digest; + if (encoding === 'hex') { + digest = ''; + for (let f = 0; f < 8; f++) { + digest += se(this.reg[f].toString(16), 8, '0'); + } + } else { + digest = Array.from({ length: 32 }); + for (let f = 0; f < 8; f++) { + let c = this.reg[f]; + digest[4 * f + 3] = (255 & c) >>> 0; + c >>>= 8; + digest[4 * f + 2] = (255 & c) >>> 0; + c >>>= 8; + digest[4 * f + 1] = (255 & c) >>> 0; + c >>>= 8; + digest[4 * f] = (255 & c) >>> 0; + } + } + this.reset(); + return digest; +} + +function expand(e) { + const r: number[] = Array.from({ length: 132 }); + for (let t = 0; t < 16; t++) { + r[t] = e[4 * t] << 24; + r[t] |= e[4 * t + 1] << 16; + r[t] |= e[4 * t + 2] << 8; + r[t] |= e[4 * t + 3]; + r[t] >>>= 0; + } + for (let n = 16; n < 68; n++) { + let a = r[n - 16] ^ r[n - 9] ^ rotateLeft32(r[n - 3], 15); + a = a ^ rotateLeft32(a, 15) ^ rotateLeft32(a, 23); + r[n] = (a ^ rotateLeft32(r[n - 13], 7) ^ r[n - 6]) >>> 0; + } + for (let n = 0; n < 64; n++) { + r[n + 68] = (r[n] ^ r[n + 4]) >>> 0; + } + return r; +} + +function _compress(this: any, t) { + if (t < 64) { + logger.error('compress error: not enough data'); + return; + } else { + const f = expand(t); + const i = this.reg.slice(0); + for (let c = 0; c < 64; c++) { + let o = rotateLeft32(i[0], 12) + i[4] + rotateLeft32(T(c), c); + o = (0xFF_FF_FF_FF & o) >>> 0; + o = rotateLeft32(o, 7); + + const s = (o ^ rotateLeft32(i[0], 12)) >>> 0; + let u = FF(c, i[0], i[1], i[2]); + u = u + i[3] + s + f[c + 68]; + u = (0xFF_FF_FF_FF & u) >>> 0; + + let b = GG(c, i[4], i[5], i[6]); + b = b + i[7] + o + f[c]; + b = (0xFF_FF_FF_FF & b) >>> 0; + i[3] = i[2]; + i[2] = rotateLeft32(i[1], 9); + i[1] = i[0]; + i[0] = u; + i[7] = i[6]; + i[6] = rotateLeft32(i[5], 19); + i[5] = i[4]; + i[4] = (b ^ rotateLeft32(b, 9) ^ rotateLeft32(b, 17)) >>> 0; + } + for (let l = 0; l < 8; l++) { + this.reg[l] = (this.reg[l] ^ i[l]) >>> 0; + } + } +} + +function _fill(this: any) { + const a = 8 * this.size; + let f = this.chunk.push(128) % 64; + for (64 - f < 8 && (f -= 64); f < 56; f++) { + this.chunk.push(0); + } + for (let i = 0; i < 4; i++) { + const c = Math.floor(a / 0x1_00_00_00_00); + this.chunk.push((c >>> (8 * (3 - i))) & 0xFF); + } + for (let i = 0; i < 4; i++) { + this.chunk.push((a >>> (8 * (3 - i))) & 0xFF); + } +} + +function SM3(this: any) { + this.reg = []; + this.chunk = []; + this.size = 0; + this.reset(); +} +SM3.prototype.reset = reset; +SM3.prototype.write = write; +SM3.prototype.sum = sum; +SM3.prototype._compress = _compress; +SM3.prototype._fill = _fill; + +function result_encrypt(long_str: string, num: 's0' | 's1' | 's2' | 's3' | 's4') { + const s_obj = { + s0: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', + s1: 'Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=', + s2: 'Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=', + s3: 'ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe', + s4: 'Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe', + }; + const constant = { + '0': 16_515_072, + '1': 258048, + '2': 4032, + str: s_obj[num], + }; + + let result = ''; + let lound = 0; + let long_int = get_long_int(lound, long_str); + for (let i = 0; i < (long_str.length / 3) * 4; i++) { + if (Math.floor(i / 4) !== lound) { + lound += 1; + long_int = get_long_int(lound, long_str); + } + const key = i % 4; + let temp_int: number; + switch (key) { + case 0: + temp_int = (long_int & constant['0']) >> 18; + result += constant.str.charAt(temp_int); + break; + case 1: + temp_int = (long_int & constant['1']) >> 12; + result += constant.str.charAt(temp_int); + break; + case 2: + temp_int = (long_int & constant['2']) >> 6; + result += constant.str.charAt(temp_int); + break; + case 3: + temp_int = long_int & 63; + result += constant.str.charAt(temp_int); + break; + default: + break; + } + } + return result; +} + +function get_long_int(round, long_str) { + round = round * 3; + return (long_str.codePointAt(round) << 16) | (long_str.codePointAt(round + 1) << 8) | long_str.codePointAt(round + 2); +} + +function gener_random(random, option) { + return [ + (random & 255 & 170) | (option[0] & 85), // 163 + (random & 255 & 85) | (option[0] & 170), // 87 + ((random >> 8) & 255 & 170) | (option[1] & 85), // 37 + ((random >> 8) & 255 & 85) | (option[1] & 170), // 41 + ]; +} + +// //////////////////////////////////////////// +function generate_rc4_bb_str(url_search_params, user_agent, window_env_str, suffix = 'cus', Arguments = [0, 1, 14]) { + const sm3 = new SM3(); + const start_time = Date.now(); + /** + * 进行3次加密处理 + * 1: url_search_params两次sm3之的结果 + * 2: 对后缀两次sm3之的结果 + * 3: 对ua处理之后的结果 + */ + // url_search_params两次sm3之的结果 + const url_search_params_list = sm3.sum(sm3.sum(url_search_params + suffix)); + // 对后缀两次sm3之的结果 + const cus = sm3.sum(sm3.sum(suffix)); + // 对ua处理之后的结果 + const ua = sm3.sum(result_encrypt(rc4_encrypt(user_agent, Reflect.apply(String.fromCharCode, null, [0.003_906_25, 1, 14])), 's3')); + // + const end_time = Date.now(); + // b + const b = { + 8: 3, // 固定 + 10: end_time, // 3次加密结束时间 + 15: { + aid: 6383, + pageId: 6241, + boe: false, + ddrt: 7, + paths: { + include: [{}, {}, {}, {}, {}, {}, {}], + exclude: [], + }, + track: { + mode: 0, + delay: 300, + paths: [], + }, + dump: true, + rpU: '', + }, + 16: start_time, // 3次加密开始时间 + 18: 44, // 固定 + 19: [1, 0, 1, 5], + }; + + // 3次加密开始时间 + b[20] = (b[16] >> 24) & 255; + b[21] = (b[16] >> 16) & 255; + b[22] = (b[16] >> 8) & 255; + b[23] = b[16] & 255; + b[24] = (b[16] / 256 / 256 / 256 / 256) >> 0; + b[25] = (b[16] / 256 / 256 / 256 / 256 / 256) >> 0; + + // 参数Arguments [0, 1, 14, ...] + // let Arguments = [0, 1, 14] + b[26] = (Arguments[0] >> 24) & 255; + b[27] = (Arguments[0] >> 16) & 255; + b[28] = (Arguments[0] >> 8) & 255; + b[29] = Arguments[0] & 255; + + b[30] = (Arguments[1] / 256) & 255; + b[31] = Arguments[1] % 256 & 255; + b[32] = (Arguments[1] >> 24) & 255; + b[33] = (Arguments[1] >> 16) & 255; + + b[34] = (Arguments[2] >> 24) & 255; + b[35] = (Arguments[2] >> 16) & 255; + b[36] = (Arguments[2] >> 8) & 255; + b[37] = Arguments[2] & 255; + + // (url_search_params + "cus") 两次sm3之的结果 + /** let url_search_params_list = [ + 91, 186, 35, 86, 143, 253, 6, 76, + 34, 21, 167, 148, 7, 42, 192, 219, + 188, 20, 182, 85, 213, 74, 213, 147, + 37, 155, 93, 139, 85, 118, 228, 213 + ]*/ + b[38] = url_search_params_list[21]; + b[39] = url_search_params_list[22]; + + // ("cus") 对后缀两次sm3之的结果 + /** + * let cus = [ + 136, 101, 114, 147, 58, 77, 207, 201, + 215, 162, 154, 93, 248, 13, 142, 160, + 105, 73, 215, 241, 83, 58, 51, 43, + 255, 38, 168, 141, 216, 194, 35, 236 + ]*/ + b[40] = cus[21]; + b[41] = cus[22]; + + // 对ua处理之后的结果 + /** + * let ua = [ + 129, 190, 70, 186, 86, 196, 199, 53, + 99, 38, 29, 209, 243, 17, 157, 69, + 147, 104, 53, 23, 114, 126, 66, 228, + 135, 30, 168, 185, 109, 156, 251, 88 + ]*/ + b[42] = ua[23]; + b[43] = ua[24]; + + // 3次加密结束时间 + b[44] = (b[10] >> 24) & 255; + b[45] = (b[10] >> 16) & 255; + b[46] = (b[10] >> 8) & 255; + b[47] = b[10] & 255; + b[48] = b[8]; + b[49] = (b[10] / 256 / 256 / 256 / 256) >> 0; + b[50] = (b[10] / 256 / 256 / 256 / 256 / 256) >> 0; + + // object配置项 + b[51] = b[15].pageId; + b[52] = (b[15].pageId >> 24) & 255; + b[53] = (b[15].pageId >> 16) & 255; + b[54] = (b[15].pageId >> 8) & 255; + b[55] = b[15].pageId & 255; + + b[56] = b[15].aid; + b[57] = b[15].aid & 255; + b[58] = (b[15].aid >> 8) & 255; + b[59] = (b[15].aid >> 16) & 255; + b[60] = (b[15].aid >> 24) & 255; + + // 中间进行了环境检测 + // 代码索引: 2496 索引值: 17 (索引64关键条件) + // '1536|747|1536|834|0|30|0|0|1536|834|1536|864|1525|747|24|24|Win32'.charCodeAt()得到65位数组 + /** + * let window_env_list = [49, 53, 51, 54, 124, 55, 52, 55, 124, 49, 53, 51, 54, 124, 56, 51, 52, 124, 48, 124, 51, + * 48, 124, 48, 124, 48, 124, 49, 53, 51, 54, 124, 56, 51, 52, 124, 49, 53, 51, 54, 124, 56, + * 54, 52, 124, 49, 53, 50, 53, 124, 55, 52, 55, 124, 50, 52, 124, 50, 52, 124, 87, 105, 110, + * 51, 50] + */ + const window_env_list: number[] = []; + for (let index = 0; index < window_env_str.length; index++) { + window_env_list.push(window_env_str.codePointAt(index)); + } + b[64] = window_env_list.length; + b[65] = b[64] & 255; + b[66] = (b[64] >> 8) & 255; + + b[69] = [].length; + b[70] = b[69] & 255; + b[71] = (b[69] >> 8) & 255; + + b[72] = + b[18] ^ + b[20] ^ + b[26] ^ + b[30] ^ + b[38] ^ + b[40] ^ + b[42] ^ + b[21] ^ + b[27] ^ + b[31] ^ + b[35] ^ + b[39] ^ + b[41] ^ + b[43] ^ + b[22] ^ + b[28] ^ + b[32] ^ + b[36] ^ + b[23] ^ + b[29] ^ + b[33] ^ + b[37] ^ + b[44] ^ + b[45] ^ + b[46] ^ + b[47] ^ + b[48] ^ + b[49] ^ + b[50] ^ + b[24] ^ + b[25] ^ + b[52] ^ + b[53] ^ + b[54] ^ + b[55] ^ + b[57] ^ + b[58] ^ + b[59] ^ + b[60] ^ + b[65] ^ + b[66] ^ + b[70] ^ + b[71]; + let bb = [ + b[18], + b[20], + b[52], + b[26], + b[30], + b[34], + b[58], + b[38], + b[40], + b[53], + b[42], + b[21], + b[27], + b[54], + b[55], + b[31], + b[35], + b[57], + b[39], + b[41], + b[43], + b[22], + b[28], + b[32], + b[60], + b[36], + b[23], + b[29], + b[33], + b[37], + b[44], + b[45], + b[59], + b[46], + b[47], + b[48], + b[49], + b[50], + b[24], + b[25], + b[65], + b[66], + b[70], + b[71], + ]; + bb = bb.concat(window_env_list).concat(b[72]); + return rc4_encrypt(String.fromCharCode.apply(null, bb), Reflect.apply(String.fromCharCode, null, [121])); +} + +function generate_random_str() { + let random_str_list: number[] = []; + random_str_list = random_str_list.concat(gener_random(Math.random() * 10000, [3, 45])); + random_str_list = random_str_list.concat(gener_random(Math.random() * 10000, [1, 0])); + random_str_list = random_str_list.concat(gener_random(Math.random() * 10000, [1, 5])); + return String.fromCharCode.apply(null, random_str_list); +} + +export function generate_a_bogus(url_search_params, user_agent) { + const result_str = generate_random_str() + generate_rc4_bb_str(url_search_params, user_agent, '1536|747|1536|834|0|30|0|0|1536|834|1536|864|1525|747|24|24|Win32'); + return result_encrypt(result_str, 's4') + '='; +} diff --git a/lib/routes/toutiao/namespace.ts b/lib/routes/toutiao/namespace.ts new file mode 100644 index 00000000000000..5f89ec4ccd17dd --- /dev/null +++ b/lib/routes/toutiao/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '今日头条', + url: 'www.toutiao.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/toutiao/templates/video.art b/lib/routes/toutiao/templates/video.art new file mode 100644 index 00000000000000..1b007c35c256e7 --- /dev/null +++ b/lib/routes/toutiao/templates/video.art @@ -0,0 +1,7 @@ +<video controls preload="metadata" + {{ if poster }} + poster="{{ poster }}" + {{ /if }} +> + <source src="{{ url }}" type="video/mp4" /> +</video> diff --git a/lib/routes/toutiao/types.ts b/lib/routes/toutiao/types.ts new file mode 100644 index 00000000000000..e3ede3b0d650a9 --- /dev/null +++ b/lib/routes/toutiao/types.ts @@ -0,0 +1,443 @@ +export interface Feed { + abstract: string; + aggr_type: number; + article_sub_type: number; + article_type: number; + article_url: string; + article_version: number; + ban_comment: boolean; + behot_time: number; + bury_count: number; + bury_style_show: number; + cell_ctrls: CellCtrls; + cell_flag: number; + cell_layout_style: number; + /** + * 0: video + * 32: text (w) + * 49: video + * 60: article + */ + cell_type: number; + comment_count: number; + common_raw_data: string; + /** + * Appears only if cell_type is 32 + */ + content: string; + content_decoration: string; + control_meta: ControlMeta; + cursor: number; + data_type: number; + digg_count: number; + display_url: string; + forum_extra_data: string; + forward_info: ForwardInfo; + gallary_image_count: number; + group_flags: number; + group_id: string; + group_source: number; + group_type: number; + has_image: boolean; + has_m3u8_video: boolean; + has_mp4_video: boolean; + has_video: boolean; + hot: number; + id: string; + is_original: boolean; + itemCell: ItemCell; + itemCellDebug: null; + item_id: string; + item_id_str: string; + item_version: number; + label_style: number; + level: number; + like_count: number; + log_pb: LogPb; + lynx_server: LynxServer; + natant_level: number; + preload_web: number; + publish_time: number; + /** + * Appears only if cell_type is 32 + */ + rich_content: string; + read_count: number; + reback_flag: number; + repin_count: number; + repin_time: number; + req_id: string; + share_url: string; + show_more: ShowMore; + source: string; + subject_group_id: number; + tag_id: number; + tip: number; + title: string; + url: string; + /** + * Appears only if cell_type is 32, 49(0) + */ + user: UserInfoCell32 | UserInfoCell49; + user_bury: number; + user_digg: number; + /** + * Appears only if cell_type is 0, 60 + */ + user_info: UserInfoCell60; + user_like: number; + user_repin: number; + user_repin_time: number; + /** + * Appears only if cell_type is 0, 49 + */ + video: Video; + video_duration: number; + video_style: number; + image_list: ImageListItem[]; + large_image_list: LargeImageListItem[]; + middle_image: MiddleImage; + video_detail_info: VideoDetailInfo; +} + +interface CellCtrls { + cell_flag: number; + cell_height: number; + cell_layout_style: number; +} + +interface ControlMeta { + modify: Modify; + remove: Remove; + share: Share; +} + +interface Modify { + hide: boolean; + name: string; + permission: boolean; + tips: string; +} + +interface Remove { + hide: boolean; + name: string; + permission: boolean; + tips: string; +} + +interface Share { + hide: boolean; + name: string; + permission: boolean; + tips: string; +} + +interface ForwardInfo { + forward_count: number; +} + +interface ItemCell { + actionCtrl: ActionCtrl; + articleBase: ArticleBase; + articleClassification: ArticleClassification; + cellCtrl: CellCtrl; + extra: Extra; + imageList: ImageList; + itemCounter: ItemCounter; + locationInfo: LocationInfo; + shareInfo: ShareInfo; + tagInfo: TagInfo; + userInteraction: UserInteraction; + videoInfo: VideoInfo; +} + +interface ActionCtrl { + actionBar: ActionBar; + banBury: boolean; + banComment: boolean; + banDigg: boolean; + controlMeta: ActionControlMeta; +} + +interface ActionBar { + actionSettingList: ActionSetting[]; +} + +interface ActionSetting { + actionType: number; + styleSetting: StyleSetting; +} + +interface StyleSetting { + iconKey: string; + layoutDirection: number; + text: string; +} + +interface ActionControlMeta { + modify: ActionModify; + remove: ActionRemove; + share: ActionShare; +} + +interface ActionModify { + permission: boolean; + tips: string; +} + +interface ActionRemove { + permission: boolean; + tips: string; +} + +interface ActionShare { + permission: boolean; + tips: string; +} + +interface ArticleBase { + gidStr: string; + itemStatus: number; +} + +interface ArticleClassification { + aggrType: number; + articleSubType: number; + articleType: number; + bizID: number; + bizTag: number; + groupSource: number; + isForAudioPlaylist: boolean; + isOriginal: boolean; + isSubject: boolean; + level: number; +} + +interface CellCtrl { + buryStyleShow: number; + cellFlag: number; + cellLayoutStyle: number; + cellType: number; + cellUIType: string; + groupFlags: number; +} + +interface Extra { + ping: string; +} + +type ImageList = unknown; + +interface ItemCounter { + commentCount: number; + diggCount: number; + forwardCount: number; + readCount: number; + repinCount: number; + shareCount: number; + showCount: number; + textCount: number; + videoWatchCount: number; + buryCount: number; +} + +interface LocationInfo { + publishLocInfo: string; +} + +interface ShareInfo { + shareURL: string; + shareControl: ShareControl; +} + +interface ShareControl { + isHighQuality: boolean; +} + +type TagInfo = unknown; + +interface UserInteraction { + userDigg: boolean; + userRepin: boolean; +} + +type VideoInfo = unknown; + +interface Video { + bitrate: number; + codec_type: string; + definition: string; + download_addr: DownloadAddr; + duration: number; + encode_user_tag: string; + file_hash: string; + height: number; + origin_cover: OriginCover; + play_addr: PlayAddr; + play_addr_list: PlayAddrListItem[]; + ratio: string; + size: number; + url_expire: number; + video_id: string; + volume: Volume; + vtype: string; + width: number; +} + +interface DownloadAddr { + uri: string; + url_list: string[]; +} + +interface OriginCover { + uri: string; + url_list: string[]; +} + +interface PlayAddr { + uri: string; + url_list: string[]; +} + +interface PlayAddrListItem { + bitrate: number; + codec_type: string; + definition: string; + encode_user_tag: string; + file_hash: string; + play_url_list: string[]; + quality: string; + size: number; + url_expire: number; + video_quality: number; + volume: Volume; + vtype: string; +} + +interface Volume { + Loudness: number; + Peak: number; +} + +interface LogPb { + cell_layout_style: string; + group_id_str: string; + group_source: string; + impr_id: string; + is_following: string; + is_yaowen: string; +} + +type LynxServer = unknown; + +interface ShowMore { + title: string; + url: string; +} + +interface UserInfoCell32 { + avatar_url: string; + desc: string; + id: number; + is_blocked: number; + is_blocking: number; + is_followed: number; + is_following: number; + is_friend: number; + live_info_type: number; + medals: unknown; + name: string; + remark_name: string; + schema: string; + screen_name: string; + theme_day: string; + user_auth_info: string; + user_decoration: string; + user_id: string; + user_verified: number; + verified_content: string; +} + +interface UserInfoCell49 { + info: { + avatar_uri: string; + avatar_url: string; + ban_status: boolean; + desc: string; + media_id: string; + name: string; + origin_profile_url: boolean; + origin_user_id: string; + schema: string; + user_auth_info: string; + user_id: string; + user_verified: number; + verified_content: string; + }; + relation: { + is_followed: number; + is_following: number; + is_friend: number; + }; + relation_count: { + followers_count: number; + following_count: number; + }; + user_id: string; +} + +interface UserInfoCell60 { + avatar_url: string; + description: string; + follow: boolean; + name: string; + user_auth_info: string; + user_id: string; + user_verified: boolean; + verified_content: string; +} + +interface ImageListItem { + height: number; + uri: string; + url: string; + url_list: UrlList[]; + width: number; +} + +interface UrlList { + url: string; +} + +interface LargeImageListItem { + height: number; + uri: string; + url: string; + url_list: UrlList[]; + width: number; +} + +interface MiddleImage { + height: number; + uri: string; + url: string; + url_list: UrlList[]; + width: number; +} + +interface VideoDetailInfo { + detail_video_large_image: DetailVideoLargeImage; + direct_play: number; + group_flags: number; + show_pgc_subscribe: number; + video_id: string; +} + +interface DetailVideoLargeImage { + height: number; + uri: string; + url: string; + url_list: UrlList[]; + width: number; +} diff --git a/lib/routes/toutiao/user.ts b/lib/routes/toutiao/user.ts new file mode 100644 index 00000000000000..21c66876eec124 --- /dev/null +++ b/lib/routes/toutiao/user.ts @@ -0,0 +1,127 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import randUserAgent from '@/utils/rand-user-agent'; +import { generate_a_bogus } from './a-bogus'; +import { Feed } from './types'; +import RejectError from '@/errors/types/reject'; +import { config } from '@/config'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +import { art } from '@/utils/render'; + +const __dirname = getCurrentPath(import.meta.url); + +export const route: Route = { + path: '/user/token/:token', + categories: ['new-media'], + example: '/toutiao/user/token/MS4wLjABAAAAEmbqJP2CmC8XXv1BpMvQ3sQHKAxFsq8wHxj8XVIQWja6tMcB-QEbFkzkRNgMl12M', + parameters: { token: '用户 token,可在用户主页 URL 找到' }, + features: { + antiCrawler: true, + }, + radar: [ + { + source: ['www.toutiao.com/c/user/token/:token'], + }, + ], + name: '头条主页', + maintainers: ['TonyRL'], + handler, +}; + +async function handler(ctx) { + const { token } = ctx.req.param(); + const ua = randUserAgent({ browser: 'chrome', os: 'windows', device: 'desktop' }); + + const feed = (await cache.tryGet( + `toutiao:user:${token}`, + async () => { + const query = `category=profile_all&token=${token}&max_behot_time=0&entrance_gid&aid=24&app_name=toutiao_web`; + + const data = await ofetch(`https://www.toutiao.com/api/pc/list/feed?${query}&a_bogus=${generate_a_bogus(query, ua)}`, { + headers: { + 'User-Agent': ua, + }, + }); + + return data.data; + }, + config.cache.routeExpire, + false + )) as Feed[]; + + if (!feed) { + throw new RejectError('无法获取用户信息'); + } + + const items = feed.map((item) => { + switch (item.cell_type) { + case 0: + case 49: { + const video = item.video.play_addr_list.sort((a, b) => b.bitrate - a.bitrate)[0]; + return { + title: item.title, + description: art(path.join(__dirname, 'templates', 'video.art'), { + poster: item.video.origin_cover.url_list[0], + url: item.video.play_addr_list.sort((a, b) => b.bitrate - a.bitrate)[0].play_url_list[0], + }), + link: `https://www.toutiao.com/video/${item.id}/`, + pubDate: parseDate(item.publish_time, 'X'), + author: item.user?.info.name ?? item.source, + enclosure_url: video?.play_url_list[0], + enclosure_type: video?.play_url_list[0] ? 'video/mp4' : undefined, + user: { + name: item.user?.info.name, + avatar: item.user?.info.avatar_url, + description: item.user?.info.desc, + }, + }; + } + + // text w/o title + case 32: { + const enclosure = item.large_image_list?.pop(); + return { + title: item.content?.split('\n')[0], + description: item.rich_content, + link: `https://www.toutiao.com/w/${item.id}/`, + pubDate: parseDate(item.publish_time, 'X'), + author: item.user?.name, + enclosure_url: enclosure?.url, + enclosure_type: enclosure?.url ? `image/${new URL(enclosure.url).pathname.split('.').pop()}` : undefined, + user: { + name: item.user?.name, + avatar: item.user?.avatar_url, + description: item.user?.desc, + }, + }; + } + + // text w/ title + case 60: + default: + return { + title: item.title, + description: item.abstract, + link: `https://www.toutiao.com/article/${item.id}/`, + pubDate: parseDate(item.publish_time, 'X'), + author: item.user_info?.name, + user: { + name: item.user_info?.name, + avatar: item.user_info?.avatar_url, + description: item.user_info?.description, + }, + }; + } + }); + + return { + title: `${items[0].user.name}的头条主页 - 今日头条(www.toutiao.com)`, + description: items[0].user.description, + link: `https://www.toutiao.com/c/user/token/${token}/`, + image: items[0].user.avatar, + item: items, + }; +} diff --git a/lib/routes/towardsdatascience/latest.ts b/lib/routes/towardsdatascience/latest.ts new file mode 100644 index 00000000000000..38ddf2f7901547 --- /dev/null +++ b/lib/routes/towardsdatascience/latest.ts @@ -0,0 +1,67 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/latest', + categories: ['blog'], + example: '/latest', + radar: [ + { + source: ['towardsdatascience.com/'], + }, + ], + name: 'Towards Data Science', + maintainers: ['mintyfrankie'], + url: 'towardsdatascience.com/latest', + handler, +}; + +async function handler() { + const baseUrl = 'https://towardsdatascience.com/latest'; + const feedLang = 'en'; + const feedDescription = 'Latest articles from Towards Data Science'; + + const response = await ofetch('https://medium.com/towards-data-science/latest?posts=true', { + headers: { + accept: 'application/json', + }, + }); + const data = JSON.parse(response.slice(16)); + + const list = data.payload.posts.map((item) => { + const title = item.title; + const link = `https://towardsdatascience.com/${item.uniqueSlug}`; + const freediumLink = `https://freedium.cfd/https://towardsdatascience.com/${item.uniqueSlug}`; + const author = data.payload.references.User[item.creatorId].name; + const pubDate = parseDate(item.createdAt); + return { + title, + link, + freediumLink, + author, + pubDate, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.freediumLink, async () => { + const response = await ofetch(item.freediumLink); + const $ = load(response); + item.description = $('div.main-content').first().html(); + return item; + }) + ) + ); + + return { + title: 'Towards Data Science - Latest', + language: feedLang, + description: feedDescription, + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/towardsdatascience/namespace.ts b/lib/routes/towardsdatascience/namespace.ts new file mode 100644 index 00000000000000..8707f62995e0ab --- /dev/null +++ b/lib/routes/towardsdatascience/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Towards Data Science', + url: 'towardsdatascience.com', + lang: 'en', +}; diff --git a/lib/routes/tradingview/namespace.ts b/lib/routes/tradingview/namespace.ts index 11db94dd47ba05..48718f1e96e2e5 100644 --- a/lib/routes/tradingview/namespace.ts +++ b/lib/routes/tradingview/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'TradingView', url: 'tradingview.com', + lang: 'en', }; diff --git a/lib/routes/transcriptforest/namespace.ts b/lib/routes/transcriptforest/namespace.ts index 9032fba5601ff6..b66736c7abc97b 100644 --- a/lib/routes/transcriptforest/namespace.ts +++ b/lib/routes/transcriptforest/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Transcript Forest', url: 'www.transcriptforest.com', + lang: 'en', }; diff --git a/lib/routes/transformer-circuits/index.ts b/lib/routes/transformer-circuits/index.ts new file mode 100644 index 00000000000000..ef5d2aa860e040 --- /dev/null +++ b/lib/routes/transformer-circuits/index.ts @@ -0,0 +1,124 @@ +import { Route, DataItem } from '@/types'; +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'; +import { fileURLToPath } from 'node:url'; +import logger from '@/utils/logger'; + +// 为ES模块创建__dirname等价物 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Define the main route path +export const route: Route = { + path: '/', + categories: ['programming'], + example: '/transformer-circuits', + parameters: {}, + radar: [ + { + source: ['transformer-circuits.pub/'], + target: '/', + }, + ], + name: 'Articles', + maintainers: ['shinmohuang'], + handler, +}; + +async function handler() { + const rootUrl = 'https://transformer-circuits.pub'; + + // Fetch the main page + const response = await ofetch(rootUrl); + const $ = load(response); + + // Get all article links and basic info + const articlePromises = $('.toc a') + .toArray() + .map((item) => { + const $item = $(item); + const currentElement = $item; + const dateElement = $item.prevAll('.date').first(); + const currentDate = dateElement.text().trim(); + + // Check if this is an article (either paper or note) + if (currentElement.hasClass('paper') || currentElement.hasClass('note')) { + const articleType = currentElement.hasClass('paper') ? 'Paper' : 'Note'; + + // Extract title and metadata from the list page + const title = currentElement.find('h3').text().trim(); + let author = ''; + const byline = currentElement.find('.byline'); + if (byline.length) { + author = byline.text().trim(); + } + const description = currentElement.find('.description').text().trim(); + const href = currentElement.attr('href'); + const articleUrl = href ? (href.startsWith('http') ? href : `${rootUrl}/${href}`) : rootUrl; + + // Cache the whole article object instead of just the content + return cache.tryGet(articleUrl, async () => { + const fullContent = await fetchArticleContent(articleUrl); + return { + title, + link: articleUrl, + pubDate: parseDate(currentDate, 'MMMM YYYY'), + author, + description: fullContent || `${articleType}: ${description}`, + category: ['AI', 'Machine Learning', 'Anthropic', 'Transformer Circuits'], + }; + }); + } + return null; + }); + + // Wait for all article fetches to complete + const articlesWithContent = (await Promise.all(articlePromises)).filter(Boolean) as DataItem[]; + + return { + title: 'Transformer Circuits Thread', + link: rootUrl, + item: articlesWithContent, + description: 'Research on reverse engineering transformer language models into human-understandable programs', + }; +} + +// Function to fetch and parse article content +async function fetchArticleContent(url) { + try { + const response = await ofetch(url); + const $ = load(response); + + // Remove navigation and other unnecessary elements + $('.article-header, .tooltip, modal, script, style, d-front-matter, .visual-toc').remove(); + + // Get the main content - d-article is the custom tag used by this site + let content = $('d-article').html(); + + // If d-article not found, try more specific fallback selectors + if (!content) { + // Try to find content in common article containers, but avoid selecting the entire body + content = $('main article, .article-content, .post-content, .content-area').html() || $('.content, .article, .post').html() || $('main').html(); + + // If still no content found, log a warning + if (!content) { + logger.warn(`No suitable content container found for ${url}`); + content = `<p>Could not extract content. Please visit <a href="${url}">the original page</a>.</p>`; + } + } + + // Create an HTML fragment (not a full document) for the RSS description + return art(path.join(__dirname, 'templates/article.art'), { + content, + link: url, + }); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Error fetching article content from ${url}: ${errorMessage}`); + return null; // Return null on error, we'll fall back to description + } +} diff --git a/lib/routes/transformer-circuits/namespace.ts b/lib/routes/transformer-circuits/namespace.ts new file mode 100644 index 00000000000000..df4d42ffc1098f --- /dev/null +++ b/lib/routes/transformer-circuits/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Transformer Circuits', + url: 'transformer-circuits.pub', + lang: 'en', +}; diff --git a/lib/routes/transformer-circuits/templates/article.art b/lib/routes/transformer-circuits/templates/article.art new file mode 100644 index 00000000000000..80de0e0b3480dc --- /dev/null +++ b/lib/routes/transformer-circuits/templates/article.art @@ -0,0 +1,62 @@ +<style> + .content-wrapper { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + line-height: 1.6; + color: #333; + } + img { + max-width: 100%; + height: auto; + } + pre, code { + background-color: #f5f5f5; + border-radius: 3px; + padding: 0.2em 0.4em; + overflow-x: auto; + } + a { + color: #0366d6; + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + h1, h2, h3, h4, h5, h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + } + p, ul, ol { + margin-bottom: 16px; + } + .read-original { + margin-top: 30px; + margin-bottom: 30px; + text-align: center; + padding: 10px; + background-color: #f7f7f7; + border-radius: 4px; + } + /* Support for custom elements used on transformer-circuits website */ + d-figure, figure { + margin: 20px 0; + text-align: center; + } + d-byline { + font-size: 0.9em; + color: #666; + margin: 15px 0; + } + .gdoc-image img { + max-width: 100%; + display: block; + margin: 0 auto; + } +</style> +<div class="content-wrapper"> + {{@ content }} +</div> +<div class="read-original"> + <a href="{{ link }}" target="_blank">Read Original</a> +</div> \ No newline at end of file diff --git a/lib/routes/trending/all-trending.ts b/lib/routes/trending/all-trending.ts index 1233cdeaf281df..a6cd7b99c01ddd 100644 --- a/lib/routes/trending/all-trending.ts +++ b/lib/routes/trending/all-trending.ts @@ -8,7 +8,7 @@ import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; dayjs.extend(utc); dayjs.extend(timezone); -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { art } from '@/utils/render'; import path from 'node:path'; import { config } from '@/config'; @@ -75,7 +75,7 @@ const filterKeyword = (keywordList) => (res) => res.filter(({ title }) => hasKey // Data Fetcher // TODO: support channel selection const fetchAllData = async (keywordList = [], dateList = [], cache) => { - const cachedGetData = (url) => cache.tryGet(url, () => got(url).json(), config.cache.contentExpire, false); + const cachedGetData = (url) => cache.tryGet(url, () => ofetch(url), config.cache.contentExpire, false); let data = await Promise.all( dateList.map(async (dateTime) => ({ diff --git a/lib/routes/trending/namespace.ts b/lib/routes/trending/namespace.ts index f2bd9447d26a42..937478e118e1da 100644 --- a/lib/routes/trending/namespace.ts +++ b/lib/routes/trending/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '热搜聚合', url: 'so.toutiao.com', + lang: 'zh-CN', }; diff --git a/lib/routes/trendingpapers/namespace.ts b/lib/routes/trendingpapers/namespace.ts index 4ad41558cf8162..067a8442c06d33 100644 --- a/lib/routes/trendingpapers/namespace.ts +++ b/lib/routes/trendingpapers/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Trending Papers', url: 'trendingpapers.com', + lang: 'en', }; diff --git a/lib/routes/trendingpapers/papers.ts b/lib/routes/trendingpapers/papers.ts index ba27dfa6ab2770..db5b76e20a64d6 100644 --- a/lib/routes/trendingpapers/papers.ts +++ b/lib/routes/trendingpapers/papers.ts @@ -1,5 +1,5 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { @@ -30,14 +30,9 @@ async function handler(ctx) { const rootUrl = 'https://trendingpapers.com'; const currentUrl = `${rootUrl}/api/papers?p=1&o=pagerank_growth&pd=${time}&cc=${cited}&c=${category}`; - const response = await got({ - method: 'get', - url: currentUrl, - }); - - const $ = response.data; + const response = await ofetch(currentUrl); - const papers = $.data.map((_) => { + const papers = response.data.map((_) => { const title = _.title; const abstract = _.abstract; const url = _.url; @@ -60,6 +55,5 @@ async function handler(ctx) { title: `Trending Papers on arXiv.org | ${category} | ${time} | ${cited} | `, link: currentUrl, item: papers, - language: $('html').attr('lang'), }; } diff --git a/lib/routes/tribalfootball/latest.ts b/lib/routes/tribalfootball/latest.ts index a87986a637b44b..96c022e2b4b414 100644 --- a/lib/routes/tribalfootball/latest.ts +++ b/lib/routes/tribalfootball/latest.ts @@ -41,7 +41,7 @@ async function handler() { link, guid: $item.find('guid').text(), pubDate: parseDate($item.find('pubDate').text()), - author: $item.find('dc\\:creator').text(), + author: $item.find(String.raw`dc\:creator`).text(), _header_image: $item.find('enclosure').attr('url'), }; }) diff --git a/lib/routes/tribalfootball/namespace.ts b/lib/routes/tribalfootball/namespace.ts index 286245e7d25c6a..586c6b26e0c6e4 100644 --- a/lib/routes/tribalfootball/namespace.ts +++ b/lib/routes/tribalfootball/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Tribal Football', url: 'tribalfootball.com', + lang: 'en', }; diff --git a/lib/routes/trow/namespace.ts b/lib/routes/trow/namespace.ts index 6440b68917ba0a..bd71984531efd1 100644 --- a/lib/routes/trow/namespace.ts +++ b/lib/routes/trow/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The Ring of Wonder', url: 'trow.cc', + lang: 'en', }; diff --git a/lib/routes/tsdm39/bd.ts b/lib/routes/tsdm39/bd.ts new file mode 100644 index 00000000000000..2f65bda68826ec --- /dev/null +++ b/lib/routes/tsdm39/bd.ts @@ -0,0 +1,101 @@ +import type { DataItem, Route } from '@/types'; +import { config } from '@/config'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; + +// type id => display name +type Mapping = Record<string, string>; + +const TYPE: Mapping = { + '403': '720P', + '404': '1080P', + '405': 'BDMV', + '4130': '4K', + '5815': 'AV1', +}; + +// render into MD table +const mkTable = (mapping: Mapping): string => { + const heading: string[] = [], + separator: string[] = [], + body: string[] = []; + + for (const key in mapping) { + heading.push(mapping[key]); + separator.push(':--:'); + body.push(key); + } + + return [heading.join(' | '), separator.join(' | '), body.join(' | ')].map((s) => `| ${s} |`).join('\n'); +}; + +const handler: Route['handler'] = async (ctx) => { + const { type } = ctx.req.param(); + + const cookie = config.tsdm39.cookie; + if (!cookie) { + throw new ConfigNotFoundError('缺少 TSDM39 用户登录后的 Cookie 值 <a href="https://docs.rsshub.app/zh/deploy/config#route-specific-configurations">TSDM 相关路由</a>'); + } + + const html = await ofetch(`https://www.tsdm39.com/forum.php?mod=forumdisplay&fid=85${type ? `&filter=typeid&typeid=${type}` : ''}`, { + headers: { + Cookie: cookie, + }, + }); + + const $ = load(html); + + const item = $('tbody.tsdm_normalthread') + .toArray() + .map<DataItem>((item) => { + const $ = load(item); + + const title = $('a.xst').text(); + const price = $('span.xw1').last().text(); + const link = $('a.xst').attr('href'); + const date = $('td.by em').first().text().trim(); + + return { + title, + description: `价格:${price}`, + link, + pubDate: parseDate(date), + }; + }); + + return { + title: '天使动漫论坛 - BD', + link: 'https://www.tsdm39.com/forum.php?mod=forumdisplay&fid=85', + language: 'zh-Hans', + item, + }; +}; + +export const route: Route = { + path: '/bd/:type?', + name: 'BD', + categories: ['anime'], + maintainers: ['equt'], + example: '/tsdm39/bd', + parameters: { + type: 'BD type, checkout the table below for details', + }, + features: { + requireConfig: [ + { + name: 'TSDM39_COOKIES', + optional: false, + description: '天使动漫论坛登陆后的 cookie 值,可在浏览器控制台通过 `document.cookie` 获取。', + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + handler, + description: [TYPE].map((el) => mkTable(el)).join('\n\n'), +}; diff --git a/lib/routes/tsdm39/namespace.ts b/lib/routes/tsdm39/namespace.ts new file mode 100644 index 00000000000000..f6cad579744093 --- /dev/null +++ b/lib/routes/tsdm39/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '天使动漫论坛', + url: 'www.tsdm39.com', + categories: ['anime'], + lang: 'zh-CN', +}; diff --git a/lib/routes/tsinghua/lib/tzgg.ts b/lib/routes/tsinghua/lib/tzgg.ts new file mode 100644 index 00000000000000..73ff5975fa106e --- /dev/null +++ b/lib/routes/tsinghua/lib/tzgg.ts @@ -0,0 +1,73 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/lib/tzgg/:category', + categories: ['university'], + example: '/tsinghua/lib/tzgg/qtkx', + parameters: { category: '分类,可在对应分类页 URL 中找到' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['lib.tsinghua.edu.cn/tzgg/:category'], + }, + ], + name: '图书馆通知公告', + maintainers: ['linsenwang'], + handler, +}; + +async function handler(ctx) { + const { category } = ctx.req.param(); + const host = `https://lib.tsinghua.edu.cn/tzgg/${category}.htm`; + const response = await ofetch(host); + const $ = load(response); + + const feedTitle = $('.tags .on').text(); + + const list = $('ul.notice-list li') + .toArray() + .map((item) => { + item = $(item); + const title = item.find('a').first().text(); + const time = item.find('.notice-date').first().text(); + const a = item.find('a').first().attr('href'); + + const fullUrl = new URL(a, host).href; + + return { + title, + link: fullUrl, + pubDate: time, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + + item.description = $('.v_news_content').first().html(); + + return item; + }) + ) + ); + + return { + allowEmpty: true, + title: '图书馆通知公告 - ' + feedTitle, + link: host, + item: items, + }; +} diff --git a/lib/routes/tsinghua/lib/zydt.ts b/lib/routes/tsinghua/lib/zydt.ts new file mode 100644 index 00000000000000..6083c27f2afbd8 --- /dev/null +++ b/lib/routes/tsinghua/lib/zydt.ts @@ -0,0 +1,129 @@ +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 } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + + const rootUrl = 'https://lib.tsinghua.edu.cn'; + const currentUrl = new URL(`zydt${category ? `/${category}` : ''}.htm`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('ul.notice-list li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + return { + title: item.find('div.notice-list-tt a').text(), + pubDate: parseDate(item.find('div.notice-date').text(), 'YYYY/MM/DD'), + link: new URL(item.find('div.notice-list-tt a').prop('href'), rootUrl).href, + category: item + .find('div.notice-label') + .toArray() + .map((c) => $(c).text()), + 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 = $$('h2').text(); + const description = $$('div.v_news_content').html(); + + item.title = title; + item.description = description; + item.content = { + html: description, + text: $$('div.v_news_content').text(), + }; + item.language = language; + + return item; + }) + ) + ); + + const title = $('title').text(); + const image = new URL($('div.logo a img').prop('href'), rootUrl).href; + + return { + title, + description: $('META[Name="keywords"]').prop('Content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: title.split(/-/).pop(), + language, + }; +}; + +export const route: Route = { + path: '/lib/zydt/:category?', + name: '图书馆资源动态', + url: 'lib.tsinghua.edu.cn', + maintainers: ['nczitzk'], + handler, + example: '/tsinghua/lib/zydt', + parameters: { category: '分类,默认为空,即全部,可在对应分类页 URL 中找到' }, + description: `::: tip + 若订阅 [清华大学图书馆已购资源动态](https://lib.tsinghua.edu.cn/zydt/yg.htm),网址为 \`https://lib.tsinghua.edu.cn/zydt/yg.htm\`。截取 \`https://lib.tsinghua.edu.cn/zydt\` 到末尾 \`.htm\` 的部分 \`yg\` 作为参数填入,此时路由为 [\`/tsinghua/lib/zydt/yg\`](https://rsshub.app/tsinghua/lib/zydt/yg)。 +::: + +| 已购 | 试用 | +| ---- | ---- | +| yg | sy | + `, + categories: ['university'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['lib.tsinghua.edu.cn/zydt/:category?'], + target: (params) => { + const category = params.category?.replace(/\.htm$/, ''); + + return `/tsinghua/lib/zydt${category ? `/${category}` : ''}`; + }, + }, + { + title: '图书馆资源动态', + source: ['lib.tsinghua.edu.cn/zydt'], + target: '/lib/zydt', + }, + { + title: '图书馆已购资源动态', + source: ['lib.tsinghua.edu.cn/zydt/yg'], + target: '/lib/zydt/yg', + }, + { + title: '图书馆试用资源动态', + source: ['lib.tsinghua.edu.cn/zydt/sy'], + target: '/lib/zydt/sy', + }, + ], +}; diff --git a/lib/routes/tsinghua/namespace.ts b/lib/routes/tsinghua/namespace.ts new file mode 100644 index 00000000000000..89db20e31b0618 --- /dev/null +++ b/lib/routes/tsinghua/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '清华大学', + url: 'tsinghua.edu.cn', + categories: ['university'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/ttv/index.ts b/lib/routes/ttv/index.ts index 042c693a9363e6..b670f0104ac556 100644 --- a/lib/routes/ttv/index.ts +++ b/lib/routes/ttv/index.ts @@ -34,7 +34,7 @@ async function handler(ctx) { const $ = load(response.data); let items = $('div.news-list li') - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.query.limit) : 30) + .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 30) .toArray() .map((item) => { item = $(item); diff --git a/lib/routes/ttv/namespace.ts b/lib/routes/ttv/namespace.ts index 95ba16c62df026..6b547ee1b772f9 100644 --- a/lib/routes/ttv/namespace.ts +++ b/lib/routes/ttv/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '台視新聞網', url: 'news.ttv.com.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/tvb/namespace.ts b/lib/routes/tvb/namespace.ts index f7cbc339f42900..60a3623ca9ca06 100644 --- a/lib/routes/tvb/namespace.ts +++ b/lib/routes/tvb/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '无线新闻', url: 'tvb.com', + lang: 'zh-HK', }; diff --git a/lib/routes/tvb/news.ts b/lib/routes/tvb/news.ts index 07195ba0c52722..6222fd42a79e1f 100644 --- a/lib/routes/tvb/news.ts +++ b/lib/routes/tvb/news.ts @@ -69,15 +69,15 @@ export const route: Route = { handler, description: `分类 - | 要聞 | 快訊 | 港澳 | 兩岸 | 國際 | 財經 | 體育 | 法庭 | 天氣 | - | ----- | ------- | ----- | ------------ | ----- | ------- | ------ | ---------- | ------- | - | focus | instant | local | greaterchina | world | finance | sports | parliament | weather | +| 要聞 | 快訊 | 港澳 | 兩岸 | 國際 | 財經 | 體育 | 法庭 | 天氣 | +| ----- | ------- | ----- | ------------ | ----- | ------- | ------ | ---------- | ------- | +| focus | instant | local | greaterchina | world | finance | sports | parliament | weather | 语言 - | 繁 | 简 | - | -- | -- | - | tc | sc |`, +| 繁 | 简 | +| -- | -- | +| tc | sc |`, }; async function handler(ctx) { @@ -85,6 +85,7 @@ async function handler(ctx) { const language = ctx.req.param('language') ?? 'tc'; const rootUrl = 'https://inews-api.tvb.com'; + const linkRootUrl = 'https://news.tvb.com'; const apiUrl = `${rootUrl}/news/entry/category`; const currentUrl = `${rootUrl}/${language}/${category}`; @@ -96,12 +97,13 @@ async function handler(ctx) { lang: language, page: 1, limit: ctx.req.query('limit') ?? 50, + country: 'HK', }, }); const items = response.data.content.map((item) => ({ title: item.title, - link: `${rootUrl}/${language}/${category}/${item.id}`, + link: `${linkRootUrl}/${language}/${category}/${item.id}`, pubDate: parseDate(item.publish_datetime), category: [...item.category.map((c) => c.title), ...item.tags], description: art(path.join(__dirname, 'templates/description.art'), { diff --git a/lib/routes/tvtropes/featured.ts b/lib/routes/tvtropes/featured.ts index 69586bb9abf393..2b30e1343a0027 100644 --- a/lib/routes/tvtropes/featured.ts +++ b/lib/routes/tvtropes/featured.ts @@ -29,8 +29,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| Today's Featured Trope | Newest Trope | - | ---------------------- | ------------ | - | today | newest |`, +| ---------------------- | ------------ | +| today | newest |`, }; async function handler(ctx) { diff --git a/lib/routes/tvtropes/namespace.ts b/lib/routes/tvtropes/namespace.ts index a10ee1e5ed22b1..9aba076818b8fb 100644 --- a/lib/routes/tvtropes/namespace.ts +++ b/lib/routes/tvtropes/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'TV Tropes', url: 'tvtropes.org', + lang: 'en', }; diff --git a/lib/routes/twitch/live.ts b/lib/routes/twitch/live.ts index 53edae7092fc89..ab26fa823bf8e3 100644 --- a/lib/routes/twitch/live.ts +++ b/lib/routes/twitch/live.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; @@ -7,7 +7,8 @@ const TWITCH_CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; export const route: Route = { path: '/live/:login', - categories: ['live'], + categories: ['live', 'popular'], + view: ViewType.Notifications, example: '/twitch/live/riotgames', parameters: { login: 'Twitch username' }, features: { diff --git a/lib/routes/twitch/namespace.ts b/lib/routes/twitch/namespace.ts index 06d66837e2fd74..c689b6ae6ee3b3 100644 --- a/lib/routes/twitch/namespace.ts +++ b/lib/routes/twitch/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Twitch', url: 'www.twitch.tv', + lang: 'en', }; diff --git a/lib/routes/twitch/video.ts b/lib/routes/twitch/video.ts index 3e3ed59a8493f3..38ecdc92852b2d 100644 --- a/lib/routes/twitch/video.ts +++ b/lib/routes/twitch/video.ts @@ -1,5 +1,5 @@ import InvalidParameterError from '@/errors/types/invalid-parameter'; -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; @@ -14,9 +14,21 @@ const FILTER_NODE_TYPE_MAP = { export const route: Route = { path: '/video/:login/:filter?', - categories: ['live'], + categories: ['live', 'popular'], + view: ViewType.Videos, example: '/twitch/video/riotgames/highlights', - parameters: { login: 'Twitch username', filter: 'Video type, Default to all' }, + parameters: { + login: 'Twitch username', + filter: { + description: 'Video type, Default to all', + options: [ + { value: 'archive', label: 'Archive' }, + { value: 'highlights', label: 'Highlights' }, + { value: 'all', label: 'All' }, + ], + default: 'all', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -34,9 +46,6 @@ export const route: Route = { name: 'Channel Video', maintainers: ['hoilc'], handler, - description: `| archive | highlights | all | -| ----------------- | ----------------------------- | ---------- | -| Recent broadcasts | Recent highlights and uploads | All videos |`, }; async function handler(ctx) { diff --git a/lib/routes/twitter/api/developer-api/search.ts b/lib/routes/twitter/api/developer-api/search.ts index 5a9fc81b47ee1d..484b93cd115b41 100644 --- a/lib/routes/twitter/api/developer-api/search.ts +++ b/lib/routes/twitter/api/developer-api/search.ts @@ -13,7 +13,7 @@ const handler = async (ctx) => { return { title: `Twitter Keyword - ${keyword}`, - link: `https://twitter.com/search?q=${encodeURIComponent(keyword)}`, + link: `https://x.com/search?q=${encodeURIComponent(keyword)}`, item: utils.ProcessFeed(ctx, { data: data.statuses, }), diff --git a/lib/routes/twitter/api/developer-api/user.ts b/lib/routes/twitter/api/developer-api/user.ts index 2d94a7165533f6..29ba86c18b78f2 100644 --- a/lib/routes/twitter/api/developer-api/user.ts +++ b/lib/routes/twitter/api/developer-api/user.ts @@ -3,11 +3,11 @@ import utils from '../../utils'; const handler = async (ctx) => { const id = ctx.req.param('id'); // For compatibility - const { exclude_replies, include_rts, count } = utils.parseRouteParams(ctx.req.param('routeParams')); + const { include_replies, include_rts, count } = utils.parseRouteParams(ctx.req.param('routeParams')); const client = await utils.getAppClient(); const user_timeline_query = { tweet_mode: 'extended', - exclude_replies, + exclude_replies: !include_replies, include_rts, count, }; @@ -27,7 +27,7 @@ const handler = async (ctx) => { return { title: `Twitter @${userInfo.name}`, - link: `https://twitter.com/${screen_name}`, + link: `https://x.com/${screen_name}`, image: profileImageUrl, description: userInfo.description, item: utils.ProcessFeed(ctx, { diff --git a/lib/routes/twitter/api/index.ts b/lib/routes/twitter/api/index.ts index 64e861cae56987..9f17ad75173050 100644 --- a/lib/routes/twitter/api/index.ts +++ b/lib/routes/twitter/api/index.ts @@ -3,8 +3,9 @@ import mobileApi from './mobile-api/api'; import webApi from './web-api/api'; import { config } from '@/config'; +const enableThirdPartyApi = config.twitter.thirdPartyApi; const enableMobileApi = config.twitter.username && config.twitter.password; -const enableWebApi = config.twitter.cookie; +const enableWebApi = config.twitter.authToken; type ApiItem = (id: string, params?: Record<string, any>) => Promise<Record<string, any>> | Record<string, any> | null; let api: { @@ -18,6 +19,7 @@ let api: { getSearch: ApiItem; getList: ApiItem; getHomeTimeline: ApiItem; + getHomeLatestTimeline: ApiItem; } = { init: () => { throw new ConfigNotFoundError('Twitter API is not configured'); @@ -31,9 +33,12 @@ let api: { getSearch: () => null, getList: () => null, getHomeTimeline: () => null, + getHomeLatestTimeline: () => null, }; -if (enableWebApi) { +if (enableThirdPartyApi) { + api = webApi; +} else if (enableWebApi) { api = webApi; } else if (enableMobileApi) { api = mobileApi; diff --git a/lib/routes/twitter/api/mobile-api/api.ts b/lib/routes/twitter/api/mobile-api/api.ts index 199f21c6e217a8..d4b79e511612a4 100644 --- a/lib/routes/twitter/api/mobile-api/api.ts +++ b/lib/routes/twitter/api/mobile-api/api.ts @@ -1,18 +1,18 @@ import { baseUrl, gqlMap, gqlFeatures, consumerKey, consumerSecret } from './constants'; import { config } from '@/config'; import logger from '@/utils/logger'; -import got from '@/utils/got'; import OAuth from 'oauth-1.0a'; import CryptoJS from 'crypto-js'; import queryString from 'query-string'; -import { initToken, getToken } from './token'; +import { getToken } from './token'; import cache from '@/utils/cache'; import InvalidParameterError from '@/errors/types/invalid-parameter'; +import ofetch from '@/utils/ofetch'; const twitterGot = async (url, params) => { const token = await getToken(); - const oauth = OAuth({ + const oauth = new OAuth({ consumer: { key: consumerKey, secret: consumerSecret, @@ -28,7 +28,7 @@ const twitterGot = async (url, params) => { connection: 'keep-alive', 'content-type': 'application/json', 'x-twitter-active-user': 'yes', - authority: 'api.twitter.com', + authority: 'api.x.com', 'accept-encoding': 'gzip', 'accept-language': 'en-US,en;q=0.9', accept: '*/*', @@ -36,11 +36,14 @@ const twitterGot = async (url, params) => { }, }; - const response = await got(requestData.url, { + const response = await ofetch.raw(requestData.url, { headers: oauth.toHeader(oauth.authorize(requestData, token)), }); + if (response.status === 401) { + cache.globalCache.set(token.cacheKey, ''); + } - return response.data; + return response._data; }; const paginationTweets = async (endpoint, userId, variables, path) => { @@ -113,6 +116,16 @@ const tweetDetail = (userId, params) => ['threaded_conversation_with_injections_v2'] ); +const listTweets = (listId, params = {}) => + paginationTweets( + gqlMap.ListTimeline, + listId, + { + ...params, + }, + ['list', 'timeline_response', 'timeline'] + ); + function gatherLegacyFromData(entries, filterNested, userId) { const tweets = []; const filteredEntries = []; @@ -147,6 +160,14 @@ function gatherLegacyFromData(entries, filterNested, userId) { t.legacy.quoted_status = quote.legacy; t.legacy.quoted_status.user = quote.core.user_result?.result?.legacy || quote.core.user_results?.result?.legacy; } + if (t.note_tweet) { + const tmp = t.note_tweet.note_tweet_results.result; + t.legacy.entities.hashtags = tmp.entity_set.hashtags; + t.legacy.entities.symbols = tmp.entity_set.symbols; + t.legacy.entities.urls = tmp.entity_set.urls; + t.legacy.entities.user_mentions = tmp.entity_set.user_mentions; + t.legacy.full_text = tmp.text; + } } const legacy = tweet.legacy; if (legacy) { @@ -169,6 +190,7 @@ const getUserTweetsAndRepliesByID = async (id, params = {}) => gatherLegacyFromD const getUserMediaByID = async (id, params = {}) => gatherLegacyFromData(await timelineMedia(id, params)); // const getUserLikesByID = async (id, params = {}) => gatherLegacyFromData(await timelineLikes(id, params)); const getUserTweetByStatus = async (id, params = {}) => gatherLegacyFromData(await tweetDetail(id, params), ['homeConversation-', 'conversationthread-']); +const getListById = async (id, params = {}) => gatherLegacyFromData(await listTweets(id, params)); const excludeRetweet = function (tweets) { const excluded = []; @@ -270,6 +292,8 @@ const getUserTweet = (id, params) => cacheTryGet(id, params, getUserTweetByStatu const getSearch = async (keywords, params = {}) => gatherLegacyFromData(await timelineKeywords(keywords, params)); +const getList = (id, params = {}) => cache.tryGet(`twitter:${id}:getListById:${JSON.stringify(params)}`, () => getListById(id, params), config.cache.routeExpire, false); + export default { getUser, getUserTweets, @@ -278,6 +302,7 @@ export default { // getUserLikes, excludeRetweet, getSearch, + getList, getUserTweet, - init: initToken, + init: () => void 0, }; diff --git a/lib/routes/twitter/api/mobile-api/constants.ts b/lib/routes/twitter/api/mobile-api/constants.ts index c4bedb05fb169c..cd9b92bbbf36a4 100644 --- a/lib/routes/twitter/api/mobile-api/constants.ts +++ b/lib/routes/twitter/api/mobile-api/constants.ts @@ -1,4 +1,4 @@ -const baseUrl = 'https://api.twitter.com'; +const baseUrl = 'https://api.x.com'; const consumerKey = '3nVuSoBZnx6U4vzUxf5w'; const consumerSecret = 'Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys'; diff --git a/lib/routes/twitter/api/mobile-api/login.ts b/lib/routes/twitter/api/mobile-api/login.ts index fe7feb9a3cc2f0..880ae0b7910a93 100644 --- a/lib/routes/twitter/api/mobile-api/login.ts +++ b/lib/routes/twitter/api/mobile-api/login.ts @@ -4,18 +4,15 @@ import { bearerToken, guestActivateUrl } from './constants'; import got from '@/utils/got'; import ofetch from '@/utils/ofetch'; import crypto from 'crypto'; -import { config } from '@/config'; import { v5 as uuidv5 } from 'uuid'; import { authenticator } from 'otplib'; import logger from '@/utils/logger'; import cache from '@/utils/cache'; +import { RateLimiterMemory, RateLimiterQueue, RateLimiterRedis } from 'rate-limiter-flexible'; -const NAMESPACE = 'd41d092b-b007-48f7-9129-e9538d2d8fe9'; -const username = config.twitter.username; -const password = config.twitter.password; -const authenticationSecret = config.twitter.authenticationSecret; +const ENDPOINT = 'https://api.x.com/1.1/onboarding/task.json'; -let authentication = null; +const NAMESPACE = 'd41d092b-b007-48f7-9129-e9538d2d8fe9'; const headers = { 'User-Agent': 'TwitterAndroid/10.21.0-release.0 (310210000-r-0) ONEPLUS+A3010/9 (OnePlus;ONEPLUS+A3010;OnePlus;OnePlus3;0;;1;2016)', @@ -29,157 +26,186 @@ const headers = { Authorization: bearerToken, }; -async function login() { - return await cache.tryGet( +const loginLimiter = cache.clients.redisClient + ? new RateLimiterRedis({ + points: 1, + duration: 20, + execEvenly: true, + storeClient: cache.clients.redisClient, + }) + : new RateLimiterMemory({ + points: 1, + duration: 20, + execEvenly: true, + }); + +const loginLimiterQueue = new RateLimiterQueue(loginLimiter); + +const postTask = async (flowToken: string, subtaskId: string, subtaskInput: Record<string, unknown>) => + await got.post(ENDPOINT, { + headers, + json: { + flow_token: flowToken, + subtask_inputs: [Object.assign({ subtask_id: subtaskId }, subtaskInput)], + }, + }); + +// In the Twitter login flow, each task successfully requested will respond with a 'subtask_id' to determine what the next task is, and the execution sequence of the tasks is non-fixed. +// So abstract these tasks out into a map so that they can be dynamically executed during the login flow. +// If there are missing tasks in the future, simply add the implementation of that task to it. +const flowTasks = { + async LoginEnterUserIdentifier({ flowToken, username }) { + return await postTask(flowToken, 'LoginEnterUserIdentifier', { + enter_text: { + suggestion_id: null, + text: username, + link: 'next_link', + }, + }); + }, + async LoginEnterPassword({ flowToken, password }) { + return await postTask(flowToken, 'LoginEnterPassword', { + enter_password: { + password, + link: 'next_link', + }, + }); + }, + async LoginEnterAlternateIdentifierSubtask({ flowToken, phoneOrEmail }) { + return await postTask(flowToken, 'LoginEnterAlternateIdentifierSubtask', { + enter_text: { + suggestion_id: null, + text: phoneOrEmail, + link: 'next_link', + }, + }); + }, + async AccountDuplicationCheck({ flowToken }) { + return await postTask(flowToken, 'AccountDuplicationCheck', { + check_logged_in_account: { + link: 'AccountDuplicationCheck_false', + }, + }); + }, + async LoginTwoFactorAuthChallenge({ flowToken, authenticationSecret }) { + const token = authenticator.generate(authenticationSecret); + return await postTask(flowToken, 'LoginTwoFactorAuthChallenge', { + enter_text: { + suggestion_id: null, + text: token, + link: 'next_link', + }, + }); + }, +}; + +async function login({ username, password, authenticationSecret, phoneOrEmail }) { + return (await cache.tryGet( `twitter:authentication:${username}`, async () => { - logger.debug('Twitter login start.'); - const android_id = uuidv5(username, NAMESPACE); - headers['X-Twitter-Client-DeviceID'] = android_id; - - const ct0 = crypto.randomUUID().replaceAll('-', ''); - const guestToken = await got(guestActivateUrl, { - headers: { - authorization: bearerToken, - 'x-csrf-token': ct0, - cookie: 'ct0=' + ct0, - }, - method: 'POST', - }); - logger.debug('Twitter login 1 finished: guest token.'); - - headers['x-guest-token'] = guestToken.data.guest_token; - - const task1 = await ofetch.raw( - 'https://api.twitter.com/1.1/onboarding/task.json?' + - new URLSearchParams({ - flow_name: 'login', - api_version: '1', - known_device_token: '', - sim_country_code: 'us', - }).toString(), - { - method: 'POST', - headers, - body: { - flow_token: null, - input_flow_data: { - country_code: null, - flow_context: { - referrer_context: { - referral_details: 'utm_source=google-play&utm_medium=organic', - referrer_url: '', - }, - start_location: { - location: 'deeplink', - }, - }, - requested_variant: null, - target_user_id: 0, - }, - }, - } - ); - logger.debug('Twitter login 2 finished: login flow.'); + try { + await loginLimiterQueue.removeTokens(1); - headers.att = task1.headers.get('att'); + logger.debug('Twitter login start.'); - const task2 = await got.post('https://api.twitter.com/1.1/onboarding/task.json', { - headers, - json: { - flow_token: task1._data.flow_token, - subtask_inputs: [ - { - enter_text: { - suggestion_id: null, - text: username, - link: 'next_link', - }, - subtask_id: 'LoginEnterUserIdentifier', - }, - ], - }, - }); - logger.debug('Twitter login 3 finished: LoginEnterUserIdentifier.'); - - const task3 = await got.post('https://api.twitter.com/1.1/onboarding/task.json', { - headers, - json: { - flow_token: task2.data.flow_token, - subtask_inputs: [ - { - enter_password: { - password, - link: 'next_link', - }, - subtask_id: 'LoginEnterPassword', - }, - ], - }, - }); - logger.debug('Twitter login 4 finished: LoginEnterPassword.'); - - const task4 = await got.post('https://api.twitter.com/1.1/onboarding/task.json', { - headers, - json: { - flow_token: task3.data.flow_token, - subtask_inputs: [ + headers['X-Twitter-Client-DeviceID'] = uuidv5(username, NAMESPACE); + + const ct0 = crypto.randomUUID().replaceAll('-', ''); + const guestToken = await got(guestActivateUrl, { + headers: { + authorization: bearerToken, + 'x-csrf-token': ct0, + cookie: 'ct0=' + ct0, + }, + method: 'POST', + }); + logger.debug('Twitter login: guest token'); + + headers['x-guest-token'] = guestToken.data.guest_token; + + let task = await ofetch + .raw( + ENDPOINT + + '?' + + new URLSearchParams({ + flow_name: 'login', + api_version: '1', + known_device_token: '', + sim_country_code: 'us', + }).toString(), { - check_logged_in_account: { - link: 'AccountDuplicationCheck_false', - }, - subtask_id: 'AccountDuplicationCheck', - }, - ], - }, - }); - logger.debug('Twitter login 5 finished: AccountDuplicationCheck.'); - - for await (const subtask of task4.data?.subtasks || []) { - if (subtask.open_account) { - authentication = subtask.open_account; - break; - } else if (subtask.subtask_id === 'LoginTwoFactorAuthChallenge') { - const token = authenticator.generate(authenticationSecret); - - const task5 = await got.post('https://api.twitter.com/1.1/onboarding/task.json', { - headers, - json: { - flow_token: task4.data.flow_token, - subtask_inputs: [ - { - enter_text: { - suggestion_id: null, - text: token, - link: 'next_link', + method: 'POST', + headers, + body: { + flow_token: null, + input_flow_data: { + country_code: null, + flow_context: { + referrer_context: { + referral_details: 'utm_source=google-play&utm_medium=organic', + referrer_url: '', + }, + start_location: { + location: 'deeplink', + }, }, - subtask_id: 'LoginTwoFactorAuthChallenge', + requested_variant: null, + target_user_id: 0, }, - ], - }, + }, + } + ) + .then(({ headers: _headers, _data }) => { + headers.att = _headers.get('att'); + return { data: _data }; }); - logger.debug('Twitter login 6 finished: LoginTwoFactorAuthChallenge.'); - for (const subtask of task5.data?.subtasks || []) { - if (subtask.open_account) { - authentication = subtask.open_account; - break; - } + logger.debug('Twitter login flow start.'); + const runTask = async ({ data }) => { + const { subtask_id, open_account } = data.subtasks.shift(); + + // If `open_account` exists (and 'subtask_id' is `LoginSuccessSubtask`), it means the login was successful. + if (open_account) { + return open_account; + } + + // If task does not exist in `flowTasks`, we need to implement it. + if (!(subtask_id in flowTasks)) { + logger.error(`Twitter login flow task failed: unknown subtask: ${subtask_id}`); + return; } - break; + + task = await flowTasks[subtask_id]({ + flowToken: data.flow_token, + username, + password, + authenticationSecret, + phoneOrEmail, + }); + logger.debug(`Twitter login flow task finished: subtask: ${subtask_id}.`); + + return await runTask(task); + }; + const authentication = await runTask(task); + logger.debug('Twitter login flow finished.'); + + if (authentication) { + logger.debug('Twitter login success.', authentication); + } else { + logger.error(`Twitter login failed. ${JSON.stringify(task.data?.subtasks, null, 2)}`); } - } - if (authentication) { - logger.debug('Twitter login success.'); - } else { - logger.error(`Twitter login failed. ${JSON.stringify(task4.data?.subtasks, null, 2)}`); - } - return authentication; + return authentication; + } catch (error) { + logger.error(`Twitter username ${username} login failed:`, error); + } }, 60 * 60 * 24 * 30, // 30 days false - ); + )) as { + oauth_token: string; + oauth_token_secret: string; + } | null; } export default login; diff --git a/lib/routes/twitter/api/mobile-api/token.ts b/lib/routes/twitter/api/mobile-api/token.ts index cba2174cfe7f5a..51d6eba44fc644 100644 --- a/lib/routes/twitter/api/mobile-api/token.ts +++ b/lib/routes/twitter/api/mobile-api/token.ts @@ -3,35 +3,31 @@ import login from './login'; import ConfigNotFoundError from '@/errors/types/config-not-found'; let tokenIndex = 0; -let authentication = null; -let first = true; -async function initToken() { - if (config.twitter.username && config.twitter.password && !authentication && first) { - authentication = await login(); - first = false; - } -} - -function getToken() { +async function getToken() { let token; if (config.twitter.username && config.twitter.password) { - if (authentication) { + const index = tokenIndex++ % config.twitter.username.length; + const username = config.twitter.username[index]; + const password = config.twitter.password[index]; + const authenticationSecret = config.twitter.authenticationSecret?.[index]; + const phoneOrEmail = config.twitter.phoneOrEmail?.[index]; + if (username && password) { + const authentication = await login({ + username, + password, + authenticationSecret, + phoneOrEmail, + }); + if (!authentication) { + throw new ConfigNotFoundError(`Invalid twitter configs: ${username}`); + } token = { key: authentication.oauth_token, secret: authentication.oauth_token_secret, + cacheKey: `twitter:authentication:${username}`, }; } - } else if (config.twitter.oauthTokens?.length && config.twitter.oauthTokenSecrets.length && config.twitter.oauthTokens.length === config.twitter.oauthTokenSecrets.length) { - token = { - key: config.twitter.oauthTokens[tokenIndex], - secret: config.twitter.oauthTokenSecrets[tokenIndex], - }; - - tokenIndex++; - if (tokenIndex >= config.twitter.oauthTokens.length) { - tokenIndex = 0; - } } else { throw new ConfigNotFoundError('Invalid twitter configs'); } @@ -39,4 +35,4 @@ function getToken() { return token; } -export { initToken, getToken }; +export { getToken }; diff --git a/lib/routes/twitter/api/web-api/api.ts b/lib/routes/twitter/api/web-api/api.ts index 0431b632a8b46b..6d331a2049b48b 100644 --- a/lib/routes/twitter/api/web-api/api.ts +++ b/lib/routes/twitter/api/web-api/api.ts @@ -3,30 +3,37 @@ import { config } from '@/config'; import cache from '@/utils/cache'; import { twitterGot, paginationTweets, gatherLegacyFromData } from './utils'; import InvalidParameterError from '@/errors/types/invalid-parameter'; +import ofetch from '@/utils/ofetch'; const getUserData = (id) => cache.tryGet(`twitter-userdata-${id}`, () => { - if (id.startsWith('+')) { - return twitterGot(`${baseUrl}${gqlMap.UserByRestId}`, { - variables: JSON.stringify({ - userId: id.slice(1), - withSafetyModeUserFields: true, - }), - features: JSON.stringify(gqlFeatures.UserByRestId), - fieldToggles: JSON.stringify({ - withAuxiliaryUserLabels: false, - }), - }); - } - return twitterGot(`${baseUrl}${gqlMap.UserByScreenName}`, { - variables: JSON.stringify({ - screen_name: id, - withSafetyModeUserFields: true, - }), - features: JSON.stringify(gqlFeatures.UserByScreenName), + const params = { + variables: id.startsWith('+') + ? JSON.stringify({ + userId: id.slice(1), + withSafetyModeUserFields: true, + }) + : JSON.stringify({ + screen_name: id, + withSafetyModeUserFields: true, + }), + features: JSON.stringify(id.startsWith('+') ? gqlFeatures.UserByRestId : gqlFeatures.UserByScreenName), fieldToggles: JSON.stringify({ withAuxiliaryUserLabels: false, }), + }; + + if (config.twitter.thirdPartyApi) { + const endpoint = id.startsWith('+') ? gqlMap.UserByRestId : gqlMap.UserByScreenName; + + return ofetch(`${config.twitter.thirdPartyApi}${endpoint}`, { + method: 'GET', + params, + }); + } + + return twitterGot(`${baseUrl}${id.startsWith('+') ? gqlMap.UserByRestId : gqlMap.UserByScreenName}`, params, { + allowNoAuth: !id.startsWith('+'), }); }); @@ -34,6 +41,7 @@ const cacheTryGet = async (_id, params, func) => { const userData: any = await getUserData(_id); const id = (userData.data?.user || userData.data?.user_result)?.result?.rest_id; if (id === undefined) { + cache.set(`twitter-userdata-${_id}`, '', config.cache.contentExpire); throw new InvalidParameterError('User not found'); } const funcName = func.name; @@ -97,7 +105,19 @@ const getUserMedia = (id: string, params?: Record<string, any>) => ); }); -const getUserLikes = (id: string, params?: Record<string, any>) => cacheTryGet(id, params, async (id, params = {}) => gatherLegacyFromData(await paginationTweets('Likes', id, params))); +const getUserLikes = (id: string, params?: Record<string, any>) => + cacheTryGet(id, params, async (id, params = {}) => + gatherLegacyFromData( + await paginationTweets('Likes', id, { + ...params, + includeHasBirdwatchNotes: false, + includePromotedContent: false, + withBirdwatchNotes: false, + withVoice: false, + withV2Timeline: true, + }) + ) + ); const getUserTweet = (id: string, params?: Record<string, any>) => cacheTryGet(id, params, async (id, params = {}) => @@ -171,6 +191,23 @@ const getHomeTimeline = async (id: string, params?: Record<string, any>) => ) ); +const getHomeLatestTimeline = async (id: string, params?: Record<string, any>) => + gatherLegacyFromData( + await paginationTweets( + 'HomeLatestTimeline', + undefined, + { + ...params, + count: 20, + includePromotedContent: true, + latestControlAvailable: true, + requestContext: 'launch', + withCommunity: true, + }, + ['home', 'home_timeline_urt'] + ) + ); + export default { getUser, getUserTweets, @@ -181,5 +218,6 @@ export default { getSearch, getList, getHomeTimeline, + getHomeLatestTimeline, init: () => {}, }; diff --git a/lib/routes/twitter/api/web-api/constants.ts b/lib/routes/twitter/api/web-api/constants.ts index c84e3c835fec55..aecefd1ab4cac5 100644 --- a/lib/routes/twitter/api/web-api/constants.ts +++ b/lib/routes/twitter/api/web-api/constants.ts @@ -1,47 +1,53 @@ -const baseUrl = 'https://twitter.com/i/api'; +const baseUrl = 'https://x.com/i/api'; const graphQLEndpointsPlain = [ - '/graphql/eS7LO5Jy3xgmd3dbL044EA/UserTweets', - '/graphql/k5XapwcSikNsEsILW5FvgA/UserByScreenName', - '/graphql/k3YiLNE_MAy5J-NANLERdg/HomeTimeline', - '/graphql/3GeIaLmNhTm1YsUmxR57tg/UserTweetsAndReplies', - '/graphql/TOU4gQw8wXIqpSzA4TYKgg/UserMedia', - '/graphql/B8I_QCljDBVfin21TTWMqA/Likes', - '/graphql/tD8zKvQzwY3kdx5yz6YmOw/UserByRestId', - '/graphql/flaR-PUMshxFWZWPNpq4zA/SearchTimeline', - '/graphql/TOTgqavWmxywKv5IbMMK1w/ListLatestTweetsTimeline', - '/graphql/zJvfJs3gSbrVhC0MKjt_OQ/TweetDetail', + '/graphql/E3opETHurmVJflFsUBVuUQ/UserTweets', + '/graphql/Yka-W8dz7RaEuQNkroPkYw/UserByScreenName', + '/graphql/HJFjzBgCs16TqxewQOeLNg/HomeTimeline', + '/graphql/DiTkXJgLqBBxCs7zaYsbtA/HomeLatestTimeline', + '/graphql/bt4TKuFz4T7Ckk-VvQVSow/UserTweetsAndReplies', + '/graphql/dexO_2tohK86JDudXXG3Yw/UserMedia', + '/graphql/Qw77dDjp9xCpUY-AXwt-yQ/UserByRestId', + '/graphql/UN1i3zUiCWa-6r-Uaho4fw/SearchTimeline', + '/graphql/Pa45JvqZuKcW1plybfgBlQ/ListLatestTweetsTimeline', + '/graphql/QuBlQ6SxNAQCt6-kBiCXCQ/TweetDetail', ]; const gqlMap = Object.fromEntries(graphQLEndpointsPlain.map((endpoint) => [endpoint.split('/')[3].replace(/V2$|Query$|QueryV2$/, ''), endpoint])); +const thirdPartySupportedAPI = ['UserByScreenName', 'UserByRestId', 'UserTweets', 'UserTweetsAndReplies', 'ListLatestTweetsTimeline', 'SearchTimeline']; + const gqlFeatureUser = { - hidden_profile_likes_enabled: true, hidden_profile_subscriptions_enabled: true, + rweb_tipjar_consumption_enabled: true, responsive_web_graphql_exclude_directive_enabled: true, verified_phone_label_enabled: false, subscriptions_verification_info_is_identity_verified_enabled: true, subscriptions_verification_info_verified_since_enabled: true, highlights_tweets_tab_ui_enabled: true, responsive_web_twitter_article_notes_tab_enabled: true, + subscriptions_feature_can_gift_premium: true, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, responsive_web_graphql_timeline_navigation_enabled: true, }; const gqlFeatureFeed = { + rweb_tipjar_consumption_enabled: true, responsive_web_graphql_exclude_directive_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, c9s_tweet_anatomy_moderator_badge_enabled: true, - tweetypie_unmention_optimization_enabled: true, + articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true, responsive_web_twitter_article_tweet_consumption_enabled: true, tweet_awards_web_tipping_enabled: false, + creator_subscriptions_quote_tweet_preview_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, @@ -60,8 +66,7 @@ const TweetDetailFeatures = { responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, communities_web_enable_tweet_community_results_fetch: true, c9s_tweet_anatomy_moderator_badge_enabled: true, - articles_preview_enabled: false, - tweetypie_unmention_optimization_enabled: true, + articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, @@ -72,7 +77,6 @@ const TweetDetailFeatures = { freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, - tweet_with_visibility_results_prefer_gql_media_interstitial_enabled: true, rweb_video_timestamps_enabled: true, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, @@ -87,7 +91,9 @@ const gqlFeatures = { SearchTimeline: gqlFeatureFeed, ListLatestTweetsTimeline: gqlFeatureFeed, HomeTimeline: gqlFeatureFeed, + HomeLatestTimeline: TweetDetailFeatures, TweetDetail: TweetDetailFeatures, + Likes: gqlFeatureFeed, }; const timelineParams = { @@ -108,4 +114,4 @@ const timelineParams = { const bearerToken = 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'; -export { baseUrl, gqlMap, gqlFeatures, timelineParams, bearerToken }; +export { baseUrl, gqlMap, gqlFeatures, timelineParams, bearerToken, thirdPartySupportedAPI }; diff --git a/lib/routes/twitter/api/web-api/login.ts b/lib/routes/twitter/api/web-api/login.ts new file mode 100644 index 00000000000000..1e843b9f2bb57f --- /dev/null +++ b/lib/routes/twitter/api/web-api/login.ts @@ -0,0 +1,75 @@ +import { authenticator } from 'otplib'; +import logger from '@/utils/logger'; +import cache from '@/utils/cache'; +import { RateLimiterMemory, RateLimiterRedis, RateLimiterQueue } from 'rate-limiter-flexible'; +import puppeteer from '@/utils/puppeteer'; +import { CookieJar } from 'tough-cookie'; + +const loginLimiter = cache.clients.redisClient + ? new RateLimiterRedis({ + points: 1, + duration: 20, + execEvenly: true, + storeClient: cache.clients.redisClient, + }) + : new RateLimiterMemory({ + points: 1, + duration: 20, + execEvenly: true, + }); + +const loginLimiterQueue = new RateLimiterQueue(loginLimiter); + +async function login({ username, password, authenticationSecret }) { + if (!username || !password) { + return; + } + try { + await loginLimiterQueue.removeTokens(1); + + const cookieJar = new CookieJar(); + const browser = await puppeteer({ + stealth: true, + }); + const page = await browser.newPage(); + await page.goto('https://x.com/i/flow/login'); + await page.waitForSelector('input[autocomplete="username"]'); + await page.type('input[autocomplete="username"]', username); + const buttons = await page.$$('button'); + await buttons[3]?.click(); + await page.waitForSelector('input[autocomplete="current-password"]'); + await page.type('input[autocomplete="current-password"]', password); + (await page.waitForSelector('button[data-testid="LoginForm_Login_Button"]'))?.click(); + if (authenticationSecret) { + await page.waitForSelector('input[inputmode="numeric"]'); + const token = authenticator.generate(authenticationSecret); + await page.type('input[inputmode="numeric"]', token); + (await page.waitForSelector('button[data-testid="ocfEnterTextNextButton"]'))?.click(); + } + const waitForRequest = new Promise<string>((resolve) => { + page.on('response', async (response) => { + if (response.url().includes('/HomeTimeline')) { + const data = await response.json(); + const message = data?.data?.home?.home_timeline_urt?.instructions?.[0]?.entries?.[0]?.entryId; + if (message === 'messageprompt-suspended-prompt') { + logger.error(`twitter debug: twitter username ${username} login failed: messageprompt-suspended-prompt`); + resolve(''); + } + const cookies = await page.cookies(); + for (const cookie of cookies) { + cookieJar.setCookieSync(`${cookie.name}=${cookie.value}`, 'https://x.com'); + } + logger.debug(`twitter debug: twitter username ${username} login success`); + resolve(JSON.stringify(cookieJar.serializeSync())); + } + }); + }); + const cookieString = await waitForRequest; + await browser.close(); + return cookieString; + } catch (error) { + logger.error(`twitter debug: twitter username ${username} login failed:`, error); + } +} + +export default login; diff --git a/lib/routes/twitter/api/web-api/utils.ts b/lib/routes/twitter/api/web-api/utils.ts index 4477684e9e237f..1a34023c123b95 100644 --- a/lib/routes/twitter/api/web-api/utils.ts +++ b/lib/routes/twitter/api/web-api/utils.ts @@ -1,74 +1,270 @@ import ConfigNotFoundError from '@/errors/types/config-not-found'; -import { baseUrl, gqlFeatures, bearerToken, gqlMap } from './constants'; +import { baseUrl, gqlFeatures, bearerToken, gqlMap, thirdPartySupportedAPI } from './constants'; import { config } from '@/config'; -import got from '@/utils/got'; import queryString from 'query-string'; -import { Cookie } from 'tough-cookie'; +import { Cookie, CookieJar } from 'tough-cookie'; +import { CookieAgent, CookieClient } from 'http-cookie-agent/undici'; +import { ProxyAgent } from 'undici'; +import cache from '@/utils/cache'; +import logger from '@/utils/logger'; +import ofetch from '@/utils/ofetch'; +import proxy from '@/utils/proxy'; +import login from './login'; -export const twitterGot = async (url, params) => { - if (!config.twitter.cookie) { - throw new ConfigNotFoundError('Twitter cookie is not configured'); +let authTokenIndex = 0; + +const token2Cookie = async (token) => { + const c = await cache.get(`twitter:cookie:${token}`); + if (c) { + return c; + } + const jar = new CookieJar(); + await jar.setCookie(`auth_token=${token}`, 'https://x.com'); + try { + const agent = proxy.proxyUri + ? new ProxyAgent({ + factory: (origin, opts) => new CookieClient(origin as string, { ...opts, cookies: { jar } }), + uri: proxy.proxyUri, + }) + : new CookieAgent({ cookies: { jar } }); + if (token) { + await ofetch('https://x.com', { + dispatcher: agent, + }); + } else { + const data = await ofetch('https://x.com/narendramodi?mx=2', { + dispatcher: agent, + }); + const gt = data.match(/document\.cookie="gt=(\d+)/)?.[1]; + if (gt) { + jar.setCookieSync(`gt=${gt}`, 'https://x.com'); + } + } + const cookie = JSON.stringify(jar.serializeSync()); + cache.set(`twitter:cookie:${token}`, cookie); + return cookie; + } catch { + // ignore + return ''; } - const jsonCookie = Object.fromEntries( - config.twitter.cookie - .split(';') - .map((c) => Cookie.parse(c)?.toJSON()) - .map((c) => [c?.key, c?.value]) - ); - if (!jsonCookie || !jsonCookie.auth_token || !jsonCookie.ct0) { - throw new ConfigNotFoundError('Twitter cookie is not valid'); +}; + +const lockPrefix = 'twitter:lock-token1:'; + +const getAuth = async (retry: number) => { + if (config.twitter.authToken && retry > 0) { + const index = authTokenIndex++ % config.twitter.authToken.length; + const token = config.twitter.authToken[index]; + const lock = await cache.get(`${lockPrefix}${token}`, false); + if (lock) { + logger.debug(`twitter debug: twitter cookie for token ${token} is locked, retry: ${retry}`); + await new Promise((resolve) => setTimeout(resolve, Math.random() * 500 + 500)); + return await getAuth(retry - 1); + } else { + logger.debug(`twitter debug: lock twitter cookie for token ${token}`); + await cache.set(`${lockPrefix}${token}`, '1', 20); + return { + token, + username: config.twitter.username?.[index], + password: config.twitter.password?.[index], + authenticationSecret: config.twitter.authenticationSecret?.[index], + }; + } + } +}; + +export const twitterGot = async ( + url, + params, + options?: { + allowNoAuth?: boolean; } +) => { + const auth = await getAuth(30); - const requestData = { - url: `${url}?${queryString.stringify(params)}`, - method: 'GET', + if (!auth && !options?.allowNoAuth) { + throw new ConfigNotFoundError('No valid Twitter token found'); + } + + const requestUrl = `${url}?${queryString.stringify(params)}`; + + let cookie: string | Record<string, any> | null | undefined = await token2Cookie(auth?.token); + if (!cookie && auth) { + cookie = await login({ + username: auth.username, + password: auth.password, + authenticationSecret: auth.authenticationSecret, + }); + } + let dispatchers: + | { + jar: CookieJar; + agent: CookieAgent | ProxyAgent; + } + | undefined; + if (cookie) { + logger.debug(`twitter debug: got twitter cookie for token ${auth?.token}`); + if (typeof cookie === 'string') { + cookie = JSON.parse(cookie); + } + const jar = CookieJar.deserializeSync(cookie as any); + const agent = proxy.proxyUri + ? new ProxyAgent({ + factory: (origin, opts) => new CookieClient(origin as string, { ...opts, cookies: { jar } }), + uri: proxy.proxyUri, + }) + : new CookieAgent({ cookies: { jar } }); + if (proxy.proxyUri) { + logger.debug(`twitter debug: Proxying request: ${requestUrl}`); + } + dispatchers = { + jar, + agent, + }; + } else if (auth) { + throw new ConfigNotFoundError(`Twitter cookie for token ${auth?.token?.replace(/(\w{8})(\w+)/, (_, v1, v2) => v1 + '*'.repeat(v2.length))} is not valid`); + } + const jsonCookie = dispatchers + ? Object.fromEntries( + dispatchers.jar + .getCookieStringSync(url) + .split(';') + .map((c) => Cookie.parse(c)?.toJSON()) + .map((c) => [c?.key, c?.value]) + ) + : {}; + + const response = await ofetch.raw(requestUrl, { + retry: 0, headers: { - authority: 'twitter.com', + authority: 'x.com', accept: '*/*', 'accept-language': 'en-US,en;q=0.9', authorization: bearerToken, 'cache-control': 'no-cache', 'content-type': 'application/json', - cookie: config.twitter.cookie, dnt: '1', pragma: 'no-cache', - referer: 'https://twitter.com/narendramodi', - 'x-csrf-token': jsonCookie.ct0, + referer: 'https://x.com/', 'x-twitter-active-user': 'yes', - 'x-twitter-auth-type': 'OAuth2Session', 'x-twitter-client-language': 'en', + 'x-csrf-token': jsonCookie.ct0, + ...(auth?.token + ? { + 'x-twitter-auth-type': 'OAuth2Session', + } + : { + 'x-guest-token': jsonCookie.gt, + }), + }, + dispatcher: dispatchers?.agent, + onResponse: async ({ response }) => { + const remaining = response.headers.get('x-rate-limit-remaining'); + const remainingInt = Number.parseInt(remaining || '0'); + const reset = response.headers.get('x-rate-limit-reset'); + logger.debug( + `twitter debug: twitter rate limit remaining for token ${auth?.token} is ${remaining} and reset at ${reset}, auth: ${JSON.stringify(auth)}, status: ${response.status}, data: ${JSON.stringify(response._data?.data)}, cookie: ${JSON.stringify(dispatchers?.jar.serializeSync())}` + ); + if (auth) { + if (remaining && remainingInt < 2 && reset) { + const resetTime = new Date(Number.parseInt(reset) * 1000); + const delay = (resetTime.getTime() - Date.now()) / 1000; + logger.debug(`twitter debug: twitter rate limit exceeded for token ${auth.token} with status ${response.status}, will unlock after ${delay}s`); + await cache.set(`${lockPrefix}${auth.token}`, '1', Math.ceil(delay) * 2); + } else if (response.status === 429 || JSON.stringify(response._data?.data) === '{"user":{}}') { + logger.debug(`twitter debug: twitter rate limit exceeded for token ${auth.token} with status ${response.status}`); + await cache.set(`${lockPrefix}${auth.token}`, '1', 2000); + } else if (response.status === 403 || response.status === 401) { + const newCookie = await login({ + username: auth.username, + password: auth.password, + authenticationSecret: auth.authenticationSecret, + }); + if (newCookie) { + logger.debug(`twitter debug: reset twitter cookie for token ${auth.token}, ${newCookie}`); + await cache.set(`twitter:cookie:${auth.token}`, newCookie, config.cache.contentExpire); + logger.debug(`twitter debug: unlock twitter cookie for token ${auth.token} with error1`); + await cache.set(`${lockPrefix}${auth.token}`, '', 1); + } else { + const tokenIndex = config.twitter.authToken?.indexOf(auth.token); + if (tokenIndex !== undefined && tokenIndex !== -1) { + config.twitter.authToken?.splice(tokenIndex, 1); + } + if (auth.username) { + const usernameIndex = config.twitter.username?.indexOf(auth.username); + if (usernameIndex !== undefined && usernameIndex !== -1) { + config.twitter.username?.splice(usernameIndex, 1); + } + } + if (auth.password) { + const passwordIndex = config.twitter.password?.indexOf(auth.password); + if (passwordIndex !== undefined && passwordIndex !== -1) { + config.twitter.password?.splice(passwordIndex, 1); + } + } + logger.debug(`twitter debug: delete twitter cookie for token ${auth.token} with status ${response.status}, remaining tokens: ${config.twitter.authToken?.length}`); + await cache.set(`${lockPrefix}${auth.token}`, '1', 86400); + } + } else { + logger.debug(`twitter debug: unlock twitter cookie with success for token ${auth.token}`); + await cache.set(`${lockPrefix}${auth.token}`, '', 1); + } + } }, - }; - - const response = await got(requestData.url, { - headers: requestData.headers, }); - return response.data; + if (auth?.token) { + logger.debug(`twitter debug: update twitter cookie for token ${auth.token}`); + await cache.set(`twitter:cookie:${auth.token}`, JSON.stringify(dispatchers?.jar.serializeSync()), config.cache.contentExpire); + } + + return response._data; }; export const paginationTweets = async (endpoint: string, userId: number | undefined, variables: Record<string, any>, path?: string[]) => { - const { data } = await twitterGot(baseUrl + gqlMap[endpoint], { - variables: JSON.stringify({ - ...variables, - userId, - }), + const params = { + variables: JSON.stringify({ ...variables, userId }), features: JSON.stringify(gqlFeatures[endpoint]), - }); - let instructions; - if (path) { - instructions = data; - for (const p of path) { - instructions = instructions[p]; + }; + + const fetchData = async () => { + if (config.twitter.thirdPartyApi && thirdPartySupportedAPI.includes(endpoint)) { + const { data } = await ofetch(`${config.twitter.thirdPartyApi}${gqlMap[endpoint]}`, { + method: 'GET', + params, + }); + return data; } - instructions = instructions.instructions; - } else { - instructions = data.user.result.timeline_v2.timeline.instructions; + const { data } = await twitterGot(baseUrl + gqlMap[endpoint], params); + return data; + }; + + const getInstructions = (data: any) => { + if (path) { + let instructions = data; + for (const p of path) { + instructions = instructions[p]; + } + return instructions.instructions; + } + + const instructions = data?.user?.result?.timeline_v2?.timeline?.instructions; + if (!instructions) { + logger.debug(`twitter debug: instructions not found in data: ${JSON.stringify(data)}`); + } + return instructions; + }; + + const data = await fetchData(); + const instructions = getInstructions(data); + if (!instructions) { + return []; } - const entries1 = instructions.find((i) => i.type === 'TimelineAddToModule')?.moduleItems; // Media - const entries2 = instructions.find((i) => i.type === 'TimelineAddEntries').entries; - return entries1 || entries2; + const moduleItems = instructions.find((i) => i.type === 'TimelineAddToModule')?.moduleItems; + const entries = instructions.find((i) => i.type === 'TimelineAddEntries')?.entries; + + return moduleItems || entries || []; }; export function gatherLegacyFromData(entries: any[], filterNested?: string[], userId?: number | string) { @@ -107,6 +303,14 @@ export function gatherLegacyFromData(entries: any[], filterNested?: string[], us t.legacy.quoted_status = quote.legacy; t.legacy.quoted_status.user = quote.core.user_result?.result?.legacy || quote.core.user_results?.result?.legacy; } + if (t.note_tweet) { + const tmp = t.note_tweet.note_tweet_results.result; + t.legacy.entities.hashtags = tmp.entity_set.hashtags; + t.legacy.entities.symbols = tmp.entity_set.symbols; + t.legacy.entities.urls = tmp.entity_set.urls; + t.legacy.entities.user_mentions = tmp.entity_set.user_mentions; + t.legacy.full_text = tmp.text; + } } const legacy = tweet.legacy; if (legacy) { diff --git a/lib/routes/twitter/home-latest.ts b/lib/routes/twitter/home-latest.ts new file mode 100644 index 00000000000000..6c1a7b51bac1c4 --- /dev/null +++ b/lib/routes/twitter/home-latest.ts @@ -0,0 +1,63 @@ +import { Route } from '@/types'; +import utils from './utils'; +import api from './api'; + +export const route: Route = { + path: '/home_latest/:routeParams?', + categories: ['social-media'], + example: '/twitter/home_latest', + features: { + requireConfig: [ + { + name: 'TWITTER_USERNAME', + description: 'Please see above for details.', + }, + { + name: 'TWITTER_PASSWORD', + description: 'Please see above for details.', + }, + { + name: 'TWITTER_AUTH_TOKEN', + description: 'Please see above for details.', + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Home latest timeline', + maintainers: ['DIYgod', 'CaoMeiYouRen'], + handler, + radar: [ + { + source: ['x.com/home'], + target: '/home_latest', + }, + ], +}; + +async function handler(ctx) { + // For compatibility + const { count, include_rts, only_media } = utils.parseRouteParams(ctx.req.param('routeParams')); + const params = count ? { count } : {}; + + await api.init(); + let data = await api.getHomeLatestTimeline('', params); + if (!include_rts) { + data = utils.excludeRetweet(data); + } + if (only_media) { + data = utils.keepOnlyMedia(data); + } + + return { + title: `Twitter following timeline`, + link: `https://x.com/home`, + // description: userInfo?.description, + item: utils.ProcessFeed(ctx, { + data, + }), + }; +} diff --git a/lib/routes/twitter/home.ts b/lib/routes/twitter/home.ts index 4b8b1254c9c7b9..1516c6829f2c69 100644 --- a/lib/routes/twitter/home.ts +++ b/lib/routes/twitter/home.ts @@ -17,7 +17,7 @@ export const route: Route = { description: 'Please see above for details.', }, { - name: 'TWITTER_COOKIE', + name: 'TWITTER_AUTH_TOKEN', description: 'Please see above for details.', }, ], @@ -28,11 +28,11 @@ export const route: Route = { supportScihub: false, }, name: 'Home timeline', - maintainers: ['DIYgod'], + maintainers: ['DIYgod', 'CaoMeiYouRen'], handler, radar: [ { - source: ['twitter.com/home'], + source: ['x.com/home'], target: '/home', }, ], @@ -40,7 +40,7 @@ export const route: Route = { async function handler(ctx) { // For compatibility - const { count, include_rts } = utils.parseRouteParams(ctx.req.param('routeParams')); + const { count, include_rts, only_media } = utils.parseRouteParams(ctx.req.param('routeParams')); const params = count ? { count } : {}; await api.init(); @@ -48,10 +48,13 @@ async function handler(ctx) { if (!include_rts) { data = utils.excludeRetweet(data); } + if (only_media) { + data = utils.keepOnlyMedia(data); + } return { title: `Twitter following timeline`, - link: `https://twitter.com/home`, + link: `https://x.com/home`, // description: userInfo?.description, item: utils.ProcessFeed(ctx, { data, diff --git a/lib/routes/twitter/keyword.ts b/lib/routes/twitter/keyword.ts index 2954a0ab6b5cdb..596ff5b9312d17 100644 --- a/lib/routes/twitter/keyword.ts +++ b/lib/routes/twitter/keyword.ts @@ -1,10 +1,11 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import api from './api'; import utils from './utils'; export const route: Route = { path: '/keyword/:keyword/:routeParams?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/twitter/keyword/RSSHub', parameters: { keyword: 'keyword', routeParams: 'extra parameters, see the table above' }, features: { @@ -18,7 +19,11 @@ export const route: Route = { description: 'Please see above for details.', }, { - name: 'TWITTER_COOKIE', + name: 'TWITTER_AUTH_TOKEN', + description: 'Please see above for details.', + }, + { + name: 'TWITTER_THIRD_PARTY_API', description: 'Please see above for details.', }, ], @@ -29,11 +34,11 @@ export const route: Route = { supportScihub: false, }, name: 'Keyword', - maintainers: ['DIYgod', 'yindaheng98', 'Rongronggg9'], + maintainers: ['DIYgod', 'yindaheng98', 'Rongronggg9', 'pseudoyu'], handler, radar: [ { - source: ['twitter.com/search'], + source: ['x.com/search'], }, ], }; @@ -45,7 +50,7 @@ async function handler(ctx) { return { title: `Twitter Keyword - ${keyword}`, - link: `https://twitter.com/search?q=${encodeURIComponent(keyword)}`, + link: `https://x.com/search?q=${encodeURIComponent(keyword)}`, item: utils.ProcessFeed(ctx, { data, }), diff --git a/lib/routes/twitter/likes.ts b/lib/routes/twitter/likes.ts index 4f69bbe21a0f2d..76f3ebe98466c1 100644 --- a/lib/routes/twitter/likes.ts +++ b/lib/routes/twitter/likes.ts @@ -1,7 +1,6 @@ import { Route } from '@/types'; import utils from './utils'; -import { config } from '@/config'; -import ConfigNotFoundError from '@/errors/types/config-not-found'; +import api from './api'; export const route: Route = { path: '/likes/:id/:routeParams?', @@ -9,7 +8,12 @@ export const route: Route = { example: '/twitter/likes/DIYgod', parameters: { id: 'username', routeParams: 'extra parameters, see the table above' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'TWITTER_AUTH_TOKEN', + description: 'Please see above for details.', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -22,19 +26,22 @@ export const route: Route = { }; async function handler(ctx) { - if (!config.twitter || !config.twitter.consumer_key || !config.twitter.consumer_secret) { - throw new ConfigNotFoundError('Twitter RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>'); - } const id = ctx.req.param('id'); - const client = await utils.getAppClient(); - const data = await client.v1.get('favorites/list.json', { - screen_name: id, - tweet_mode: 'extended', - }); + const { count, include_rts, only_media } = utils.parseRouteParams(ctx.req.param('routeParams')); + const params = count ? { count } : {}; + + await api.init(); + let data = await api.getUserLikes(id, params); + if (!include_rts) { + data = utils.excludeRetweet(data); + } + if (only_media) { + data = utils.keepOnlyMedia(data); + } return { title: `Twitter Likes - ${id}`, - link: `https://twitter.com/${id}/likes`, + link: `https://x.com/${id}/likes`, item: utils.ProcessFeed(ctx, { data, }), diff --git a/lib/routes/twitter/list.ts b/lib/routes/twitter/list.ts index 645c17c0cc8018..1618f3920668ad 100644 --- a/lib/routes/twitter/list.ts +++ b/lib/routes/twitter/list.ts @@ -4,21 +4,17 @@ import utils from './utils'; export const route: Route = { path: '/list/:id/:routeParams?', - categories: ['social-media'], - example: '/twitter/list/ladyleet/javascript', - parameters: { id: 'username', name: 'list name', routeParams: 'extra parameters, see the table above' }, + categories: ['social-media', 'popular'], + example: '/twitter/list/1502570462752219136', + parameters: { id: 'list id, get from url', routeParams: 'extra parameters, see the table above' }, features: { requireConfig: [ { - name: 'TWITTER_USERNAME', + name: 'TWITTER_AUTH_TOKEN', description: 'Please see above for details.', }, { - name: 'TWITTER_PASSWORD', - description: 'Please see above for details.', - }, - { - name: 'TWITTER_COOKIE', + name: 'TWITTER_THIRD_PARTY_API', description: 'Please see above for details.', }, ], @@ -29,11 +25,11 @@ export const route: Route = { supportScihub: false, }, name: 'List timeline', - maintainers: ['DIYgod', 'xyqfer'], + maintainers: ['DIYgod', 'xyqfer', 'pseudoyu'], handler, radar: [ { - source: ['twitter.com/i/lists/:id'], + source: ['x.com/i/lists/:id'], target: '/list/:id', }, ], @@ -41,15 +37,21 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const { count } = utils.parseRouteParams(ctx.req.param('routeParams')); + const { count, include_rts, only_media } = utils.parseRouteParams(ctx.req.param('routeParams')); const params = count ? { count } : {}; await api.init(); - const data = await api.getList(id, params); + let data = await api.getList(id, params); + if (!include_rts) { + data = utils.excludeRetweet(data); + } + if (only_media) { + data = utils.keepOnlyMedia(data); + } return { title: `Twitter List - ${id}`, - link: `https://twitter.com/i/lists/${id}`, + link: `https://x.com/i/lists/${id}`, item: utils.ProcessFeed(ctx, { data, }), diff --git a/lib/routes/twitter/media.ts b/lib/routes/twitter/media.ts index c43ad10c289f7d..a8f87abcaf64b8 100644 --- a/lib/routes/twitter/media.ts +++ b/lib/routes/twitter/media.ts @@ -1,11 +1,13 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import api from './api'; import utils from './utils'; +import logger from '@/utils/logger'; export const route: Route = { path: '/media/:id/:routeParams?', - categories: ['social-media'], - example: '/twitter/media/DIYgod', + categories: ['social-media', 'popular'], + view: ViewType.Pictures, + example: '/twitter/media/_RSSHub', parameters: { id: 'username; in particular, if starts with `+`, it will be recognized as a [unique ID](https://github.com/DIYgod/RSSHub/issues/12221), e.g. `+44196397`', routeParams: 'extra parameters, see the table above.' }, features: { requireConfig: [ @@ -18,7 +20,7 @@ export const route: Route = { description: 'Please see above for details.', }, { - name: 'TWITTER_COOKIE', + name: 'TWITTER_AUTH_TOKEN', description: 'Please see above for details.', }, ], @@ -33,7 +35,7 @@ export const route: Route = { handler, radar: [ { - source: ['twitter.com/:id/media'], + source: ['x.com/:id/media'], target: '/media/:id', }, ], @@ -46,16 +48,24 @@ async function handler(ctx) { await api.init(); const userInfo = await api.getUser(id); - const data = await api.getUserMedia(id, params); + let data; + try { + data = await api.getUserMedia(id, params); + } catch (error) { + logger.error(error); + } const profileImageUrl = userInfo?.profile_image_url || userInfo?.profile_image_url_https; return { title: `Twitter @${userInfo?.name}`, - link: `https://twitter.com/${userInfo?.screen_name}/media`, + link: `https://x.com/${userInfo?.screen_name}/media`, image: profileImageUrl.replace(/_normal.jpg$/, '.jpg'), description: userInfo?.description, - item: utils.ProcessFeed(ctx, { - data, - }), + item: + data && + utils.ProcessFeed(ctx, { + data, + }), + allowEmpty: true, }; } diff --git a/lib/routes/twitter/namespace.ts b/lib/routes/twitter/namespace.ts index 38f1269e0db080..a2d1ccd9b5756a 100644 --- a/lib/routes/twitter/namespace.ts +++ b/lib/routes/twitter/namespace.ts @@ -1,8 +1,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Twitter', - url: 'twitter.com', + name: 'X (Twitter)', + url: 'x.com', description: `Specify options (in the format of query string) in parameter \`routeParams\` to control some extra features for Tweets | Key | Description | Accepts | Defaults to | @@ -10,6 +10,7 @@ export const namespace: Namespace = { | \`readable\` | Enable readable layout | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | | \`authorNameBold\` | Display author name in bold | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | | \`showAuthorInTitle\` | Show author name in title | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` (\`true\` in \`/twitter/followings\`) | +| \`showAuthorAsTitleOnly\` | Show only author name as title | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | | \`showAuthorInDesc\` | Show author name in description (RSS body) | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` (\`true\` in \`/twitter/followings\`) | | \`showQuotedAuthorAvatarInDesc\` | Show avatar of quoted Tweet's author in description (RSS body) (Not recommended if your RSS reader extracts images from description) | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | | \`showAuthorAvatarInDesc\` | Show avatar of author in description (RSS body) (Not recommended if your RSS reader extracts images from description) | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | @@ -23,10 +24,11 @@ export const namespace: Namespace = { | \`heightOfPics\` | Height of Tweet pictures | Unspecified/Integer | Unspecified | | \`sizeOfAuthorAvatar\` | Size of author's avatar | Integer | \`48\` | | \`sizeOfQuotedAuthorAvatar\` | Size of quoted tweet's author's avatar | Integer | \`24\` | -| \`excludeReplies\` | Exclude replies, only available in \`/twitter/user\` | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | +| \`includeReplies\` | Include replies, only available in \`/twitter/user\` | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | | \`includeRts\` | Include retweets, only available in \`/twitter/user\` | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | | \`forceWebApi\` | Force using Web API even if Developer API is configured, only available in \`/twitter/user\` and \`/twitter/keyword\` | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | | \`count\` | \`count\` parameter passed to Twitter API, only available in \`/twitter/user\` | Unspecified/Integer | Unspecified | +| \`onlyMedia\` | Only get tweets with a media | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | Specify different option values than default values to improve readability. The URL @@ -40,8 +42,9 @@ generates Currently supports two authentication methods: -- Using \`TWITTER_COOKIE\` (recommended): Configure the cookies of logged-in Twitter Web, at least including the fields auth_token and ct0. RSSHub will use this information to directly access Twitter's web API to obtain data. +- Using \`TWITTER_AUTH_TOKEN\` (recommended): Configure a comma-separated list of \`auth_token\` cookies of logged-in Twitter Web. RSSHub will use this information to directly access Twitter's web API to obtain data. -- Using \`TWITTER_USERNAME\` \`TWITTER_PASSWORD\` and \`TWITTER_AUTHENTICATION_SECRET\`: Configure the Twitter username and password. RSSHub will use this information to log in to Twitter and obtain data using the mobile API. Please note that if you have not logged in with the current IP address before, it is easy to trigger Twitter's risk control mechanism. +- Using \`TWITTER_USERNAME\` \`TWITTER_PASSWORD\` and \`TWITTER_AUTHENTICATION_SECRET\`: Configure a comma-separated list of Twitter username and password. RSSHub will use this information to log in to Twitter and obtain data using the mobile API. Please note that if you have not logged in with the current IP address before, it is easy to trigger Twitter's risk control mechanism. `, + lang: 'en', }; diff --git a/lib/routes/twitter/trends.ts b/lib/routes/twitter/trends.ts index 37d28812d45731..5bfb1d56e1310c 100644 --- a/lib/routes/twitter/trends.ts +++ b/lib/routes/twitter/trends.ts @@ -32,7 +32,7 @@ async function handler(ctx) { return { title: `Twitter Trends on ${data[0].locations[0].name}`, - link: `https://twitter.com/i/trends`, + link: `https://x.com/i/trends`, item: trends .filter((t) => !t.promoted_content) .map((t) => ({ diff --git a/lib/routes/twitter/tweet.ts b/lib/routes/twitter/tweet.ts index 1883869ce5392c..9b809e4e767a00 100644 --- a/lib/routes/twitter/tweet.ts +++ b/lib/routes/twitter/tweet.ts @@ -2,6 +2,7 @@ import { Route } from '@/types'; import api from './api'; import utils from './utils'; import { fallback, queryToBoolean } from '@/utils/readable-social'; +import { config } from '@/config'; export const route: Route = { path: '/tweet/:id/status/:status/:original?', @@ -57,7 +58,7 @@ async function handler(ctx) { return { title: `Twitter @${userInfo.name}`, - link: `https://twitter.com/${userInfo.screen_name}/status/${status}`, + link: `https://x.com/${userInfo.screen_name}/status/${status}`, image: profileImageUrl.replace(/_normal.jpg$/, '.jpg'), description: userInfo.description, item, diff --git a/lib/routes/twitter/user.ts b/lib/routes/twitter/user.ts index 6d78b8f152c191..ae7e1596974ebd 100644 --- a/lib/routes/twitter/user.ts +++ b/lib/routes/twitter/user.ts @@ -1,15 +1,16 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import utils from './utils'; import api from './api'; +import logger from '@/utils/logger'; export const route: Route = { path: '/user/:id/:routeParams?', - categories: ['social-media'], - example: '/twitter/user/DIYgod', + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, + example: '/twitter/user/_RSSHub', parameters: { id: 'username; in particular, if starts with `+`, it will be recognized as a [unique ID](https://github.com/DIYgod/RSSHub/issues/12221), e.g. `+44196397`', - routeParams: - 'extra parameters, see the table above; particularly when `routeParams=exclude_replies`, replies are excluded; `routeParams=exclude_rts` excludes retweets,`routeParams=exclude_rts_replies` exclude replies and retweets; for default include all.', + routeParams: 'extra parameters, see the table above', }, features: { requireConfig: [ @@ -27,9 +28,14 @@ export const route: Route = { optional: true, }, { - name: 'TWITTER_COOKIE', + name: 'TWITTER_AUTH_TOKEN', description: 'Please see above for details.', }, + { + name: 'TWITTER_THIRD_PARTY_API', + description: 'Use third-party API to query twitter data', + optional: true, + }, ], requirePuppeteer: false, antiCrawler: false, @@ -38,11 +44,11 @@ export const route: Route = { supportScihub: false, }, name: 'User timeline', - maintainers: ['DIYgod', 'yindaheng98', 'Rongronggg9'], + maintainers: ['DIYgod', 'yindaheng98', 'Rongronggg9', 'CaoMeiYouRen', 'pseudoyu'], handler, radar: [ { - source: ['twitter.com/:id'], + source: ['x.com/:id'], target: '/user/:id', }, ], @@ -52,25 +58,33 @@ async function handler(ctx) { const id = ctx.req.param('id'); // For compatibility - const { count, exclude_replies, include_rts } = utils.parseRouteParams(ctx.req.param('routeParams')); + const { count, include_replies, include_rts } = utils.parseRouteParams(ctx.req.param('routeParams')); const params = count ? { count } : {}; await api.init(); const userInfo = await api.getUser(id); - let data = await (exclude_replies ? api.getUserTweets(id, params) : api.getUserTweetsAndReplies(id, params)); - if (!include_rts) { - data = utils.excludeRetweet(data); + let data; + try { + data = await (include_replies ? api.getUserTweetsAndReplies(id, params) : api.getUserTweets(id, params)); + if (!include_rts) { + data = utils.excludeRetweet(data); + } + } catch (error) { + logger.error(error); } const profileImageUrl = userInfo?.profile_image_url || userInfo?.profile_image_url_https; return { title: `Twitter @${userInfo?.name}`, - link: `https://twitter.com/${userInfo?.screen_name}`, + link: `https://x.com/${userInfo?.screen_name}`, image: profileImageUrl.replace(/_normal.jpg$/, '.jpg'), description: userInfo?.description, - item: utils.ProcessFeed(ctx, { - data, - }), + item: + data && + utils.ProcessFeed(ctx, { + data, + }), + allowEmpty: true, }; } diff --git a/lib/routes/twitter/utils.ts b/lib/routes/twitter/utils.ts index d6a8e3815d6ab8..bbd983404ddff8 100644 --- a/lib/routes/twitter/utils.ts +++ b/lib/routes/twitter/utils.ts @@ -35,7 +35,7 @@ const formatText = (item) => { const urls = item.entities.urls || []; for (const url of urls) { // trim link pointing to the tweet itself (usually appears when the tweet is truncated) - text = text.replaceAll(url.url, url.expanded_url.endsWith(id_str) ? '' : url.expanded_url); + text = text.replaceAll(url.url, url.expanded_url?.endsWith(id_str) ? '' : url.expanded_url); } const media = item.extended_entities?.media || []; for (const m of media) { @@ -52,6 +52,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { readable: fallback(params.readable, queryToBoolean(routeParams.get('readable')), false), authorNameBold: fallback(params.authorNameBold, queryToBoolean(routeParams.get('authorNameBold')), false), showAuthorInTitle: fallback(params.showAuthorInTitle, queryToBoolean(routeParams.get('showAuthorInTitle')), false), + showAuthorAsTitleOnly: fallback(params.showAuthorAsTitleOnly, queryToBoolean(routeParams.get('showAuthorAsTitleOnly')), false), showAuthorInDesc: fallback(params.showAuthorInDesc, queryToBoolean(routeParams.get('showAuthorInDesc')), false), showQuotedAuthorAvatarInDesc: fallback(params.showQuotedAuthorAvatarInDesc, queryToBoolean(routeParams.get('showQuotedAuthorAvatarInDesc')), false), showAuthorAvatarInDesc: fallback(params.showAuthorAvatarInDesc, queryToBoolean(routeParams.get('showAuthorAvatarInDesc')), false), @@ -74,6 +75,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { readable, authorNameBold, showAuthorInTitle, + showAuthorAsTitleOnly, showAuthorInDesc, showQuotedAuthorAvatarInDesc, showAuthorAvatarInDesc, @@ -104,7 +106,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { if (!readable) { content += '<br>'; } - content += `<video src='${video.url}' ${gifAutoPlayAttr} controls='controls' poster='${getOriginalImg(media.media_url_https)}' ${extraAttrs}></video>`; + content += `<video width="${media.sizes.large.w}" height="${media.sizes.large.h}" src='${video.url}' ${gifAutoPlayAttr} controls='controls' poster='${getOriginalImg(media.media_url_https)}' ${extraAttrs}></video>`; } return content; @@ -114,7 +116,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { let img = ''; if (item.extended_entities) { for (const media of item.extended_entities.media) { - // https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/extended-entities-object + // https://developer.x.com/en/docs/tweets/data-dictionary/overview/extended-entities-object let content = ''; let style = ''; let originalImg; @@ -142,6 +144,9 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { content += `height="${heightOfPics}" `; style += `height: ${heightOfPics}px;`; } + if (widthOfPics <= 0 && heightOfPics <= 0) { + content += `width="${media.sizes.large.w}" height="${media.sizes.large.h}" `; + } content += ` style="${style}" ` + `${readable ? 'hspace="4" vspace="8"' : ''} src="${originalImg}">`; if (addLinkForPics) { content += `</a>`; @@ -211,7 +216,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { } if (readable) { - quote += `<a href='https://twitter.com/${author.screen_name}' target='_blank' rel='noopener noreferrer'>`; + quote += `<a href='https://x.com/${author.screen_name}' target='_blank' rel='noopener noreferrer'>`; } if (showQuotedAuthorAvatarInDesc) { @@ -240,11 +245,11 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { } quote += formatMedia(quoteData); picsPrefix += generatePicsPrefix(quoteData); - quoteInTitle += showEmojiForRetweetAndReply ? ' 💬 ' : showSymbolForRetweetAndReply ? ' RT ' : ''; + quoteInTitle += showEmojiForRetweetAndReply ? ' 💬 ' : (showSymbolForRetweetAndReply ? ' RT ' : ''); quoteInTitle += `${author.name}: ${formatText(quoteData)}`; if (readable) { - quote += `<br><small>Link: <a href='https://twitter.com/${author.screen_name}/status/${quoteData.id_str || quoteData.conversation_id_str}' target='_blank' rel='noopener noreferrer'>https://twitter.com/${ + quote += `<br><small>Link: <a href='https://x.com/${author.screen_name}/status/${quoteData.id_str || quoteData.conversation_id_str}' target='_blank' rel='noopener noreferrer'>https://x.com/${ author.screen_name }/status/${quoteData.id_str || quoteData.conversation_id_str}</a></small>`; } @@ -272,15 +277,15 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { const isQuote = item.is_quote_status; if (!isRetweet && (!isQuote || showRetweetTextInTitle)) { if (item.in_reply_to_screen_name) { - title += showEmojiForRetweetAndReply ? '↩️ ' : showSymbolForRetweetAndReply ? 'Re ' : ''; + title += showEmojiForRetweetAndReply ? '↩️ ' : (showSymbolForRetweetAndReply ? 'Re ' : ''); } title += replaceBreak(originalItem.full_text); } if (isRetweet) { - title += showEmojiForRetweetAndReply ? '🔁 ' : showSymbolForRetweetAndReply ? 'RT ' : ''; + title += showEmojiForRetweetAndReply ? '🔁 ' : (showSymbolForRetweetAndReply ? 'RT ' : ''); title += item.user.name + ': '; if (item.in_reply_to_screen_name) { - title += showEmojiForRetweetAndReply ? ' ↩️ ' : showSymbolForRetweetAndReply ? ' Re ' : ''; + title += showEmojiForRetweetAndReply ? ' ↩️ ' : (showSymbolForRetweetAndReply ? ' Re ' : ''); } title += replaceBreak(item.full_text); } @@ -289,6 +294,10 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { title += quoteInTitle; } + if (showAuthorAsTitleOnly) { + title = originalItem.user.name; + } + // Make description let description = ''; if (showAuthorInDesc && showAuthorAvatarInDesc) { @@ -298,7 +307,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { if (showAuthorInDesc) { if (readable) { description += '<small>'; - description += `<a href='https://twitter.com/${originalItem.user.screen_name}' target='_blank' rel='noopener noreferrer'>`; + description += `<a href='https://x.com/${originalItem.user.screen_name}' target='_blank' rel='noopener noreferrer'>`; } if (authorNameBold) { description += `<strong>`; @@ -312,11 +321,11 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { } description += ' '; } - description += showEmojiForRetweetAndReply ? '🔁' : showSymbolForRetweetAndReply ? 'RT' : ''; + description += showEmojiForRetweetAndReply ? '🔁' : (showSymbolForRetweetAndReply ? 'RT' : ''); if (!showAuthorInDesc) { description += ' '; if (readable) { - description += `<a href='https://twitter.com/${item.user.screen_name}' target='_blank' rel='noopener noreferrer'>`; + description += `<a href='https://x.com/${item.user.screen_name}' target='_blank' rel='noopener noreferrer'>`; } if (authorNameBold) { description += `<strong>`; @@ -336,7 +345,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { } if (showAuthorInDesc) { if (readable) { - description += `<a href='https://twitter.com/${item.user.screen_name}' target='_blank' rel='noopener noreferrer'>`; + description += `<a href='https://x.com/${item.user.screen_name}' target='_blank' rel='noopener noreferrer'>`; } if (showAuthorAvatarInDesc) { @@ -355,13 +364,14 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { description += `: `; } if (item.in_reply_to_screen_name) { - description += showEmojiForRetweetAndReply ? '↩️ ' : showSymbolForRetweetAndReply ? 'Re ' : ''; + description += showEmojiForRetweetAndReply ? '↩️ ' : (showSymbolForRetweetAndReply ? 'Re ' : ''); } description += item.full_text; + // 从 description 提取 话题作为 category,放在此处是为了避免 匹配到 quote 中的 # 80808030 颜色字符 + const category = description.match(/(\s)?(#[^\s;<]+)/g)?.map((e) => e?.match(/#([^\s<]+)/)?.[1]); description += img; description += quote; - if (readable) { description += `<br clear='both' /><div style='clear: both'></div>`; } @@ -373,22 +383,29 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { description += `<small>${parseDate(item.created_at)}</small>`; } - const authorName = originalItem.user.name; const link = originalItem.user.screen_name && (originalItem.id_str || originalItem.conversation_id_str) - ? `https://twitter.com/${originalItem.user.screen_name}/status/${originalItem.id_str || originalItem.conversation_id_str}` - : `https://twitter.com/${item.user.screen_name}/status/${item.id_str || item.conversation_id_str}`; + ? `https://x.com/${originalItem.user.screen_name}/status/${originalItem.id_str || originalItem.conversation_id_str}` + : `https://x.com/${item.user.screen_name}/status/${item.id_str || item.conversation_id_str}`; return { title, - author: authorName, + author: [ + { + name: originalItem.user.name, + url: `https://x.com/${originalItem.user.screen_name}`, + avatar: originalItem.user.profile_image_url_https, + }, + ], description, pubDate: parseDate(item.created_at), link, - + guid: link.replace('x.com', 'twitter.com'), + category, _extra: (isRetweet && { links: [ { + url: `https://x.com/${item.user?.screen_name}/status/${item.conversation_id_str}`, type: 'repost', }, ], @@ -396,7 +413,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { (item.is_quote_status && { links: [ { - url: `https://twitter.com/${item.quoted_status?.user?.screen_name}/status/${item.quoted_status?.id_str || item.quoted_status?.conversation_id_str}`, + url: `https://x.com/${item.quoted_status?.user?.screen_name}/status/${item.quoted_status?.id_str || item.quoted_status?.conversation_id_str}`, type: 'quote', }, ], @@ -405,7 +422,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { item.in_reply_to_status_id_str && { links: [ { - url: `https://twitter.com/${item.in_reply_to_screen_name}/status/${item.in_reply_to_status_id_str}`, + url: `https://x.com/${item.in_reply_to_screen_name}/status/${item.in_reply_to_status_id_str}`, type: 'reply', }, ], @@ -441,24 +458,24 @@ if (config.twitter.consumer_key && config.twitter.consumer_secret) { } const parseRouteParams = (routeParams) => { - let count, exclude_replies, include_rts; + let count, include_replies, include_rts, only_media; let force_web_api = false; switch (routeParams) { case 'exclude_rts_replies': case 'exclude_replies_rts': - exclude_replies = true; + include_replies = false; include_rts = false; break; - case 'exclude_replies': - exclude_replies = true; + case 'include_replies': + include_replies = true; include_rts = true; break; case 'exclude_rts': - exclude_replies = false; + include_replies = false; include_rts = false; break; @@ -466,12 +483,13 @@ const parseRouteParams = (routeParams) => { default: { const parsed = new URLSearchParams(routeParams); count = fallback(undefined, queryToInteger(parsed.get('count'))); - exclude_replies = fallback(undefined, queryToBoolean(parsed.get('excludeReplies')), false); + include_replies = fallback(undefined, queryToBoolean(parsed.get('includeReplies')), false); include_rts = fallback(undefined, queryToBoolean(parsed.get('includeRts')), true); force_web_api = fallback(undefined, queryToBoolean(parsed.get('forceWebApi')), false); + only_media = fallback(undefined, queryToBoolean(parsed.get('onlyMedia')), false); } } - return { count, exclude_replies, include_rts, force_web_api }; + return { count, include_replies, include_rts, force_web_api, only_media }; }; export const excludeRetweet = function (tweets) { @@ -485,4 +503,9 @@ export const excludeRetweet = function (tweets) { return excluded; }; -export default { ProcessFeed, getAppClient, parseRouteParams, excludeRetweet }; +export const keepOnlyMedia = function (tweets) { + const excluded = tweets.filter((t) => t.extended_entities && t.extended_entities.media); + return excluded; +}; + +export default { ProcessFeed, getAppClient, parseRouteParams, excludeRetweet, keepOnlyMedia }; diff --git a/lib/routes/twreporter/category.ts b/lib/routes/twreporter/category.ts index 5bf0534576d156..16aee8e321bdf9 100644 --- a/lib/routes/twreporter/category.ts +++ b/lib/routes/twreporter/category.ts @@ -1,12 +1,12 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import fetch from './fetch-article'; export const route: Route = { path: '/category/:category', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/twreporter/category/world', parameters: { category: 'Category' }, features: { @@ -28,28 +28,73 @@ export const route: Route = { url: 'twreporter.org/', }; +// 发现其实是个开源项目 https://github.com/twreporter/go-api,所以我们能在以下两个文件找到相应的类目 ID,从 https://go-api.twreporter.org/v2/index_page 这里拿的话复杂度比较高而且少了侧栏的几个类目: +// https://github.com/twreporter/go-api/blob/master/internal/news/category_set.go +// https://github.com/twreporter/go-api/blob/master/internal/news/category.go +const CATEGORIES = { + world: { + name: '國際兩岸', + url_name: 'world', + category_id: '63206383207bf7c5f871622c', + }, + humanrights: { + name: '人權司法', + url_name: 'humanrights', + category_id: '63206383207bf7c5f8716234', + }, + politics_and_society: { + name: '政治社會', + url_name: 'politics-and-society', + category_id: '63206383207bf7c5f871623d', + }, + health: { + name: '醫療健康', + url_name: 'health', + category_id: '63206383207bf7c5f8716245', + }, + environment: { + name: '環境永續', + url_name: 'environment', + category_id: '63206383207bf7c5f871624d', + }, + econ: { + name: '經濟產業', + url_name: 'econ', + category_id: '63206383207bf7c5f8716254', + }, + culture: { + name: '文化生活', + url_name: 'culture', + category_id: '63206383207bf7c5f8716259', + }, + education: { + name: '教育校園', + url_name: 'education', + category_id: '63206383207bf7c5f8716260', + }, + podcast: { + name: 'Podcast', + url_name: 'podcast', + category_id: '63206383207bf7c5f8716266', + }, + opinion: { + name: '評論', + url_name: 'opinion', + category_id: '63206383207bf7c5f8716269', + }, + photos_section: { + name: '影像', + url_name: 'photography', + category_id: '574d028748fa171000c45d48', + }, +}; + async function handler(ctx) { const category = ctx.req.param('category'); - const url = `https://go-api.twreporter.org/v2/index_page`; - const res = await got(url).json(); - const list = res.data[category]; - - let name = list[0].category_set[0].category.name; - let categoryID = category; - switch (categoryID) { - case 'photos_section': - categoryID = 'photography'; - - break; - case 'politics_and_society': - categoryID = categoryID.replaceAll('_', '-'); - name = '政治社會'; - - break; - default: - break; - } - const home = `https://www.twreporter.org/categories/${categoryID}`; + const url = `https://go-api.twreporter.org/v2/posts?category_id=${CATEGORIES[category].category_id}`; + const home = `https://www.twreporter.org/categories/${CATEGORIES[category].url_name}`; + const res = await ofetch(url); + const list = res.data.records; const out = await Promise.all( list.map((item) => { @@ -64,7 +109,7 @@ async function handler(ctx) { ); return { - title: `報導者 | ${name}`, + title: `報導者 | ${CATEGORIES[category].name}`, link: home, item: out, }; diff --git a/lib/routes/twreporter/fetch-article.ts b/lib/routes/twreporter/fetch-article.ts index 035fbbf85effc5..28713322fe70f1 100644 --- a/lib/routes/twreporter/fetch-article.ts +++ b/lib/routes/twreporter/fetch-article.ts @@ -1,15 +1,15 @@ 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'; export default async function fetch(slug: string) { const url = `https://go-api.twreporter.org/v2/posts/${slug}?full=true`; - const res = await got(url); - const post = res.data.data; + const res = await ofetch(url); + const post = res.data; const time = post.published_date; // For `writers` @@ -33,9 +33,9 @@ export default async function fetch(slug: string) { authors += ';' + photographers; } - const bannerImage = post.og_image.resized_targets.desktop.url; + const bannerImage = post.hero_image.resized_targets.desktop.url; const caption = post.leading_image_description; - const bannerDescription = post.og_image.description; + const bannerDescription = post.hero_image.description; const ogDescription = post.og_description; const banner = art(path.join(__dirname, 'templates/image.art'), { image: bannerImage, description: bannerDescription, caption }); @@ -77,6 +77,12 @@ export default async function fetch(slug: string) { break; } + case 'quoteby': { + const quote = content[0]; + block = `<blockquote>${quote.quote}</blockquote><p>${quote.quoteBy}</p>`; + + break; + } default: block = `${content}<br>`; } diff --git a/lib/routes/twreporter/namespace.ts b/lib/routes/twreporter/namespace.ts index 0ba0ed845a0f59..90737c4ec6823d 100644 --- a/lib/routes/twreporter/namespace.ts +++ b/lib/routes/twreporter/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '報導者', url: 'twreporter.org', + lang: 'zh-TW', }; diff --git a/lib/routes/twreporter/newest.ts b/lib/routes/twreporter/newest.ts index a71354ba8de6fc..1f746b61a64e57 100644 --- a/lib/routes/twreporter/newest.ts +++ b/lib/routes/twreporter/newest.ts @@ -1,12 +1,12 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import fetch from './fetch-article'; export const route: Route = { path: '/newest', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/twreporter/newest', parameters: {}, features: { @@ -31,7 +31,7 @@ export const route: Route = { async function handler() { const base = `https://www.twreporter.org`; const url = `https://go-api.twreporter.org/v2/index_page`; - const res = await got(url).json(); + const res = await ofetch(url); const list = res.data.latest_section; const out = await Promise.all( list.map((item) => { diff --git a/lib/routes/txks/namespace.ts b/lib/routes/txks/namespace.ts new file mode 100644 index 00000000000000..39ff09c37ef7b8 --- /dev/null +++ b/lib/routes/txks/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '全国通信专业技术人员职业水平考试', + url: 'www.txks.org.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/txks/news.ts b/lib/routes/txks/news.ts new file mode 100644 index 00000000000000..f267d4a7733142 --- /dev/null +++ b/lib/routes/txks/news.ts @@ -0,0 +1,107 @@ +import type { DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; + +const BASE_URL = 'https://www.txks.org.cn/index/work.html'; + +const removeFontPresetting = (html: string = ''): string => { + const $ = load(html); + $('[style]').each((_, element) => { + const style = $(element).attr('style') || ''; + const cleanedStyle = style.replaceAll(/font-family:[^;]*;?/gi, '').trim(); + $(element).attr('style', cleanedStyle || null); + }); + $('style').each((_, styleElement) => { + const cssText = $(styleElement).html() || ''; + const cleanedCssText = cssText.replaceAll(/font-family:[^;]*;?/gi, ''); + $(styleElement).html(cleanedCssText); + }); + + return $.html(); +}; + +const handler: Route['handler'] = async () => { + // Fetch the index page + const { data: listResponse } = await got(BASE_URL); + const $ = load(listResponse); + + // Select all list items containing news information + const ITEM_SELECTOR = 'ul[class*="newsList"] > li'; + const listItems = $(ITEM_SELECTOR); + + // Map through each list item to extract details + const contentLinkList = listItems.toArray().map((element) => { + const date = $(element).find('label.time').text().trim().slice(1, -1); + const title = $(element).find('a').attr('title')!; + const link = $(element).find('a').attr('href')!; + + const formattedDate = parseDate(date); + return { + date: formattedDate, + title, + link, + }; + }); + + return { + title: '全国通信专业技术人员职业水平考试', + description: '全国通信专业技术人员职业水平考试网站最新动态和消息推送', + link: BASE_URL, + image: 'https://www.txks.org.cn/asset/image/logo/logo.png', + item: (await Promise.all( + contentLinkList.map((item) => + cache.tryGet(item.link, async () => { + const CONTENT_SELECTOR = '#contentTxt'; + const { data: contentResponse } = await got(item.link); + const contentPage = load(contentResponse); + const content = removeFontPresetting(contentPage(CONTENT_SELECTOR).html() || ''); + return { + title: item.title, + pubDate: item.date, + link: item.link, + description: content, + category: ['study'], + guid: item.link, + id: item.link, + image: 'https://www.txks.org.cn/asset/image/logo/logo.png', + content, + updated: item.date, + language: 'zh-CN', + }; + }) + ) + )) as DataItem[], + allowEmpty: true, + language: 'zh-CN', + feedLink: 'https://rsshub.app/txks/news', + id: 'https://rsshub.app/txks/news', + }; +}; + +export const route: Route = { + path: '/news', + name: '通信考试动态', + description: '**注意:** 官方网站限制了国外网络请求,可能需要通过部署在中国大陆内的 RSSHub 实例访问。', + maintainers: ['PrinOrange'], + handler, + categories: ['study'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + supportRadar: true, + }, + radar: [ + { + title: '全国通信专业技术人员职业水平考试动态', + source: ['www.txks.org.cn/index/work', 'www.txks.org.cn'], + target: `/news`, + }, + ], + example: '/txks/news', +}; diff --git a/lib/routes/txrjy/fornumtopic.ts b/lib/routes/txrjy/fornumtopic.ts index f384ff1842f54f..e4e186930d2a2a 100644 --- a/lib/routes/txrjy/fornumtopic.ts +++ b/lib/routes/txrjy/fornumtopic.ts @@ -30,8 +30,8 @@ export const route: Route = { maintainers: ['Fatpandac'], handler, description: `| 最新 500 个主题帖 | 最新 500 个回复帖 | 最新精华帖 | 最新精华帖 | 一周热帖 | 本月热帖 | - | :---------------: | :---------------: | :--------: | :--------: | :------: | :------: | - | 1 | 2 | 3 | 4 | 5 | 6 |`, +| :---------------: | :---------------: | :--------: | :--------: | :------: | :------: | +| 1 | 2 | 3 | 4 | 5 | 6 |`, }; async function handler(ctx) { diff --git a/lib/routes/txrjy/namespace.ts b/lib/routes/txrjy/namespace.ts index e623d125a2b4c7..8344778afd1af5 100644 --- a/lib/routes/txrjy/namespace.ts +++ b/lib/routes/txrjy/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '通信人家园', url: 'txrjy.com', + lang: 'zh-CN', }; diff --git a/lib/routes/tynu/namespace.ts b/lib/routes/tynu/namespace.ts index 9066fc6d502915..644ee14062c6e9 100644 --- a/lib/routes/tynu/namespace.ts +++ b/lib/routes/tynu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '太原师范学院', url: 'tynu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/typora/namespace.ts b/lib/routes/typora/namespace.ts index 70c459a4911c8f..23d279674f465c 100644 --- a/lib/routes/typora/namespace.ts +++ b/lib/routes/typora/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Typora', url: 'typora.io', + lang: 'en', }; diff --git a/lib/routes/typst/namespace.ts b/lib/routes/typst/namespace.ts new file mode 100644 index 00000000000000..a24e78df132930 --- /dev/null +++ b/lib/routes/typst/namespace.ts @@ -0,0 +1,14 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Typst', + url: 'typst.com', + description: ` +Compose papers faster: Focus on your text and let Typst take care of layout and formatting. +`, + + zh: { + name: 'Typst', + }, + lang: 'en', +}; diff --git a/lib/routes/typst/universe.ts b/lib/routes/typst/universe.ts new file mode 100644 index 00000000000000..7e57263a0fabbb --- /dev/null +++ b/lib/routes/typst/universe.ts @@ -0,0 +1,108 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import markdownit from 'markdown-it'; +import vm from 'node:vm'; + +interface Package { + name: string; + version: string; + entrypoint: string; + authors: Array<string>; + license: string; + description: string; + repository: string; + keywords: Array<string>; + compiler: string; + exclude: Array<string>; + size: number; + readme: string; + updatedAt: number; + releasedAt: number; +} + +interface Context { + an: { exports: Array<Package> }; +} + +const GITHUBRAW_BASE = 'https://raw.githubusercontent.com'; +const PKG_GITHUB_BASE = `${GITHUBRAW_BASE}/typst/packages/main/packages/preview`; + +function fixImageSrc(src: string, env: Package) { + if (src.includes('://')) { + if (src.startsWith('https://typst.app/universe/package')) { + src = src.replaceAll('https://typst.app/universe/package', `${PKG_GITHUB_BASE}/${env.name}/${env.version}`); + } else if (src.startsWith('https://github.com/') && src.match(/\.(jpeg|jpg|gif|png|bmp|webp)$/gi)?.length) { + src = src.replace('https://github.com/', `${GITHUBRAW_BASE}/`); + } + } else { + const suffix = src.startsWith('/') ? '' : '/'; + const package_base = `${PKG_GITHUB_BASE}/${env.name}/${env.version}${suffix}`; + const url = new URL(src, package_base); + src = url.toString(); + } + return src; +} + +export const route: Route = { + path: '/universe', + categories: ['program-update'], + example: '/typst/universe', + radar: [ + { + source: ['typst.app/universe'], + target: '/universe', + }, + ], + name: 'Universe', + maintainers: ['HPDell'], + handler: async () => { + const targetUrl = 'https://typst.app/universe/search?kind=packages%2Ctemplates&packages=last-updated'; + const page = await ofetch(targetUrl); + const $ = load(page); + const script = $('script') + .toArray() + .map((item) => item.attribs.src) + .find((item) => item && item.startsWith('/scripts/universe-search')); + const data: string = await ofetch(`https://typst.app${script}`, { + parseResponse: (txt) => txt, + }); + let packages = data.match(/(an.exports=[\S\s]+);var ([$A-Z_a-z][\w$]*)=new Intl.Collator/)?.[1]; + if (packages) { + packages = packages.slice(0, -2); + const context: Context = { an: { exports: [] } }; + vm.createContext(context); + vm.runInContext(packages, context, { + displayErrors: true, + }); + const md = markdownit('commonmark'); + const items = context.an.exports.sort((a, b) => a.updatedAt - b.updatedAt); + const groups = new Map(items.map((it) => [it.name, it])); + const pkgs = [...groups.values()].map((item) => { + const $ = load(md.render(item.readme)); + $('img').each((i, el) => { + const src = el.attribs.src; + el.attribs.src = fixImageSrc(src, item); + }); + return { + title: `${item.name} (${item.version}) | ${item.description}`, + link: `https://typst.app/universe/package/${item.name}`, + description: $.html(), + pubDate: parseDate(item.updatedAt, 'X'), + }; + }); + return { + title: 'Typst universe', + link: targetUrl, + item: pkgs, + }; + } else { + return { + title: 'Typst universe', + link: targetUrl, + item: [], + }; + } + }, +}; diff --git a/lib/routes/u3c3/namespace.ts b/lib/routes/u3c3/namespace.ts index 1147a73bc18e17..690e7a898f38fb 100644 --- a/lib/routes/u3c3/namespace.ts +++ b/lib/routes/u3c3/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'U3C3', url: 'u3c3.com', + lang: 'zh-CN', }; diff --git a/lib/routes/u9a9/namespace.ts b/lib/routes/u9a9/namespace.ts index e0786c3a8de6dd..197fb6c9b3d318 100644 --- a/lib/routes/u9a9/namespace.ts +++ b/lib/routes/u9a9/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'U9A9', url: 'u9a9.com', + lang: 'zh-CN', }; diff --git a/lib/routes/uber/blog.ts b/lib/routes/uber/blog.ts index 284d88097f3488..d282628514e00c 100644 --- a/lib/routes/uber/blog.ts +++ b/lib/routes/uber/blog.ts @@ -21,7 +21,7 @@ export const route: Route = { }, radar: [ { - source: ['www.uber.com/blog/pittsburgh/engineering'], + source: ['www.uber.com/:language/blog/engineering', 'www.uber.com/:language/blog'], target: '/blog', }, ], diff --git a/lib/routes/uber/namespace.ts b/lib/routes/uber/namespace.ts index bdba13c06ba199..6c58181d7a3a10 100644 --- a/lib/routes/uber/namespace.ts +++ b/lib/routes/uber/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Uber 优步', + name: 'Uber', url: 'www.uber.com', + lang: 'en', }; diff --git a/lib/routes/ucas/index.ts b/lib/routes/ucas/index.ts index 96ee2e13a665bc..38556a613e5bfb 100644 --- a/lib/routes/ucas/index.ts +++ b/lib/routes/ucas/index.ts @@ -37,8 +37,8 @@ export const route: Route = { maintainers: ['Fatpandac'], handler, description: `| 招聘类型 | 博士后 | 课题项目聘用 | 管理支撑人才 | 教学科研人才 | - | :------: | :----: | :----------: | :----------: | :----------: | - | 参数 | bsh | ktxmpy | glzcrc | jxkyrc |`, +| :------: | :----: | :----------: | :----------: | :----------: | +| 参数 | bsh | ktxmpy | glzcrc | jxkyrc |`, }; async function handler(ctx) { diff --git a/lib/routes/ucas/namespace.ts b/lib/routes/ucas/namespace.ts index 2df1600d7ab5f7..74a4c7646678db 100644 --- a/lib/routes/ucas/namespace.ts +++ b/lib/routes/ucas/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国科学院大学', url: 'ai.ucas.ac.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/uchicago/current.ts b/lib/routes/uchicago/current.ts index b27ddd679ff377..3d7516d38353b2 100644 --- a/lib/routes/uchicago/current.ts +++ b/lib/routes/uchicago/current.ts @@ -46,7 +46,7 @@ async function handler(ctx) { }); const response = await page.evaluate(() => document.documentElement.innerHTML); const cookies = await getCookies(page); - page.close(); + await page.close(); const $ = load(response); const list = $('.issue-item__title') @@ -68,7 +68,7 @@ async function handler(ctx) { referer: link, }); const response = await page.evaluate(() => document.documentElement.innerHTML); - page.close(); + await page.close(); const $ = load(response); @@ -94,7 +94,7 @@ async function handler(ctx) { ) ); - browser.close(); + await browser.close(); return { title: $('head title').text(), diff --git a/lib/routes/uchicago/namespace.ts b/lib/routes/uchicago/namespace.ts index 1a4ed461be579c..65cfad67a143d4 100644 --- a/lib/routes/uchicago/namespace.ts +++ b/lib/routes/uchicago/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The University of Chicago Press: Journals', url: 'journals.uchicago.edu', + lang: 'en', }; diff --git a/lib/routes/udn/breaking-news.ts b/lib/routes/udn/breaking-news.ts index 9fa1080695a538..04a007e5f5b991 100644 --- a/lib/routes/udn/breaking-news.ts +++ b/lib/routes/udn/breaking-news.ts @@ -32,8 +32,8 @@ export const route: Route = { maintainers: ['miles170'], handler, description: `| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 99 | - | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | - | 精選 | 要聞 | 社會 | 地方 | 兩岸 | 國際 | 財經 | 運動 | 娛樂 | 生活 | 股市 | 文教 | 數位 | 不分類 |`, +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | +| 精選 | 要聞 | 社會 | 地方 | 兩岸 | 國際 | 財經 | 運動 | 娛樂 | 生活 | 股市 | 文教 | 數位 | 不分類 |`, }; async function handler(ctx) { @@ -85,6 +85,15 @@ async function handler(ctx) { description += body.html(); } + if (data.publisher.name === '轉角國際 udn Global') { + // 轉角24小時 + description = $('.story_body_content') + .html() + .split(/<!--\d+?-->/g) + .slice(1, -1) + .join(''); + } + return { title: item.title, author: data.author.name, diff --git a/lib/routes/udn/global/index.ts b/lib/routes/udn/global/index.ts index 436a2e27d03af6..48401f8b9aadcc 100644 --- a/lib/routes/udn/global/index.ts +++ b/lib/routes/udn/global/index.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 首頁 | 最新文章 | 熱門文章 | - | ---- | -------- | -------- | - | | new | hot |`, +| ---- | -------- | -------- | +| | new | hot |`, }; async function handler(ctx) { diff --git a/lib/routes/udn/global/tag.ts b/lib/routes/udn/global/tag.ts index 679a59ef5d5dbc..31de80e4a1251e 100644 --- a/lib/routes/udn/global/tag.ts +++ b/lib/routes/udn/global/tag.ts @@ -26,7 +26,7 @@ export const route: Route = { maintainers: ['emdoe', 'nczitzk'], handler, description: `| 過去 24 小時 | 鏡頭背後 | 深度專欄 | 重磅廣播 | - | ------------ | -------- | -------- | -------- |`, +| ------------ | -------- | -------- | -------- |`, }; async function handler(ctx) { diff --git a/lib/routes/udn/namespace.ts b/lib/routes/udn/namespace.ts index ca9b4cd9f022e6..0bc19494a1561b 100644 --- a/lib/routes/udn/namespace.ts +++ b/lib/routes/udn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '聯合新聞網', url: 'udn.com', + lang: 'zh-TW', }; diff --git a/lib/routes/uestc/bbs.ts b/lib/routes/uestc/bbs.ts new file mode 100644 index 00000000000000..da8f590f7d2d14 --- /dev/null +++ b/lib/routes/uestc/bbs.ts @@ -0,0 +1,97 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 +import { parseDate } from '@/utils/parse-date'; // 解析日期的工具函数 +import timezone from '@/utils/timezone'; +import { config } from '@/config'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; + +export const route: Route = { + path: '/bbs/:types?', + name: '清水河畔', + maintainers: ['huyyi'], + categories: ['university'], + url: 'bbs.uestc.edu.cn', + example: '/uestc/bbs/newthread', + parameters: { types: '选择内容类型(多选`,`分割),可选值:[newreply,newthread,digest,life,hotlist]。默认为所有。' }, + features: { + requireConfig: [ + { + name: 'UESTC_BBS_COOKIE', + optional: false, + description: '河畔的cookie', + }, + { + name: 'UESTC_BBS_AUTH_KEY', + optional: false, + description: '河畔Header中的authorization字段', + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + description: ` +::: tip +仅支持自建,您需要设置以下配置才能正常使用: +- 河畔cookie: \`UESTC_BBS_COOKIE\` +- Header中的授权字段: \`UESTC_BBS_AUTH_KEY\` +::: +`, + radar: [ + { + source: ['bbs.uestc.edu.cn/*'], + target: '/bbs/newthread', + }, + ], + handler: async (ctx) => { + const { bbsCookie, bbsAuthStr } = config.uestc; + if (!bbsCookie || !bbsAuthStr) { + throw new ConfigNotFoundError('未配置 Cookie 或 Authorization。请检查配置。'); + } + const { types = 'newreply,newthread,digest,life,hotlist' } = ctx.req.param(); + const data = await ofetch(`https://bbs.uestc.edu.cn/star/api/v1/index?top_list=${types}`, { + headers: { + Cookie: bbsCookie, + Referer: 'https://bbs.uestc.edu.cn/new', + authorization: bbsAuthStr, + }, + }); + const itemsRaw = Object.entries(data.data.top_list).flatMap(([label, items]) => items.map((item) => ({ ...item, label }))); + const items = await Promise.all( + itemsRaw.map((item) => + cache.tryGet(`https://bbs.uestc.edu.cn/forum.php?mod=viewthread&tid=${item.thread_id}`, async () => { + const response = await ofetch(`https://bbs.uestc.edu.cn/forum.php?mod=viewthread&tid=${item.thread_id}`, { + headers: { + Cookie: bbsCookie, + Referer: 'https://bbs.uestc.edu.cn', + authorization: bbsAuthStr, + }, + }); + const $ = load(response); + item.description = $('div#postlist').html(); + return { + title: item.subject, + link: `https://bbs.uestc.edu.cn/thread/${item.thread_id}`, + author: item.author, + category: item.label, + img: item.icon, + pubDate: timezone(parseDate(item.dateline), +8), + description: item.description, + }; + }) + ) + ); + return { + // 源标题 + title: '清水河畔', + // 源链接 + link: 'https://bbs.uestc.edu.cn/new', + // 源文章 + item: items, + }; + }, +}; diff --git a/lib/routes/uestc/cqe.ts b/lib/routes/uestc/cqe.ts index 9d3db276117b4f..adb9265e95bb41 100644 --- a/lib/routes/uestc/cqe.ts +++ b/lib/routes/uestc/cqe.ts @@ -40,8 +40,8 @@ export const route: Route = { handler, url: 'cqe.uestc.edu.cn/', description: `| 活动预告 | 通知公告 | - | -------- | -------- | - | hdyg | tzgg |`, +| -------- | -------- | +| hdyg | tzgg |`, }; async function handler(ctx) { diff --git a/lib/routes/uestc/gr.ts b/lib/routes/uestc/gr.ts index e7a635683e6b00..8ebe51475ac669 100644 --- a/lib/routes/uestc/gr.ts +++ b/lib/routes/uestc/gr.ts @@ -1,17 +1,38 @@ -import { Route } from '@/types'; +import { Data, DataItem, 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'; +import timezone from '@/utils/timezone'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import type { Context } from 'hono'; -const baseUrl = 'https://gr.uestc.edu.cn/tongzhi/'; -const baseIndexUrl = 'https://gr.uestc.edu.cn'; +const baseUrl = 'https://gr.uestc.edu.cn/'; +const detailUrl = 'https://gr.uestc.edu.cn/'; + +const dateTimeRegex = /(\d{4}-\d{2}-\d{2} \d{2}:\d{2})/; + +const typeUrlMap = { + important: 'tongzhi/', + teaching: 'tongzhi/119', + degree: 'tongzhi/129', + student: 'tongzhi/122', + practice: 'tongzhi/123', +}; + +const typeNameMap = { + important: '重要公告', + teaching: '教学管理', + degree: '学位管理', + student: '学生管理', + practice: '就业实践', +}; export const route: Route = { - path: '/gr', + path: '/gr/:type?', categories: ['university'], - example: '/uestc/gr', - parameters: {}, + example: '/uestc/gr/student', + parameters: { type: '默认为 `important`' }, features: { requireConfig: false, requirePuppeteer: false, @@ -29,46 +50,53 @@ export const route: Route = { maintainers: ['huyyi', 'mobyw'], handler, url: 'gr.uestc.edu.cn/', + description: `\ +| 重要公告 | 教学管理 | 学位管理 | 学生管理 | 就业实践 | +| --------- | -------- | -------- | -------- | -------- | +| important | teaching | degree | student | practice |`, }; -async function handler() { - const response = await got.get(baseIndexUrl); +async function handler(ctx: Context): Promise<Data> { + const type = ctx.req.param('type') || 'important'; + if (type in typeUrlMap === false) { + throw new InvalidParameterError('type not supported'); + } + const typeName = typeNameMap[type]; - const $ = load(response.data); + const indexContent = await ofetch(baseUrl + typeUrlMap[type]); - const items = []; - $('[href^="/tongzhi/"]').each((_, item) => { - items.push(baseIndexUrl + item.attribs.href); - }); + const $ = load(indexContent); + const entries = $('div.title').toArray(); + + const items = entries.map(async (entry) => { + const element = $(entry); + const newsTitle = element.find('a').text() ?? ''; + const newsLink = detailUrl + element.find('a').attr('href'); - const out = await Promise.all( - items.map(async (newsUrl) => { - const newsDetail = await cache.tryGet(newsUrl, async () => { - const result = await got.get(newsUrl); + const newsDetail = await cache.tryGet(newsLink, async () => { + const newsContent = await ofetch(newsLink); + const content = load(newsContent); - const $ = load(result.data); + const basicInfo = content('div.topic_detail_header').find('div.info').text(); + const match = dateTimeRegex.exec(basicInfo); - const title = '[' + $('.over').text() + '] ' + $('div.title').text(); - const author = $('.info').text().split('|')[1].trim().substring(3); - const date = parseDate($('.info').text().split('|')[0].trim().substring(4)); - const description = $('.content').html(); + return { + title: newsTitle, + link: newsLink, + pubDate: match ? timezone(parseDate(match[1]), +8) : null, + description: content('div.content').html(), + }; + }); + + return newsDetail; + }); - return { - title, - link: newsUrl, - author, - pubDate: date, - description, - }; - }); - return newsDetail; - }) - ); + const out = await Promise.all(items); return { - title: '研究生院通知', + title: `研究生院通知(${typeName})`, link: baseUrl, - description: '电子科技大学研究生院通知公告', - item: out, + description: `电子科技大学研究生院通知(${typeName})`, + item: out as DataItem[], }; } diff --git a/lib/routes/uestc/jwc.ts b/lib/routes/uestc/jwc.ts index 324e0f34c8c505..301a1e2573e58a 100644 --- a/lib/routes/uestc/jwc.ts +++ b/lib/routes/uestc/jwc.ts @@ -1,22 +1,33 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; +import { Data, DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; import InvalidParameterError from '@/errors/types/invalid-parameter'; - -const dateRegex = /(20\d{2})\/(\d{2})\/(\d{2})/; +import type { Context } from 'hono'; const baseUrl = 'https://www.jwc.uestc.edu.cn/'; const detailUrl = 'https://www.jwc.uestc.edu.cn/info/'; -const map = { +const dateTimeRegex = /(\d{4}-\d{2}-\d{2} \d{2}:\d{2})/; + +const typeUrlMap = { important: 'hard/?page=1', student: 'list/256/?page=1', teacher: 'list/255/?page=1', - teach: 'list/40/?page=1', + teaching: 'list/40/?page=1', office: 'list/ff80808160bcf79c0160c010a8d20020/?page=1', }; +const typeNameMap = { + important: '重要公告', + student: '学生事务公告', + teacher: '教师事务公告', + teaching: '教学新闻', + office: '办公室', +}; + export const route: Route = { path: '/jwc/:type?', categories: ['university'], @@ -32,51 +43,61 @@ export const route: Route = { }, radar: [ { - source: ['jwc.uestc.edu.cn/'], + source: ['www.jwc.uestc.edu.cn/'], target: '/jwc', }, ], name: '教务处', maintainers: ['achjqz', 'mobyw'], handler, - url: 'jwc.uestc.edu.cn/', - description: `| 重要公告 | 学生事务公告 | 教师事务公告 | 教学新闻 | 办公室 | - | --------- | ------------ | ------------ | -------- | ------ | - | important | student | teacher | teach | office |`, + url: 'www.jwc.uestc.edu.cn/', + description: `\ +| 重要公告 | 学生事务公告 | 教师事务公告 | 教学新闻 | 办公室 | +| --------- | ------------ | ------------ | -------- | ------ | +| important | student | teacher | teaching | office |`, }; -async function handler(ctx) { +async function handler(ctx: Context): Promise<Data> { const type = ctx.req.param('type') || 'important'; - const pageUrl = map[type]; - if (!pageUrl) { + if (type in typeUrlMap === false) { throw new InvalidParameterError('type not supported'); } + const typeName = typeNameMap[type]; - const response = await got.get(baseUrl + pageUrl); + const indexContent = await ofetch(baseUrl + typeUrlMap[type]); - const $ = load(response.data); + const $ = load(indexContent); + const entries = $('div.textAreo.clearfix').toArray(); - const items = $('div.textAreo.clearfix'); + const items = entries.map(async (entry) => { + const element = $(entry); + const newsTitle = element.find('a').attr('title') ?? ''; + const newsLink = detailUrl + element.find('a').attr('newsid'); - const out = $(items) - .map((_, item) => { - item = $(item); - const newsTitle = item.find('a').attr('title'); - const newsLink = detailUrl + item.find('a').attr('newsid'); - const newsDate = parseDate(item.find('i').text().replace(dateRegex, '$1-$2-$3')); + const newsDetail = await cache.tryGet(newsLink, async () => { + const newsContent = await ofetch(newsLink); + const content = load(newsContent); + + const basicInfo = content('div.detail_header').find('div.item').text(); + const match = dateTimeRegex.exec(basicInfo); return { title: newsTitle, link: newsLink, - pubDate: newsDate, + pubDate: match ? timezone(parseDate(match[1]), +8) : null, + description: content('div.NewText').html(), }; - }) - .get(); + }); + + return newsDetail; + }); + + const out = await Promise.all(items); return { - title: '教务处通知', + title: `教务处通知(${typeName})`, link: baseUrl, - description: '电子科技大学教务处通知', - item: out, + description: `电子科技大学教务处通知(${typeName})`, + item: out as DataItem[], }; } diff --git a/lib/routes/uestc/namespace.ts b/lib/routes/uestc/namespace.ts index 0522f690b3f841..41e2c026626697 100644 --- a/lib/routes/uestc/namespace.ts +++ b/lib/routes/uestc/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '电子科技大学', - url: 'gr.uestc.edu.cn', + url: 'www.uestc.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/uestc/news.ts b/lib/routes/uestc/news.ts index a602a1b1409fec..472c3cc1ab562d 100644 --- a/lib/routes/uestc/news.ts +++ b/lib/routes/uestc/news.ts @@ -38,8 +38,8 @@ export const route: Route = { handler, url: 'news.uestc.edu.cn/', description: `| 学术 | 文化 | 公告 | 校内通知 | - | ------- | ------- | ------------ | ------------ | - | academy | culture | announcement | notification |`, +| ------- | ------- | ------------ | ------------ | +| academy | culture | announcement | notification |`, }; async function handler(ctx) { diff --git a/lib/routes/uestc/sise.ts b/lib/routes/uestc/sise.ts index de987645646bc8..f20454edfb6b62 100644 --- a/lib/routes/uestc/sise.ts +++ b/lib/routes/uestc/sise.ts @@ -55,8 +55,8 @@ export const route: Route = { handler, url: 'sise.uestc.edu.cn/', description: `| 最新 | 院办 | 学生科 | 教务科 | 研管科 | 组织 | 人事 | 实践教育中心 | Int'I | - | ---- | ---- | ------ | ------ | ------ | ---- | ---- | ------------ | ----- | - | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |`, +| ---- | ---- | ------ | ------ | ------ | ---- | ---- | ------------ | ----- | +| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |`, }; async function handler(ctx) { diff --git a/lib/routes/uibe/hr.ts b/lib/routes/uibe/hr.ts index 4843c8253f4cb4..69ed2a5da4ef0e 100644 --- a/lib/routes/uibe/hr.ts +++ b/lib/routes/uibe/hr.ts @@ -25,11 +25,11 @@ export const route: Route = { name: '人力资源处', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 如 [通知公告](http://hr.uibe.edu.cn/tzgg) 的 URL 为 \`http://hr.uibe.edu.cn/tzgg\`,其路由为 [\`/uibe/hr/tzgg\`](https://rsshub.app/uibe/hr/tzgg) 如 [教师招聘](http://hr.uibe.edu.cn/jszp) 中的 [招聘信息](http://hr.uibe.edu.cn/jszp/zpxx) 的 URL 为 \`http://hr.uibe.edu.cn/jszp/zpxx\`,其路由为 [\`/uibe/hr/jszp/zpxx\`](https://rsshub.app/uibe/jszp/zpxx) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/uibe/namespace.ts b/lib/routes/uibe/namespace.ts index 38ecd569ab07ab..663e210c34b7e2 100644 --- a/lib/routes/uibe/namespace.ts +++ b/lib/routes/uibe/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '对外经济贸易大学', url: 'hr.uibe.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ulapia/index.ts b/lib/routes/ulapia/index.ts index 0182e6af6d0fe6..b41165a0e3461a 100644 --- a/lib/routes/ulapia/index.ts +++ b/lib/routes/ulapia/index.ts @@ -40,8 +40,8 @@ export const route: Route = { maintainers: ['Fatpandac'], handler, description: `| 个股研报 | 行业研报 | 策略研报 | 宏观研报 | 新股研报 | 券商晨报(今日晨报) | - | :-------------: | :----------------: | :----------------: | :-------------: | :-----------: | :------------------: | - | stock\_research | industry\_research | strategy\_research | macro\_research | ipo\_research | brokerage\_news |`, +| :-------------: | :----------------: | :----------------: | :-------------: | :-----------: | :------------------: | +| stock\_research | industry\_research | strategy\_research | macro\_research | ipo\_research | brokerage\_news |`, }; async function handler(ctx) { diff --git a/lib/routes/ulapia/namespace.ts b/lib/routes/ulapia/namespace.ts index 4054ab81ab20c1..02371700e2d296 100644 --- a/lib/routes/ulapia/namespace.ts +++ b/lib/routes/ulapia/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '乌拉邦', url: 'www.ulapia.com', + lang: 'zh-CN', }; diff --git a/lib/routes/unipd/ilbolive/news.ts b/lib/routes/unipd/ilbolive/news.ts new file mode 100644 index 00000000000000..32fd5ed3313fe2 --- /dev/null +++ b/lib/routes/unipd/ilbolive/news.ts @@ -0,0 +1,104 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +export const route: Route = { + path: '/ilbolive/news', + name: 'Il Bo Live - News', + url: 'ilbolive.unipd.it/it/news', + maintainers: ['Gexi0619'], + example: '/unipd/ilbolive/news', + parameters: {}, + description: 'Il Bo Live - News', + categories: ['university'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['ilbolive.unipd.it/it/news'], + target: '/ilbolive/news', + }, + ], + handler, +}; + +async function handler() { + const baseUrl = 'https://ilbolive.unipd.it'; + const homeUrl = `${baseUrl}/it/news`; + + const response = await got(homeUrl); + const $ = load(response.data); + + const items = $('#list-nodes .col.-s-6') + .toArray() + .map((el) => { + const item = $(el); + const title = item.find('.title a').text().trim(); + const href = item.find('.title a').attr('href'); + const link = baseUrl + href; + const category = item.find('.category').text().trim(); + const image = item.find('.photo img').attr('src'); + const imageUrl = baseUrl + image; + + return { + title, + link, + category, + enclosure_url: imageUrl, + enclosure_type: 'image/jpeg', + }; + }); + + const finalItems = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got(item.link); + const $ = load(detailResponse.data); + + const article = $('article.post-generic'); + + // Picture + article.find('img').each((_, el) => { + const img = $(el); + const src = img.attr('src'); + if (src && src.startsWith('/')) { + img.attr('src', baseUrl + src); + } + img.attr('style', 'max-width: 100%; height: auto;'); + }); + + const datetime = article.find('time.date').attr('datetime'); + const pubDate = datetime ? timezone(parseDate(datetime), 0) : undefined; + + const author = article.find('.author a').text().trim(); + + // Delete header + article.find('.header').remove(); + + return { + ...item, + description: article.html() ?? '', + pubDate, + author, + }; + }) + ) + ); + + return { + title: 'Il Bo Live - News', + link: homeUrl, + item: finalItems, + language: 'it', + }; +} diff --git a/lib/routes/unipd/namespace.ts b/lib/routes/unipd/namespace.ts new file mode 100644 index 00000000000000..51251e3efa0865 --- /dev/null +++ b/lib/routes/unipd/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Università di Padova', + url: 'unipd.it', + lang: 'it', +}; diff --git a/lib/routes/uniqlo/namespace.ts b/lib/routes/uniqlo/namespace.ts index 12ff69ce152e5a..b2b3a1519cb61a 100644 --- a/lib/routes/uniqlo/namespace.ts +++ b/lib/routes/uniqlo/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Uniqlo', url: 'www.uniqlo.com', + lang: 'en', }; diff --git a/lib/routes/unraid/namespace.ts b/lib/routes/unraid/namespace.ts index 4170cac12105d2..a8a1bf15694673 100644 --- a/lib/routes/unraid/namespace.ts +++ b/lib/routes/unraid/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Unraid', url: 'unraid.net', + lang: 'en', }; diff --git a/lib/routes/unusualwhales/namespace.ts b/lib/routes/unusualwhales/namespace.ts index e18a57355bd6db..779f315bc8574b 100644 --- a/lib/routes/unusualwhales/namespace.ts +++ b/lib/routes/unusualwhales/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Unusual Whales', url: 'unusualwhales.com', + lang: 'en', }; diff --git a/lib/routes/upc/jsj.ts b/lib/routes/upc/jsj.ts index 44ea81ee06dc5e..4092d37323635e 100644 --- a/lib/routes/upc/jsj.ts +++ b/lib/routes/upc/jsj.ts @@ -43,8 +43,8 @@ export const route: Route = { maintainers: ['Veagau'], handler, description: `| 学院新闻 | 学术关注 | 学工动态 | 通知公告 | - | -------- | -------- | -------- | -------- | - | news | scholar | states | notice |`, +| -------- | -------- | -------- | -------- | +| news | scholar | states | notice |`, }; async function handler(ctx) { diff --git a/lib/routes/upc/jwc.ts b/lib/routes/upc/jwc.ts index e96772d2551a90..65f35b6090af5b 100644 --- a/lib/routes/upc/jwc.ts +++ b/lib/routes/upc/jwc.ts @@ -114,8 +114,8 @@ export const route: Route = { name: '教务处通知公告', maintainers: ['sddzhyc'], description: `| 所有通知 | 教学·运行 | 学业·学籍 | 教学·研究 | 课程·教材 | 实践·教学 | 创新·创业 | 语言·文字 | 继续·教育 | 本科·招生 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | tzgg | 18519 | 18520 | 18521 | 18522 | 18523 | 18524 | yywwz | jxwjy | bkwzs |`, +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| tzgg | 18519 | 18520 | 18521 | 18522 | 18523 | 18524 | yywwz | jxwjy | bkwzs |`, url: 'jwc.upc.edu.cn/tzgg/list.htm', handler, }; diff --git a/lib/routes/upc/main.ts b/lib/routes/upc/main.ts index 8e5d387d190451..b308fee8d284a3 100644 --- a/lib/routes/upc/main.ts +++ b/lib/routes/upc/main.ts @@ -34,8 +34,8 @@ export const route: Route = { maintainers: ['Veagau'], handler, description: `| 通知公告 | 学术动态 | - | -------- | -------- | - | notice | scholar |`, +| -------- | -------- | +| notice | scholar |`, }; async function handler(ctx) { diff --git a/lib/routes/upc/namespace.ts b/lib/routes/upc/namespace.ts index 674da491174975..b4664f5f1c562e 100644 --- a/lib/routes/upc/namespace.ts +++ b/lib/routes/upc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国石油大学(华东)', url: 'computer.upc.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ups/namespace.ts b/lib/routes/ups/namespace.ts new file mode 100644 index 00000000000000..21b8d8adef2ada --- /dev/null +++ b/lib/routes/ups/namespace.ts @@ -0,0 +1,12 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'UPS', + url: 'ups.com', + description: 'United Parcel Service (UPS) updates, news, and tracking RSS feeds.', + + zh: { + name: 'UPS(联合包裹服务公司)', + description: '联合包裹服务公司(UPS)的更新、新闻和追踪 RSS 源。', + }, +}; diff --git a/lib/routes/ups/track.ts b/lib/routes/ups/track.ts new file mode 100644 index 00000000000000..d04352d6fe674d --- /dev/null +++ b/lib/routes/ups/track.ts @@ -0,0 +1,114 @@ +import { Route, DataItem } from '@/types'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import puppeteer from '@/utils/puppeteer'; + +export const route: Route = { + path: '/track/:trackingNumber', + categories: ['other'], + example: '/ups/track/1Z78R6790470567520', + parameters: { trackingNumber: 'The UPS tracking number (e.g., 1Z78R6790470567520).' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Tracking', + maintainers: ['Aquabet'], + handler, +}; + +async function handler(ctx) { + const { trackingNumber } = ctx.req.param(); + const url = `https://www.ups.com/track?loc=en_US&tracknum=${trackingNumber}`; + + const browser = await puppeteer(); + const page = await browser.newPage(); + + await page.setRequestInterception(true); + + // skip loading images, stylesheets, and fonts + page.on('request', (request) => { + if (['image', 'stylesheet', 'font', 'ping', 'fetch'].includes(request.resourceType())) { + request.abort(); + } else { + request.continue(); + } + }); + + await page.goto(url, { waitUntil: 'domcontentloaded' }); + + const viewDetailsButton = '#st_App_View_Details'; + try { + await page.waitForSelector(viewDetailsButton); + await page.click(viewDetailsButton); + } catch { + return { + title: `UPS Tracking - ${trackingNumber}`, + link: url, + item: [], + }; + } + + await page.waitForSelector('tr[id^="stApp_activitydetails_row"]'); + + const content = await page.content(); + await browser.close(); + + const $ = load(content); + + const rows = $('tr[id^="stApp_activitydetails_row"]'); + + const items: DataItem[] = rows.toArray().map((el, i) => { + const dateTimeRaw = $(el).find(`#stApp_activitiesdateTime${i}`).text() || 'Not Provided'; + + const dateTimeStr = dateTimeRaw + .trim() + .replace(/(\d{1,}\/\d{1,}\/\d{4})(\d{1,}:\d{1,}\s[AP]\.?M\.?)/, '$1 $2') + .replaceAll('P.M.', 'PM') + .replaceAll('A.M.', 'AM'); + + const pubDate = parseDate(dateTimeStr); + + const activityCellText = $(el) + .find(`#stApp_milestoneActivityLocation${i}`) + .text() + .trim() + .replaceAll(/\s*\n+\s*/g, '\n'); + + const lines = activityCellText + .split('\n') + .map((l) => l.trim()) + .filter(Boolean); + + // Situation 0: There is text within the strong element + // Example: ["Delivered", "DELIVERED", "REDMOND, WA, United States"] + // Situation 1: strong is empty => the first line in lines is the status + // Example: ["Departed from Facility", "Seattle, WA, United States"] + const status = lines[0]; + const location = lines.at(-1) || ''; + + const item: DataItem = { + title: status, + link: url, + guid: `${trackingNumber}-${i}`, + description: ` + Status: ${status} <br> + Location: ${location} <br> + Date and Time: ${dateTimeStr} + `, + pubDate, + }; + + return item; + }); + + return { + title: `UPS Tracking - ${trackingNumber}`, + link: url, + item: items, + }; +} diff --git a/lib/routes/uptimerobot/namespace.ts b/lib/routes/uptimerobot/namespace.ts index 5aa2a76983ba5f..f5ff2c75c162a1 100644 --- a/lib/routes/uptimerobot/namespace.ts +++ b/lib/routes/uptimerobot/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Uptime Robot', url: 'rss.uptimerobot.com', + lang: 'en', }; diff --git a/lib/routes/uptimerobot/rss.ts b/lib/routes/uptimerobot/rss.ts index 46f174f5d74b49..65bd97d94dd0c6 100644 --- a/lib/routes/uptimerobot/rss.ts +++ b/lib/routes/uptimerobot/rss.ts @@ -81,8 +81,8 @@ export const route: Route = { maintainers: ['Rongronggg9'], handler, description: `| Key | Description | Accepts | Defaults to | - | ------ | ------------------------------------------------------------------------ | -------------- | ----------- | - | showID | Show monitor ID (disabling it will also disable link for each RSS entry) | 0/1/true/false | true |`, +| ------ | ------------------------------------------------------------------------ | -------------- | ----------- | +| showID | Show monitor ID (disabling it will also disable link for each RSS entry) | 0/1/true/false | true |`, }; async function handler(ctx) { diff --git a/lib/routes/uraaka-joshi/namespace.ts b/lib/routes/uraaka-joshi/namespace.ts index 64cd2fc39b4daf..22e565603c769c 100644 --- a/lib/routes/uraaka-joshi/namespace.ts +++ b/lib/routes/uraaka-joshi/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '裏垢女子まとめ', url: 'uraaka-joshi.com', + lang: 'ja', }; diff --git a/lib/routes/uraaka-joshi/uraaka-joshi-user.ts b/lib/routes/uraaka-joshi/uraaka-joshi-user.ts index f21851e460b5eb..b675dd0b8be240 100644 --- a/lib/routes/uraaka-joshi/uraaka-joshi-user.ts +++ b/lib/routes/uraaka-joshi/uraaka-joshi-user.ts @@ -42,9 +42,9 @@ async function handler(ctx) { page.on('request', (request) => { request.resourceType() === 'document' || request.resourceType() === 'script' || request.resourceType() === 'fetch' ? request.continue() : request.abort(); }); - page.on('requestfinished', (request) => { + page.on('requestfinished', async (request) => { if (request.url() === link && request.response().status() === 403) { - page.close(); + await page.close(); } }); diff --git a/lib/routes/uraaka-joshi/uraaka-joshi.ts b/lib/routes/uraaka-joshi/uraaka-joshi.ts index f1f927fbb1c75f..008c17828fe2fb 100644 --- a/lib/routes/uraaka-joshi/uraaka-joshi.ts +++ b/lib/routes/uraaka-joshi/uraaka-joshi.ts @@ -28,9 +28,9 @@ async function handler() { page.on('request', (request) => { request.resourceType() === 'document' || request.resourceType() === 'script' || request.resourceType() === 'fetch' ? request.continue() : request.abort(); }); - page.on('requestfinished', (request) => { + page.on('requestfinished', async (request) => { if (request.url() === link && request.response().status() === 403) { - page.close(); + await page.close(); } }); @@ -57,49 +57,45 @@ async function handler() { return { title, link, - item: - list && - list - .map((index, item) => { - item = $(item); - - // remove event and styles - item.find('*').removeAttr('onclick'); - item.find('*').removeAttr('onerror'); - item.find('*').removeAttr('style'); - - // format account style - const account = item.find('.account-group-link-row'); - account.html(account.text()); - - // extract video tag from its player - item.find('.plyr--video').each((_, player) => { - player = $(player); - - const video = player.find('video'); - player.replaceWith(video); - const poster = video.attr('data-poster'); - video.attr('poster', 'https:' + poster); - - const source = video.find('source'); - const src = source.attr('src'); - source.attr('src', 'https:' + src); - }); - - // correct src of img tags - item.find('img').each((_, image) => { - const src = $(image).attr('data-src'); - $(image).attr('src', 'https:' + src); - }); - - return { - title: item.find('.account-group').text() + ` - ${title}`, - description: item.html(), - link: item.find('.account-group-link-row').attr('href'), - pubDate: parseDate(item.find('.profile-char').attr('datetime')), - guid: item.find('a.tap-image').attr('data-tweet-id') || item.find('video[class^="js-player-"]').attr('data-tweet-id') || parseDate(item.find('.profile-char').attr('datetime')).getTime(), - }; - }) - .get(), + item: list.toArray().map((item) => { + item = $(item); + + // remove event and styles + item.find('*').removeAttr('onclick'); + item.find('*').removeAttr('onerror'); + item.find('*').removeAttr('style'); + + // format account style + const account = item.find('.account-group-link-row'); + account.html(account.text()); + + // extract video tag from its player + item.find('.plyr--video').each((_, player) => { + player = $(player); + + const video = player.find('video'); + player.replaceWith(video); + const poster = video.attr('data-poster'); + video.attr('poster', 'https:' + poster); + + const source = video.find('source'); + const src = source.attr('src'); + source.attr('src', 'https:' + src); + }); + + // correct src of img tags + item.find('img').each((_, image) => { + const src = $(image).attr('data-src'); + $(image).attr('src', 'https:' + src); + }); + + return { + title: item.find('.account-group').text() + ` - ${title}`, + description: item.html(), + link: item.find('.account-group-link-row').attr('href'), + pubDate: parseDate(item.find('.profile-char').attr('datetime')), + guid: item.find('a.tap-image').attr('data-tweet-id') || item.find('video[class^="js-player-"]').attr('data-tweet-id') || parseDate(item.find('.profile-char').attr('datetime')).getTime(), + }; + }), }; } diff --git a/lib/routes/urbandictionary/namespace.ts b/lib/routes/urbandictionary/namespace.ts index abd6cae4aa38fa..11dfffcfceea1a 100644 --- a/lib/routes/urbandictionary/namespace.ts +++ b/lib/routes/urbandictionary/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Urban Dictionary', url: 'urbandictionary.com', + lang: 'en', }; diff --git a/lib/routes/usenix/namespace.ts b/lib/routes/usenix/namespace.ts index ab4fcb47e5a8a4..61f2d16203f03d 100644 --- a/lib/routes/usenix/namespace.ts +++ b/lib/routes/usenix/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'USENIX', url: 'usenix.org', + lang: 'en', }; diff --git a/lib/routes/usepanda/index.ts b/lib/routes/usepanda/index.ts index aad60e226f0a2e..3ef3a4a132e391 100644 --- a/lib/routes/usepanda/index.ts +++ b/lib/routes/usepanda/index.ts @@ -19,8 +19,8 @@ export const route: Route = { maintainers: ['lyrl'], handler, description: `| Channel | feedId | - | ------- | ------------------------ | - | Github | 5718e53e7a84fb1901e059cc |`, +| ------- | ------------------------ | +| Github | 5718e53e7a84fb1901e059cc |`, }; async function handler(ctx) { diff --git a/lib/routes/usepanda/namespace.ts b/lib/routes/usepanda/namespace.ts index 842d79ba559669..493cdc4fda4350 100644 --- a/lib/routes/usepanda/namespace.ts +++ b/lib/routes/usepanda/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Panda', url: 'usepanda.com', + lang: 'en', }; diff --git a/lib/routes/ustb/namespace.ts b/lib/routes/ustb/namespace.ts index 1bd487063b1200..33ebcba154b964 100644 --- a/lib/routes/ustb/namespace.ts +++ b/lib/routes/ustb/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京科技大学', url: 'gs.ustb.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ustb/tj/news.ts b/lib/routes/ustb/tj/news.ts index c71ef2e08711bd..294239e35898c3 100644 --- a/lib/routes/ustb/tj/news.ts +++ b/lib/routes/ustb/tj/news.ts @@ -48,8 +48,8 @@ export const route: Route = { maintainers: ['henbf'], handler, description: `| 全部 | 学院新闻 | 学术活动 | 城市建设学院 | 信息工程学院 | 经济学院 | 管理学院 | 材料系 | 机械工程系 | 护理系 | 法律系 | 外语系 | 艺术系 | - | ---- | -------- | -------- | ------------ | ------------ | -------- | -------- | ------ | ---------- | ------ | ------ | ------ | ------ | - | all | xyxw | xshhd | csjsxy | xxgcxy | jjx | glxy | clx | jxgcx | hlx | flx | wyx | ysx |`, +| ---- | -------- | -------- | ------------ | ------------ | -------- | -------- | ------ | ---------- | ------ | ------ | ------ | ------ | +| all | xyxw | xshhd | csjsxy | xxgcxy | jjx | glxy | clx | jxgcx | hlx | flx | wyx | ysx |`, }; async function handler(ctx) { diff --git a/lib/routes/ustb/yjsy/news.ts b/lib/routes/ustb/yjsy/news.ts index 9968f1645aceab..22e3a2aff5183a 100644 --- a/lib/routes/ustb/yjsy/news.ts +++ b/lib/routes/ustb/yjsy/news.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['DA1Y1'], handler, description: `| 北京科技大学研究生院 | 土木与资源工程学院 | 能源与环境工程学院 | 冶金与生态工程学院 | 材料科学与工程学院 | 机械工程学院 | 自动化学院 | 计算机与通信工程学院 | 数理学院 | 化学与生物工程学院 | 经济管理学院 | 文法学院 | 马克思主义学院 | 外国语学院 | 国家材料服役安全科学中心 | 新金属材料国家重点实验室 | 工程技术研究院 | 钢铁共性技术协同创新中心 | 钢铁冶金新技术国家重点实验室 | 新材料技术研究院 | 科技史与文化遗产研究院 | 顺德研究生院 | - | -------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------ | ---------- | -------------------- | -------- | ------------------ | ------------ | -------- | -------------- | ---------- | ------------------------ | ------------------------ | -------------- | ------------------------ | ---------------------------- | ---------------- | ---------------------- | ------------ | - | all | cres | seee | metall | mse | me | saee | scce | shuli | huasheng | sem | wenfa | marx | sfs | ncms | skl | iet | cicst | slam | adma | ihmm | sd |`, +| -------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------ | ---------- | -------------------- | -------- | ------------------ | ------------ | -------- | -------------- | ---------- | ------------------------ | ------------------------ | -------------- | ------------------------ | ---------------------------- | ---------------- | ---------------------- | ------------ | +| all | cres | seee | metall | mse | me | saee | scce | shuli | huasheng | sem | wenfa | marx | sfs | ncms | skl | iet | cicst | slam | adma | ihmm | sd |`, }; async function handler(ctx) { diff --git a/lib/routes/ustc/eeis.ts b/lib/routes/ustc/eeis.ts index 85d532d5ba7077..e155d0a6f849ad 100644 --- a/lib/routes/ustc/eeis.ts +++ b/lib/routes/ustc/eeis.ts @@ -37,8 +37,8 @@ export const route: Route = { handler, url: 'eeis.ustc.edu.cn/', description: `| 通知公告 | 新闻信息 | - | -------- | -------- | - | tzgg | xwxx |`, +| -------- | -------- | +| tzgg | xwxx |`, }; async function handler(ctx) { diff --git a/lib/routes/ustc/gs.ts b/lib/routes/ustc/gs.ts index b5cfb55b0200d1..8dbd4e08c554e4 100644 --- a/lib/routes/ustc/gs.ts +++ b/lib/routes/ustc/gs.ts @@ -37,8 +37,8 @@ export const route: Route = { handler, url: 'gradschool.ustc.edu.cn/', description: `| 通知公告 | 新闻动态 | - | -------- | -------- | - | tzgg | xwdt |`, +| -------- | -------- | +| tzgg | xwdt |`, }; async function handler(ctx) { diff --git a/lib/routes/ustc/index.ts b/lib/routes/ustc/index.ts index 07bba24d84192b..dc955755263939 100644 --- a/lib/routes/ustc/index.ts +++ b/lib/routes/ustc/index.ts @@ -51,8 +51,8 @@ export const route: Route = { handler, url: 'ustc.edu.cn/', description: `| 教学类 | 科研类 | 管理类 | 服务类 | - | ------ | ------ | ------ | ------ | - | jx | ky | gl | fw |`, +| ------ | ------ | ------ | ------ | +| jx | ky | gl | fw |`, }; async function handler(ctx) { diff --git a/lib/routes/ustc/job.ts b/lib/routes/ustc/job.ts index ea0e27adeb22ec..0e0b0d546a711c 100644 --- a/lib/routes/ustc/job.ts +++ b/lib/routes/ustc/job.ts @@ -34,8 +34,8 @@ export const route: Route = { handler, url: 'job.ustc.edu.cn/', description: `| 专场招聘会 | 校园双选会 | 空中宣讲 | 招聘公告 | - | ----------- | ------------ | --------- | -------- | - | RecruitList | Doublechoice | Broadcast | joblist2 |`, +| ----------- | ------------ | --------- | -------- | +| RecruitList | Doublechoice | Broadcast | joblist2 |`, }; async function handler(ctx) { diff --git a/lib/routes/ustc/jwc.ts b/lib/routes/ustc/jwc.ts index 414ebdf6346e79..76c9f0ba03de4f 100644 --- a/lib/routes/ustc/jwc.ts +++ b/lib/routes/ustc/jwc.ts @@ -31,8 +31,8 @@ export const route: Route = { handler, url: 'www.teach.ustc.edu.cn/', description: `| 信息 | 教学 | 考试 | 交流 | - | ---- | -------- | ---- | -------- | - | info | teaching | exam | exchange |`, +| ---- | -------- | ---- | -------- | +| info | teaching | exam | exchange |`, }; async function handler(ctx) { diff --git a/lib/routes/ustc/math.ts b/lib/routes/ustc/math.ts index cccdd7e245053b..212b6e6e4cee17 100644 --- a/lib/routes/ustc/math.ts +++ b/lib/routes/ustc/math.ts @@ -39,8 +39,8 @@ export const route: Route = { handler, url: 'math.ustc.edu.cn/', description: `| 学院新闻 | 通知公告 | 学术交流 | 学术报告 | - | -------- | -------- | -------- | -------- | - | xyxw | tzgg | xsjl | xsbg |`, +| -------- | -------- | -------- | -------- | +| xyxw | tzgg | xsjl | xsbg |`, }; async function handler(ctx) { diff --git a/lib/routes/ustc/namespace.ts b/lib/routes/ustc/namespace.ts index 24a29b4bbcd8fa..c45d8e828af03f 100644 --- a/lib/routes/ustc/namespace.ts +++ b/lib/routes/ustc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国科学技术大学', url: 'ustc.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ustc/scms.ts b/lib/routes/ustc/scms.ts new file mode 100644 index 00000000000000..5ae9e447d7b185 --- /dev/null +++ b/lib/routes/ustc/scms.ts @@ -0,0 +1,97 @@ +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'; +// import InvalidParameterError from '@/errors/types/invalid-parameter'; + +const map = new Map([ + ['kydt', { title: '科研动态', id: '2404' }], + ['tzgg', { title: '通知公告', id: '2402' }], + ['xshd', { title: '学术活动', id: 'xshd' }], + ['ynxw', { title: '院内新闻', id: '2405' }], +]); + +const host = 'https://scms.ustc.edu.cn'; + +export const route: Route = { + path: '/scms/:type?', + categories: ['university'], + example: '/ustc/scms/tzgg', + parameters: { type: '分类,见下表,默认为通知公告' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['scms.ustc.edu.cn/:id/list.htm'], + target: '/scms', + }, + ], + name: '化学与材料科学学院', + maintainers: ['boxie123'], + handler, + url: 'scms.ustc.edu.cn/', + description: `| 院内新闻 | 通知公告 | 科研动态 | 学术活动 | 其他 | +| -------- | -------- | -------- | -------- | -------- | +| ynxw | tzgg | kydt | xshd | 自定义id |`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type') ?? 'tzgg'; + const info = map.get(type); + // ?? { title: `中国科学技术大学化学与材料科学学院 - ${type}`, id: type }; + // if (!info) { + // throw new InvalidParameterError('invalid type'); + // } + const id = info?.id ?? type; + + const response = await got(`${host}/${id}/list.htm`); + const $ = load(response.data); + + const pageTitle = info?.title ?? $('head > title').text(); + + let items = $('#wp_news_w6 > .wp_article_list > .list_item') + .toArray() + .map((item) => { + const elem = $(item); + const title = elem.find('.Article_Title > a').attr('title').trim(); + let link = elem.find('.Article_Title > a').attr('href'); + link = link.startsWith('/') ? host + link : link; + // Assume that the articles are published at 12:00 UTC+8 + const pubDate = timezone(parseDate(elem.find('.Article_PublishDate').text(), 'YYYY-MM-DD'), -4); + return { + title, + pubDate, + link, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + let desc = ''; + try { + const response = await got(item.link); + desc = load(response.data)('div.wp_articlecontent').html(); + item.description = desc; + } catch { + // Intranet only contents + } + return item; + }) + ) + ); + + return { + title: `中国科学技术大学化学与材料科学学院 - ${pageTitle}`, + link: `${host}/${id}/list.htm`, + item: items, + }; +} diff --git a/lib/routes/ustc/sist.ts b/lib/routes/ustc/sist.ts index 3b0a2953929af5..44d0ba3dc9e96e 100644 --- a/lib/routes/ustc/sist.ts +++ b/lib/routes/ustc/sist.ts @@ -37,8 +37,8 @@ export const route: Route = { handler, url: 'sist.ustc.edu.cn/', description: `| 通知公告 | 招生工作 | - | -------- | -------- | - | tzgg | zsgz |`, +| -------- | -------- | +| tzgg | zsgz |`, }; async function handler(ctx) { diff --git a/lib/routes/usts/jwch.ts b/lib/routes/usts/jwch.ts index e9bf36669f0863..863e16f16ed825 100644 --- a/lib/routes/usts/jwch.ts +++ b/lib/routes/usts/jwch.ts @@ -24,8 +24,8 @@ export const route: Route = { maintainers: [], handler, description: `| 类型 | 教务动态 | 公告在线 | 选课通知 | - | ---- | -------- | -------- | -------- | - | | jwdt | ggzx | xktz |`, +| ---- | -------- | -------- | -------- | +| | jwdt | ggzx | xktz |`, }; async function handler(ctx) { diff --git a/lib/routes/usts/namespace.ts b/lib/routes/usts/namespace.ts index 46ea0e79c34db1..2254b6d4cc2f5f 100644 --- a/lib/routes/usts/namespace.ts +++ b/lib/routes/usts/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '苏州科技大学', url: 'jwch.usts.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/utgd/category.ts b/lib/routes/utgd/category.ts index 30d738f720f2ef..322903d2939d60 100644 --- a/lib/routes/utgd/category.ts +++ b/lib/routes/utgd/category.ts @@ -1,22 +1,12 @@ 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 timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; -import { art } from '@/utils/render'; -import path from 'node:path'; -import MarkdownIt from 'markdown-it'; -const md = MarkdownIt({ - html: true, -}); +import ofetch from '@/utils/ofetch'; +import { rootUrl, apiRootUrl, parseResult, parseArticle } from './utils'; export const route: Route = { - path: '/:category?', - categories: ['new-media'], - example: '/utgd/method', + path: '/category/:category?', + categories: ['new-media', 'popular'], + example: '/utgd/category/method', parameters: { category: '分类,可在对应分类页的 URL 中找到,默认为方法' }, features: { requireConfig: false, @@ -29,74 +19,43 @@ export const route: Route = { radar: [ { source: ['utgd.net/category/s/:category', 'utgd.net/'], - target: '/:category', + target: '/category/:category', }, ], name: '分类', maintainers: ['nczitzk'], handler, description: `| 方法 | 观点 | - | ------ | ------- | - | method | opinion |`, +| ------ | ------- | +| method | opinion |`, }; async function handler(ctx) { const category = ctx.req.param('category') ?? 'method'; - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20; + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 9; - const rootUrl = 'https://utgd.net'; - const apiUrl = `${rootUrl}/api/v2/pages/`; + const apiUrl = `${apiRootUrl}/api/v2/categories`; const currentUrl = `${rootUrl}/category/s/${category}`; - const slugUrl = `${rootUrl}/api/v2/category/slug/${category}/`; + const slugUrl = `${apiRootUrl}/api/v2/category/slug/${category}/`; - let response = await got({ - method: 'get', - url: slugUrl, - }); - - const data = response.data; + const categoryData = await ofetch(slugUrl); - response = await got({ - method: 'get', - url: apiUrl, - searchParams: { - type: 'article.Article', - fields: `article_category(category_name),article_tag(tag_name),title,article_image,article_author,article_description,article_published_time`, - article_category: data.id, - order: '-article_published_time', - limit, + const response = await ofetch(`${apiUrl}/${categoryData.id}/related_articles`, { + query: { + page: 1, + page_size: limit, }, }); - const items = await Promise.all( - response.data.items.map((item) => - cache.tryGet(`untag-${item.id}`, async () => { - const authorResponse = await got({ - method: 'get', - url: `${rootUrl}/api/v2/user/profile/${item.article_author.id}/`, - }); + const list = parseResult(response.results, limit); - return { - title: item.title, - link: `${rootUrl}/article/${item.id}`, - description: art(path.join(__dirname, 'templates/description.art'), { - membership: item.article_for_membership, - image: item.article_image, - description: md.render(item.article_description), - }), - author: authorResponse.data.display_name, - pubDate: timezone(parseDate(item.article_published_time), +8), - category: [...item.article_category.map((c) => c.category_name), ...item.article_tag.map((t) => t.tag_name)], - }; - }) - ) - ); + const items = await Promise.all(list.map((item) => parseArticle(item))); return { - title: `UNTAG - ${data.category_name}`, + title: `UNTAG - ${categoryData.category_name}`, link: currentUrl, item: items, - image: data.category_image, - description: data.category_description, + image: categoryData.category_image, + description: categoryData.category_description, }; } diff --git a/lib/routes/utgd/namespace.ts b/lib/routes/utgd/namespace.ts index e60baf5b204dbc..f6cd008c22c158 100644 --- a/lib/routes/utgd/namespace.ts +++ b/lib/routes/utgd/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'UNTAG', url: 'utgd.net', + lang: 'zh-CN', }; diff --git a/lib/routes/utgd/timeline.ts b/lib/routes/utgd/timeline.ts index cd3d147787788a..d3573166239b56 100644 --- a/lib/routes/utgd/timeline.ts +++ b/lib/routes/utgd/timeline.ts @@ -1,21 +1,11 @@ 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 timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; -import { art } from '@/utils/render'; -import path from 'node:path'; -import MarkdownIt from 'markdown-it'; -const md = MarkdownIt({ - html: true, -}); +import ofetch from '@/utils/ofetch'; +import { rootUrl, apiRootUrl, parseResult, parseArticle } from './utils'; export const route: Route = { path: '/timeline', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/utgd/timeline', parameters: {}, features: { @@ -40,42 +30,16 @@ export const route: Route = { async function handler(ctx) { const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20; - const rootUrl = 'https://utgd.net'; - const apiUrl = `${rootUrl}/api/v2/timeline/?page=1&page_size=${limit}`; - - const response = await got({ - method: 'get', - url: apiUrl, + const response = await ofetch(`${apiRootUrl}/api/v2/timeline/`, { + query: { + page: 1, + page_size: limit, + }, }); - const items = await Promise.all( - response.data.results.slice(0, limit).map((item) => - cache.tryGet(`untag-${item.id}`, async () => { - const detailResponse = await got({ - method: 'get', - url: `${rootUrl}/api/v2/article/${item.id}/`, - searchParams: { - fields: 'article_description,article_category(category_name),article_tag(tag_name)', - }, - }); - - const data = detailResponse.data; + const list = parseResult(response.results, limit); - return { - title: item.title, - link: `${rootUrl}/article/${item.id}`, - description: art(path.join(__dirname, 'templates/description.art'), { - membership: data.article_for_membership, - image: item.article_image, - description: md.render(data.article_description), - }), - author: item.article_author_displayname, - pubDate: timezone(parseDate(item.article_published_time), +8), - category: [...data.article_category.map((c) => c.category_name), ...data.article_tag.map((t) => t.tag_name)], - }; - }) - ) - ); + const items = await Promise.all(list.map((item) => parseArticle(item))); return { title: 'UNTAG', diff --git a/lib/routes/utgd/topic.ts b/lib/routes/utgd/topic.ts index e01fa5147c272d..10fa04a7d2c8dc 100644 --- a/lib/routes/utgd/topic.ts +++ b/lib/routes/utgd/topic.ts @@ -1,22 +1,12 @@ 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 timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; -import { art } from '@/utils/render'; -import path from 'node:path'; -import MarkdownIt from 'markdown-it'; +import ofetch from '@/utils/ofetch'; import InvalidParameterError from '@/errors/types/invalid-parameter'; -const md = MarkdownIt({ - html: true, -}); +import { rootUrl, apiRootUrl, parseResult, parseArticle } from './utils'; export const route: Route = { path: '/topic/:topic?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/utgd/topic/在线阅读专栏', parameters: { topic: '专题,默认为在线阅读专栏' }, features: { @@ -38,7 +28,7 @@ export const route: Route = { handler, url: 'utgd.net/topic', description: `| 在线阅读专栏 | 卡片笔记专题 | - | ------------ | ------------ | +| ------------ | ------------ | 更多专栏请见 [专题广场](https://utgd.net/topic)`, }; @@ -47,56 +37,24 @@ async function handler(ctx) { const topic = ctx.req.param('topic') ?? '在线阅读专栏'; const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20; - const rootUrl = 'https://utgd.net'; const currentUrl = `${rootUrl}/topic`; - const topicUrl = `${rootUrl}/api/v2/topic/`; + const topicUrl = `${apiRootUrl}/api/v2/topic/`; - let response = await got({ - method: 'get', - url: topicUrl, - }); + let response = await ofetch(topicUrl); - const topicItems = response.data.filter((i) => i.title === topic); + const topicItem = response.find((i) => i.title === topic); - if (!topicItems) { + if (!topicItem) { throw new InvalidParameterError(`No topic named ${topic}`); } - const topicItem = topicItems[0]; - const apiUrl = `${rootUrl}/api/v2/topic/${topicItem.id}/article/`; - response = await got({ - method: 'get', - url: apiUrl, - }); + response = await ofetch(apiUrl); - const items = await Promise.all( - response.data.slice(0, limit).map((item) => - cache.tryGet(`untag-${item.id}`, async () => { - const detailResponse = await got({ - method: 'get', - url: `${rootUrl}/api/v2/article/${item.id}/`, - searchParams: { - fields: 'article_description', - }, - }); + const list = parseResult(response.results, limit); - return { - title: item.title, - link: `${rootUrl}/article/${item.id}`, - description: art(path.join(__dirname, 'templates/description.art'), { - membership: item.article_for_membership, - image: item.article_image, - description: md.render(detailResponse.data.article_description), - }), - author: item.article_author_displayname, - pubDate: timezone(parseDate(item.article_published_time), +8), - category: [...item.article_category.map((c) => c.name), ...item.article_tag.map((t) => t.name)], - }; - }) - ) - ); + const items = await Promise.all(list.map((item) => parseArticle(item))); return { title: `UNTAG - ${topicItem.title}`, diff --git a/lib/routes/utgd/utils.ts b/lib/routes/utgd/utils.ts new file mode 100644 index 00000000000000..06dd1acca74a13 --- /dev/null +++ b/lib/routes/utgd/utils.ts @@ -0,0 +1,41 @@ +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import MarkdownIt from 'markdown-it'; +const md = MarkdownIt({ + html: true, +}); + +export const rootUrl = 'https://utgd.net'; +export const apiRootUrl = 'https://api.utgd.net'; + +export const parseResult = (results, limit) => + results.slice(0, limit).map((item) => ({ + id: item.id, + title: item.title, + link: `${rootUrl}/article/${item.id}`, + author: item.article_author_displayname, + pubDate: timezone(parseDate(item.article_published_time), +8), + category: item.article_category.map((c) => c.category_name), + })); + +export const parseArticle = (item) => + cache.tryGet(`untag-${item.id}`, async () => { + const data = await ofetch(`${apiRootUrl}/api/v2/article/${item.id}/`); + + item.description = art(path.join(__dirname, 'templates/description.art'), { + membership: data.article_for_membership, + image: data.article_image, + description: md.render(data.article_content), + }); + + item.category = [...data.article_category.map((c) => c.category_name), ...data.article_tag.map((t) => t.tag_name)]; + + return item; + }); diff --git a/lib/routes/uw/gix/news.ts b/lib/routes/uw/gix/news.ts index b48d42766c89db..d55cd103abcbd8 100644 --- a/lib/routes/uw/gix/news.ts +++ b/lib/routes/uw/gix/news.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['dykderrick'], handler, description: `| Blog | In The News | - | ---- | ----------- | - | blog | inthenews |`, +| ---- | ----------- | +| blog | inthenews |`, }; async function handler(ctx) { diff --git a/lib/routes/uw/namespace.ts b/lib/routes/uw/namespace.ts index fc54dfa7c2b28c..2f9169b767e7bf 100644 --- a/lib/routes/uw/namespace.ts +++ b/lib/routes/uw/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'University of Washington', url: 'gixnetwork.org', + lang: 'en', }; diff --git a/lib/routes/v1tx/namespace.ts b/lib/routes/v1tx/namespace.ts index 760820d3a133a4..7767031af77dfd 100644 --- a/lib/routes/v1tx/namespace.ts +++ b/lib/routes/v1tx/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'v1tx', url: 'v1tx.com', + lang: 'zh-CN', }; diff --git a/lib/routes/v2ex/namespace.ts b/lib/routes/v2ex/namespace.ts index dcfbbd0408f319..1db432c6259e59 100644 --- a/lib/routes/v2ex/namespace.ts +++ b/lib/routes/v2ex/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'V2EX', url: 'v2ex.com', + lang: 'zh-CN', }; diff --git a/lib/routes/v2ex/tab.ts b/lib/routes/v2ex/tab.ts index a0ba456e01cced..5eae5c52108f3c 100644 --- a/lib/routes/v2ex/tab.ts +++ b/lib/routes/v2ex/tab.ts @@ -1,11 +1,12 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; export const route: Route = { path: '/tab/:tabid', - categories: ['bbs'], + categories: ['bbs', 'popular'], + view: ViewType.Articles, example: '/v2ex/tab/hot', parameters: { tabid: 'tab标签ID,在 URL 可以找到' }, features: { diff --git a/lib/routes/v2ex/topics.ts b/lib/routes/v2ex/topics.ts index 57c4daa31f0907..5ef462078e83ff 100644 --- a/lib/routes/v2ex/topics.ts +++ b/lib/routes/v2ex/topics.ts @@ -1,12 +1,28 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/topics/:type', - categories: ['bbs'], + categories: ['bbs', 'popular'], + view: ViewType.Articles, example: '/v2ex/topics/latest', - parameters: { type: 'hot 或 latest' }, + parameters: { + type: { + description: '主题类型', + options: [ + { + value: 'hot', + label: '最热主题', + }, + { + value: 'latest', + label: '最新主题', + }, + ], + default: 'hot', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -44,6 +60,7 @@ async function handler(ctx) { link: item.url, author: item.member.username, comments: item.replies, + category: [item.node.title], })), }; } diff --git a/lib/routes/v2ex/xna.ts b/lib/routes/v2ex/xna.ts new file mode 100644 index 00000000000000..6dcf2eff8f7af1 --- /dev/null +++ b/lib/routes/v2ex/xna.ts @@ -0,0 +1,55 @@ +import { Route, ViewType } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/xna', + categories: ['bbs', 'blog'], + view: ViewType.Articles, + example: '/v2ex/xna', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'XNA', + maintainers: ['luckyscript'], + handler, +}; + +async function handler(ctx) { + const host = 'https://v2ex.com'; + const pageUrl = `${host}/xna`; + + const response = await got({ + method: 'get', + url: pageUrl, + }); + + const $ = load(response.data); + const items = $('div.xna-entry-main-container') + .toArray() + .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 50) + .map((dom) => { + const link = $(dom).find('.xna-entry-title > a'); + const author = $(dom).find('.xna-source-author > a').text(); + + return { + title: $(link).text(), + link: $(link).attr('href'), + description: $(link).text(), + author, + }; + }); + + return { + title: `V2EX-xna`, + link: pageUrl, + description: `V2EX-xna`, + item: items, + }; +} diff --git a/lib/routes/v2rayshare/namespace.ts b/lib/routes/v2rayshare/namespace.ts index dd6eb50b294edd..16d3faa41aac1f 100644 --- a/lib/routes/v2rayshare/namespace.ts +++ b/lib/routes/v2rayshare/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'V2rayShare', url: 'v2rayshare.com', + lang: 'zh-CN', }; diff --git a/lib/routes/vcb-s/category.ts b/lib/routes/vcb-s/category.ts index 2ab017c39270de..c2c84636268d8a 100644 --- a/lib/routes/vcb-s/category.ts +++ b/lib/routes/vcb-s/category.ts @@ -35,8 +35,8 @@ export const route: Route = { handler, url: 'vcb-s.com/', description: `| 作品项目 | 科普系列 | 计划与日志 | - | -------- | -------- | ---------- | - | works | kb | planlog |`, +| -------- | -------- | ---------- | +| works | kb | planlog |`, }; async function handler(ctx) { diff --git a/lib/routes/vcb-s/namespace.ts b/lib/routes/vcb-s/namespace.ts index f4e3908192fc28..9e32769fec3d96 100644 --- a/lib/routes/vcb-s/namespace.ts +++ b/lib/routes/vcb-s/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'VCB-Studio', url: 'vcb-s.com', + lang: 'zh-CN', }; diff --git a/lib/routes/verfghbw/namespace.ts b/lib/routes/verfghbw/namespace.ts index d60b596ac7a25b..27bafa833d2ecb 100644 --- a/lib/routes/verfghbw/namespace.ts +++ b/lib/routes/verfghbw/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Constitutional Court of Baden-Württemberg (Germany)', url: 'verfgh.baden-wuerttemberg.de', + lang: 'de', }; diff --git a/lib/routes/vertikal/latest.ts b/lib/routes/vertikal/latest.ts new file mode 100644 index 00000000000000..831ed1242504a0 --- /dev/null +++ b/lib/routes/vertikal/latest.ts @@ -0,0 +1,73 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/latest', + categories: ['new-media', 'popular'], + example: '/vertikal/latest', + radar: [ + { + source: ['vertikal.net/en/news', 'vertikal.net'], + }, + ], + name: 'News Archive', + maintainers: ['TonyRL'], + handler, + url: 'vertikal.net/en/news', +}; + +const baseUrl = 'https://vertikal.net'; + +async function handler() { + const response = await ofetch(`${baseUrl}/en/homepage/async-news-loader`, { + query: { + perPage: 24, + page: 1, + }, + }); + const $ = cheerio.load(response); + + const list = $('.grid__column') + .toArray() + .map((item) => { + const $item = $(item); + return { + title: $item.find('.news-teaser__title').text(), + link: `${baseUrl}${$item.find('.news-teaser').attr('href')}`, + pubDate: parseDate($item.find('.news-teaser__date').text(), 'DD.MM.YYYY'), + description: $item.find('.news-teaser__text').text(), + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = cheerio.load(response); + + const content = $('.newsentry'); + + item.category = content + .find('.newsentry__tags a') + .toArray() + .map((tag) => $(tag).text().trim()); + + content.find('.newsentry__date, .newsentry__title, .lazyimage-placeholder, .newsentry__tags, .newsentry__share, .newsentry__comments, .newsentry__write-comment').remove(); + + item.description = content.html(); + + return item; + }) + ) + ); + + return { + title: 'News Archive | Vertikal.net', + link: `${baseUrl}/en/news`, + image: `${baseUrl}/apple-touch-icon-152x152.png`, + item: items, + }; +} diff --git a/lib/routes/vertikal/namespace.ts b/lib/routes/vertikal/namespace.ts new file mode 100644 index 00000000000000..92bb61b7b0697b --- /dev/null +++ b/lib/routes/vertikal/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Vertikal.net', + url: 'vertikal.net', + lang: 'en', +}; diff --git a/lib/routes/vice/namespace.ts b/lib/routes/vice/namespace.ts new file mode 100644 index 00000000000000..549e358b3cbf04 --- /dev/null +++ b/lib/routes/vice/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'VICE', + url: 'vice.com', + lang: 'en', +}; diff --git a/lib/routes/vice/templates/article.art b/lib/routes/vice/templates/article.art new file mode 100644 index 00000000000000..0bfa7bf8416988 --- /dev/null +++ b/lib/routes/vice/templates/article.art @@ -0,0 +1,19 @@ +{{ if image }} +<figure> + <img src="{{ image.url }}" alt="{{ image.alt }}"> + <figcaption>{{ image.caption || image.credit || image.alt }}</figcaption> +</figure> +{{ /if }} + +{{ if body }} +<p>{{@ body.html }}</p> +{{ /if }} + +{{ if heading2 }} +<hr/> +<h2>{{@ heading2.html }}</h2> +{{ /if }} + +{{ if oembed }} +{{@ oembed.html }} +{{ /if }} diff --git a/lib/routes/vice/topic.ts b/lib/routes/vice/topic.ts new file mode 100644 index 00000000000000..51b0f977fcd953 --- /dev/null +++ b/lib/routes/vice/topic.ts @@ -0,0 +1,103 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +import { art } from '@/utils/render'; + +const __dirname = getCurrentPath(import.meta.url); +const render = (data) => art(path.join(__dirname, 'templates', 'article.art'), data); + +export const route: Route = { + path: '/topic/:topic/:language?', + categories: ['traditional-media'], + example: '/vice/topic/politics/en', + parameters: { + topic: 'Can be found in the URL', + language: 'defaults to `en`, use the website to discover other codes', + }, + radar: [ + { + source: ['www.vice.com/:language/topic/:topic'], + target: '/topic/:topic/:language', + }, + ], + name: 'Topic', + maintainers: ['K33k0'], + handler, + url: 'vice.com/', +}; + +async function handler(ctx) { + const { language = 'en', topic } = ctx.req.param(); + const response = await ofetch(`https://www.vice.com/${language}/topic/${topic}`); + const $ = load(response); + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + + const list = nextData.props.pageProps.listPageData.articles.map((item) => ({ + title: item.title, + link: `https://vice.com${item.url}`, + pubDate: parseDate(item.publish_date, 'x'), + author: item.contributions.map((c) => c.contributor.full_name).join(', '), + description: item.dek, + category: [...new Set([item.primary_topic.name, ...item.topics.map((t) => t.name)])], + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + const articleNextData = JSON.parse($('script#__NEXT_DATA__').text()).props.pageProps.data.articles[0]; + const bodyComponent = JSON.parse(articleNextData.body_components_json); + + item.description = + render({ + image: { + url: articleNextData.thumbnail_url, + alt: articleNextData.caption, + caption: articleNextData.caption, + credit: articleNextData.credit, + }, + }) + + bodyComponent + .map((component) => { + switch (component.role) { + case 'body': + return render({ body: { html: component.html } }); + case 'heading2': + return render({ heading2: { html: component.html } }); + case 'image': + return render({ + image: { + url: component.URL, + alt: component.alt, + caption: component.caption, + }, + }); + case 'oembed': + case 'tweet': + case 'youtube': + return render({ oembed: { html: component.oembed.html } }); + default: + return ''; + } + }) + .join(''); + + return item; + }) + ) + ); + + return { + // channel title + title: `VICE | ${topic} articles`, + // channel link + link: `https://vice.com/${language}/topic/${topic}`, + // each feed item + item: items, + }; +} diff --git a/lib/routes/vimeo/category.ts b/lib/routes/vimeo/category.ts index b8069104c790de..c95f098cbad2f9 100644 --- a/lib/routes/vimeo/category.ts +++ b/lib/routes/vimeo/category.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); @@ -11,7 +11,8 @@ import path from 'node:path'; export const route: Route = { path: '/category/:category/:staffpicks?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.Videos, example: '/vimeo/category/documentary/staffpicks', parameters: { category: 'Category name can get from url like `documentary` in [https://vimeo.com/categories/documentary/videos](https://vimeo.com/categories/documentary/videos) ', @@ -63,7 +64,7 @@ async function handler(ctx) { const vimeojs = response.data.data; const feedlink = `https://vimeo.com/categories/${category}/videos/sort:latest`; - const feedlinkstaffpicks = '?staffpicked=ture'; + const feedlinkstaffpicks = '?staffpicked=true'; const feedDescription = await cache.tryGet(feedlink + (staffpicks ? feedlinkstaffpicks : ''), async () => { const response = await got({ url: feedlink + (staffpicks ? feedlinkstaffpicks : ''), diff --git a/lib/routes/vimeo/namespace.ts b/lib/routes/vimeo/namespace.ts index 03ea2189b2011b..51b59df60b6872 100644 --- a/lib/routes/vimeo/namespace.ts +++ b/lib/routes/vimeo/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Vimeo', url: 'vimeo.com', + lang: 'en', }; diff --git a/lib/routes/vimeo/usr-videos.ts b/lib/routes/vimeo/usr-videos.ts index bd16a4a670b799..9588a6fe3845c7 100644 --- a/lib/routes/vimeo/usr-videos.ts +++ b/lib/routes/vimeo/usr-videos.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 path from 'node:path'; export const route: Route = { path: '/user/:username/:cat?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.Videos, example: '/vimeo/user/filmsupply/picks', parameters: { username: 'In this example [https://vimeo.com/filmsupply](https://vimeo.com/filmsupply) is `filmsupply`', @@ -26,9 +27,9 @@ export const route: Route = { name: 'User Profile', maintainers: ['MisteryMonster'], handler, - description: `:::tip Special category name attention + description: `::: tip Special category name attention Some of the categories contain slash like \`3D/CG\` , must change the slash \`/\` to the vertical bar\`|\`. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/visionias/namespace.ts b/lib/routes/visionias/namespace.ts new file mode 100644 index 00000000000000..311b0a695be1f2 --- /dev/null +++ b/lib/routes/visionias/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'VisionIAS', + url: 'visionias.in', + lang: 'en', + categories: ['study'], +}; diff --git a/lib/routes/visionias/news-today.ts b/lib/routes/visionias/news-today.ts new file mode 100644 index 00000000000000..d05e51313dc267 --- /dev/null +++ b/lib/routes/visionias/news-today.ts @@ -0,0 +1,100 @@ +import { Data, Route } from '@/types'; +import { baseUrl, extractNews } from './utils'; +import dayjs from 'dayjs'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +import logger from '@/utils/logger'; + +export const route: Route = { + path: '/newsToday/:filter?', + example: '/visionias/newsToday', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + parameters: { + filter: { + description: 'Period to fetch news for the current month. All news for the current month or only the latest', + default: 'latest', + options: [ + { value: 'all', label: 'All' }, + { value: 'latest', label: 'Latest' }, + ], + }, + }, + radar: [ + { + source: ['visionias.in/current-affairs/news-today'], + target: '/newsToday', + }, + ], + name: 'News Today', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler(ctx): Promise<Data> { + const filter = ctx.req.param('filter') ?? 'latest'; + const currentYear = dayjs().year(); + const currentMonth = dayjs().month() + 1; + logger.debug(`Getting news for month ${currentMonth} and year ${currentYear}`); + const response = await ofetch(`${baseUrl}/current-affairs/news-today/getbymonth?year=${currentYear}&month=${currentMonth}`); + + let items: any = []; + // let title = 'News Today'; + + if (response.length !== 0) { + if (filter === 'latest') { + const currentUrl = response[0].url; + // title = response[0].formatted_published_at; + items = await processCurrentNews(currentUrl); + } else { + const results = await Promise.all(response.map((element) => processCurrentNews(element.url))); + items.push(...results.flat()); + } + } + return { + title: 'News Today | Current Affairs | Vision IAS', + link: `${baseUrl}/current-affairs/news-today/archive`, + description: 'News Today is a daily bulletin providing readers with a comprehensive overview of news developments, news types, and technical terms.', + language: 'en', + item: items, + image: `${baseUrl}/current-affairs/images/news-today-logo.svg`, + icon: `${baseUrl}/current-affairs/favicon.ico`, + logo: `${baseUrl}/current-affairs/favicon.ico`, + allowEmpty: true, + }; +} + +async function processCurrentNews(currentUrl) { + const response = await ofetch(`${baseUrl}${currentUrl}`); + const $ = load(response); + const items = $(`#table-of-content > ul > li > a`) + .toArray() + .map((item) => { + const link = $(item).attr('href'); + const title = $(item).clone().children('span').remove().end().text().trim(); + return { + title, + link: title === 'Also in News' ? link : `${baseUrl}${link}`, + guid: link, + }; + }); + const newsPromises = await Promise.allSettled(items.map((item) => extractNews(item, 'main > div > div.mt-6 > div.flex > div.flex.mt-6'))); + const finalItems: any = []; + for (const news of newsPromises) { + if (news.status === 'fulfilled') { + finalItems.push(...(Array.isArray(news.value) ? news.value : [news.value])); + } else { + finalItems.push({ + title: 'Error Parse News', + }); + } + } + return finalItems; +} diff --git a/lib/routes/visionias/templates/description-sub.art b/lib/routes/visionias/templates/description-sub.art new file mode 100644 index 00000000000000..e947d6315aa283 --- /dev/null +++ b/lib/routes/visionias/templates/description-sub.art @@ -0,0 +1,6 @@ +{{ if heading }} + <h2>{{ heading }}</h2> +{{ /if }} +{{ if articleContent }} + {{@ articleContent }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/visionias/templates/description.art b/lib/routes/visionias/templates/description.art new file mode 100644 index 00000000000000..2f92e6a1c8d17b --- /dev/null +++ b/lib/routes/visionias/templates/description.art @@ -0,0 +1,12 @@ +{{ if heading }} + <h1>{{ heading }}</h1> +{{ /if }} +{{ if subItems }} + {{ each subItems item }} + {{ if item?.description }} + {{@ item.description }} + {{ /if }} + {{ /each }} +{{else}} + {{@ articleContent }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/visionias/utils.ts b/lib/routes/visionias/utils.ts new file mode 100644 index 00000000000000..e9ebec22d51a16 --- /dev/null +++ b/lib/routes/visionias/utils.ts @@ -0,0 +1,109 @@ +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import { DataItem } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; + +export const baseUrl = 'https://visionias.in'; + +export async function extractNews(item, selector) { + if (item.link === '') { + return item; + } + return await cache.tryGet(item.link, async () => { + const response = await ofetch(item.link || ''); + const $$ = load(response); + const postedDate = String($$('meta[property="article:published_time"]').attr('content')); + const updatedDate = String($$('meta[property="article:modified_time"]').attr('content')); + const tags = $$('meta[property="article:tag"]') + .toArray() + .map((tag) => $$(tag).attr('content')); + const content = $$(selector); + const heading = content.find('div.space-y-4 > h1').text(); + const mainGroup = content.find('div.flex > div.w-full'); + + const shortArticles = mainGroup.find('[x-data^="{isShortArticleOpen"]'); + const sections = mainGroup.find('[x-data^="{isSectionOpen"]'); + if (shortArticles.length !== 0) { + const items = shortArticles.toArray().map((element) => { + const mainDiv = $$(element); + const title = mainDiv.find('a > div > h1').text().trim(); + const id = mainDiv.find('a').attr('href'); + const htmlContent = extractArticle(mainDiv.html()); + const innerTags = mainDiv + .find('ul > li:contains("Tags :")') + ?.nextAll('li') + .toArray() + .map((tag) => $$(tag).text()); + const description = art(path.join(__dirname, `templates/description.art`), { + heading: title, + articleContent: htmlContent, + }); + return { + title: `${title} | ${heading}`, + pubDate: parseDate(postedDate), + category: innerTags, + description, + link: `${item.link}${id}`, + author: 'Vision IAS', + } as DataItem; + }); + return items; + } else if (sections.length === 0) { + const htmlContent = extractArticle(mainGroup.html()); + const description = art(path.join(__dirname, 'templates/description.art'), { + heading, + articleContent: htmlContent, + }); + return { + title: item.title, + pubDate: parseDate(postedDate), + category: tags, + description, + link: item.link, + updated: updatedDate ? parseDate(updatedDate) : null, + author: 'Vision IAS', + } as DataItem; + } else { + const items = sections.toArray().map((element) => { + const mainDiv = $$(element); + const title = mainDiv.find('a > div > h2').text().trim(); + const htmlContent = extractArticle(mainDiv.html(), 'div.ck-content'); + const description = art(path.join(__dirname, `templates/description-sub.art`), { + heading: title, + articleContent: htmlContent, + }); + return { description }; + }); + const description = art(path.join(__dirname, `templates/description.art`), { + heading, + subItems: items, + }); + return { + title: heading, + pubDate: parseDate(postedDate), + category: tags, + description, + link: item.link, + updated: updatedDate ? parseDate(updatedDate) : null, + author: 'Vision IAS', + } as DataItem; + } + }); +} + +function extractArticle(articleDiv, selectorString: string = '#article-content') { + const $ = load(articleDiv, null, false); + const articleDiv$ = $(articleDiv); + const articleContent = articleDiv$.find(String(selectorString)); + articleContent.find('figure').each((_, element) => { + $(element).css('width', ''); + }); + const htmlContent = articleContent.html(); + return htmlContent; +} diff --git a/lib/routes/visionias/weekly-focus.ts b/lib/routes/visionias/weekly-focus.ts new file mode 100644 index 00000000000000..65d55a478cffcb --- /dev/null +++ b/lib/routes/visionias/weekly-focus.ts @@ -0,0 +1,56 @@ +import { Data, Route } from '@/types'; +import { baseUrl, extractNews } from './utils'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/weeklyFocus', + example: '/visionias/weeklyFocus', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['visionias.in/current-affairs/weekly-focus'], + target: '/weeklyFocus', + }, + ], + name: 'Weekly Focus', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler(ctx): Promise<Data> { + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 1; + const response = await ofetch(`${baseUrl}/current-affairs/weekly-focus/archive`); + const $ = load(response); + const cards = $('div.weekly-focus-single-card').slice(0, limit).toArray(); + const individualLinks = cards.flatMap((card) => + $(card) + .find('a:has(p)') + .toArray() + .map((item) => { + const link = $(item).attr('href'); + return { link: link?.startsWith('http') ? link : `${baseUrl}${link}` }; + }) + ); + + const itemsPromise = await Promise.allSettled(individualLinks.map(({ link }) => extractNews({ link }, 'main > div > div.flex > div.flex.w-full > div.w-full.mt-6'))); + + return { + title: 'Weekly Focus | Current Affairs | Vision IAS', + link: `${baseUrl}/current-affairs/weekly-focus/archive`, + description: 'Weekly Focus provides weekly comprehensive analysis of current themes with multidimensional and consolidated content.', + language: 'en', + item: itemsPromise.map((item) => (item.status === 'fulfilled' ? item.value : { title: 'Error Parse News' })), + image: `${baseUrl}/current-affairs/images/weekly-focus-logo.svg`, + icon: `${baseUrl}/current-affairs/favicon.ico`, + logo: `${baseUrl}/current-affairs/favicon.ico`, + allowEmpty: true, + }; +} diff --git a/lib/routes/vocus/namespace.ts b/lib/routes/vocus/namespace.ts index 0220cbf4953dfc..38296d9cff8e93 100644 --- a/lib/routes/vocus/namespace.ts +++ b/lib/routes/vocus/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '方格子', url: 'vocus.cc', + lang: 'zh-TW', }; diff --git a/lib/routes/vocus/publication.ts b/lib/routes/vocus/publication.ts index e144d1003e84e6..fc829902028431 100644 --- a/lib/routes/vocus/publication.ts +++ b/lib/routes/vocus/publication.ts @@ -5,7 +5,7 @@ import { processList, ProcessFeed, baseUrl, apiUrl } from './utils'; export const route: Route = { path: '/publication/:id', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/vocus/publication/bass', parameters: { id: '出版專題 id,可在出版專題主页的 URL 找到' }, features: { diff --git a/lib/routes/vocus/user.ts b/lib/routes/vocus/user.ts index 95263d1d267934..fd4e9d623765b3 100644 --- a/lib/routes/vocus/user.ts +++ b/lib/routes/vocus/user.ts @@ -5,7 +5,7 @@ import { processList, ProcessFeed, baseUrl, apiUrl } from './utils'; export const route: Route = { path: '/user/:id', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/vocus/user/tsetyan', parameters: { id: '用户 id,可在用户主页的 URL 找到' }, features: { diff --git a/lib/routes/vom/featured.ts b/lib/routes/vom/featured.ts index 21230d588c2166..eab557c807131d 100644 --- a/lib/routes/vom/featured.ts +++ b/lib/routes/vom/featured.ts @@ -28,8 +28,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| English | 日本語 | Монгол | Русский | 简体中文 | - | ------- | ------ | ------ | ------- | -------- | - | en | ja | mn | ru | zh |`, +| ------- | ------ | ------ | ------- | -------- | +| en | ja | mn | ru | zh |`, }; async function handler(ctx) { diff --git a/lib/routes/vom/namespace.ts b/lib/routes/vom/namespace.ts index b8c5bd4f47f3d3..febf2023a429e8 100644 --- a/lib/routes/vom/namespace.ts +++ b/lib/routes/vom/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'Voice of Mongolia 蒙古之声', + name: 'Voice of Mongolia', url: 'vom.mn', + lang: 'en', }; diff --git a/lib/routes/voronoiapp/author.ts b/lib/routes/voronoiapp/author.ts new file mode 100644 index 00000000000000..9731aa638624d5 --- /dev/null +++ b/lib/routes/voronoiapp/author.ts @@ -0,0 +1,41 @@ +import type { Data, Route } from '@/types'; +import { CommonDataProperties, CommonRouteProperties, getPostItems } from './common'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; + +export const route: Route = { + ...CommonRouteProperties, + name: 'Author Posts', + path: '/author/:username', + radar: [ + { + source: ['www.voronoiapp.com/author/:username'], + target: '/author/:username', + }, + ], + example: '/voronoiapp/author/visualcapitalist', + parameters: { + username: 'The username of the author', + }, + handler: async (ctx) => { + const { username } = ctx.req.param(); + const uid = await getUidFromUsername(username); + const items = await getPostItems({ order: 'DESC', author: uid }); + return { + ...CommonDataProperties, + title: `Voronoi Posts by ${username}`, + link: `https://www.voronoiapp.com/author/${username}`, + item: items, + } as Data; + }, +}; +async function getUidFromUsername(username: string): Promise<string> { + return (await cache.tryGet(`voronoiapp-author-${username}`, async () => { + const response = await ofetch<'text'>(`https://www.voronoiapp.com/author/${username}`); + const match = response.match(/\\"uid\\":\\"([\w-]+)\\"/); + if (!match) { + throw new Error(`No UID found for username: ${username}`); + } + return match[1]; + })) as string; +} diff --git a/lib/routes/voronoiapp/common.ts b/lib/routes/voronoiapp/common.ts new file mode 100644 index 00000000000000..7bffb78a1275b4 --- /dev/null +++ b/lib/routes/voronoiapp/common.ts @@ -0,0 +1,247 @@ +import { ViewType, type Data, type DataItem, type Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; +import type { Post } from './types'; + +export async function getPostItems(params: { + feed?: string; + search?: string; + swimlane?: string; + tab?: string; + time_range?: string; + category?: string; + order?: string; + author?: string; + limit?: number; + offset?: number; +}): Promise<DataItem[]> { + const baseUrl = 'https://9oyi4rk426.execute-api.ca-central-1.amazonaws.com/production/post'; + const url = new URL(baseUrl); + const finalSearchParams = Object.assign( + { + limit: 20, + offset: 0, + }, + params + ); + if (finalSearchParams.time_range !== undefined) { + finalSearchParams.time_range = finalSearchParams.time_range.toUpperCase(); + if (!TimeRangeParam.options.some((option) => option.value === finalSearchParams.time_range)) { + throw new Error(`Invalid time range: ${finalSearchParams.time_range}`); + } + // The Voronoi API doesn't support "ALL" + if (finalSearchParams.time_range === 'ALL') { + finalSearchParams.time_range = undefined; + } + } + if (finalSearchParams.category !== undefined && finalSearchParams.category !== null) { + const category = finalSearchParams.category; + finalSearchParams.category = CategoryParam.options.find((option) => option.value.toLowerCase() === category.toLowerCase())?.value; + if (finalSearchParams.category === undefined) { + throw new Error(`Invalid category: ${finalSearchParams.category}`); + } + } + if (finalSearchParams.tab !== undefined && finalSearchParams.tab !== null) { + finalSearchParams.tab = finalSearchParams.tab.toUpperCase(); + if (!Object.values(TabMap).includes(finalSearchParams.tab)) { + throw new Error(`Invalid tab: ${finalSearchParams.tab}`); + } + } + for (const key in finalSearchParams) { + if (finalSearchParams[key] !== undefined && finalSearchParams[key] !== null) { + url.searchParams.set(key, finalSearchParams[key]); + } + } + const data = await ofetch<Post[]>(url.toString()); + const items: DataItem[] = data.map((post) => ({ + title: post.headline, + link: `https://www.voronoiapp.com/${post.category.split(' ').join('-').toLowerCase()}/${post.link}`, + pubDate: parseDate(post.published_at), + description: `<img src="https://cdn.voronoiapp.com/public/${post.webp_image}" /> + ${post.description}`, + image: `https://cdn.voronoiapp.com/public/${post.webp_image}`, + author: post.author.first_name + ' ' + post.author.last_name, + updated: parseDate(post.updated_at), + category: [post.category], + enclosure_url: `https://cdn.voronoiapp.com/public/${post.dataset}`, + enclosure_type: 'text/csv', + enclosure_title: post.dataset, + upvotes: post.likes, + comments: post.commented, + })); + + return items; +} + +export const CategoryParam = { + description: 'The category of the post', + default: '', + options: [ + { + value: '', + label: 'All categories', + }, + { + value: 'Automotive', + label: 'Automotive Data Insights - Explore a range of automotive data visualizations showcasing trends, innovations, and market dynamics in the automotive industry.', + }, + { + value: 'Business', + label: 'Business Visualization Trends - Discover business visualizations covering market analysis, corporate strategies, and economic impacts across global industries.', + }, + { + value: 'Climate', + label: 'Climate Data Visualized - Delve into climate change data visualizations that detail weather patterns, environmental impacts, and sustainability efforts worldwide.', + }, + { + value: 'Demographics', + label: 'Demographic Visual Insights - Explore visual demographics data showcasing population trends, societal changes, and demographic analytics across regions.', + }, + { + value: 'Economy', + label: 'Economic Visualization Insights - View economic visualizations illustrating financial markets, economic policies, and global economic health.', + }, + { + value: 'Energy', + label: 'Energy Industry Visual Data - Discover the dynamics of global energy consumption, renewable sources, and energy market trends through vivid visualizations.', + }, + { + value: 'Entertainment', + label: 'Entertainment Industry Data - Explore data visualizations in the entertainment industry, covering everything from box office trends to streaming service analytics.', + }, + { + value: 'Geopolitics', + label: 'Geopolitical Data Visualized - Understand global geopolitical shifts and international relations through comprehensive geopolitical data visualizations.', + }, + { + value: 'Healthcare', + label: 'Healthcare Insights Visualized - Analyze healthcare data visualizations spanning disease trends, healthcare services, and public health policies.', + }, + { + value: 'Innovation', + label: 'Innovation in Data - Dive into innovation data visualizations highlighting technology advancements, R&D investments, and patent trends.', + }, + { + value: 'Maps', + label: 'Cartographic Visual Insights - Discover cartographic visualizations that map everything from socio-economic data to geographical phenomena.', + }, + { + value: 'Markets', + label: 'Market Trends Visualized - Visualize market trends, financial data, and economic forecasts through comprehensive market visualizations.', + }, + { + value: 'Money', + label: 'Financial Data Visualized - Dive into financial visualizations depicting currency trends, investment flows, and banking statistics.', + }, + { + value: 'Natural Resources', + label: 'Natural Resources Data - Explore visualizations of natural resources, detailing extraction, consumption, and conservation data.', + }, + { + value: 'Politics', + label: 'Political Visual Insights - Analyze political trends, election results, and legislative impacts through detailed political visualizations.', + }, + { + value: 'Public Opinion', + label: 'Public Opinion Trends - Discover visualizations of public opinion polls, social trends, and cultural shifts across different regions.', + }, + { + value: 'Real Estate', + label: 'Real Estate Market Insights - Explore real estate market trends, property values, and urban development through targeted data visualizations.', + }, + { + value: 'Sports', + label: 'Sports Data Insights - Analyze sports data visualizations that showcase performance statistics, team rankings, and sports economics.', + }, + { + value: 'Technology', + label: 'Technology Trends Visualized - Dive into technology visualizations highlighting industry trends, tech adoption rates, and innovation impacts.', + }, + { + value: 'Wealth', + label: 'Wealth Distribution Insights - Explore wealth distribution, financial health, and economic disparities through detailed visualizations.', + }, + { + value: 'Travel', + label: 'Travel Trends Visualized - Discover travel trends, tourism statistics, and destination analytics through engaging visualizations.', + }, + { + value: 'Nature', + label: 'Nature and Conservation Data - Delve into visualizations of ecological data, wildlife statistics, and conservation efforts around the globe.', + }, + { + value: 'Space', + label: 'Space Exploration Data - Explore the universe with space data visualizations covering planetary science, space missions, and astronomical discoveries.', + }, + { + value: 'Diagram', + label: 'Diagrammatic Data Insights - Understand complex data through diagrams that simplify information across various topics and industries.', + }, + { + value: 'Other', + label: "Diverse Data Visualizations - Explore a variety of data visualizations that don't neatly fit into any single category but offer unique insights.", + }, + ], +}; + +export const TimeRangeParam = { + description: 'Time range between which the posts are popular.', + default: 'MONTH', + options: [ + { + value: 'WEEK', + label: 'Last 7 days', + }, + { + value: 'MONTH', + label: 'Last 30 days', + }, + { + value: 'YEAR', + label: 'Last 12 months', + }, + { + value: 'ALL', + label: 'All time', + }, + ], +}; + +export const TabMap = { + 'most-popular': 'POPULAR', + 'most-discussed': 'DISCUSSED', + 'most-viewed': 'VIEWED', +}; + +export const TabParam = { + description: 'The tab to get the popular posts from.', + default: 'most-popular', + options: [ + { + value: 'most-popular', + label: 'Most Liked', + }, + { + value: 'most-discussed', + label: 'Most Discussed', + }, + { + value: 'most-viewed', + label: 'Most Viewed', + }, + ], +}; + +export const CommonRouteProperties: Pick<Route, 'url' | 'categories' | 'maintainers' | 'view'> = { + url: 'voronoiapp.com', + categories: ['picture', 'popular'], + view: ViewType.Pictures, + maintainers: ['Cesaryuan'], +}; + +export const CommonDataProperties: Pick<Data, 'allowEmpty' | 'image'> | { logo: string; icon: string } = { + logo: 'https://about.voronoiapp.com/wp-content/uploads/2023/07/voronoi-icon.png', + image: 'https://about.voronoiapp.com/wp-content/uploads/2023/07/voronoi-icon.png', + icon: 'https://about.voronoiapp.com/wp-content/uploads/2023/07/voronoi-icon.png', + allowEmpty: true, +}; diff --git a/lib/routes/voronoiapp/editors-pick.ts b/lib/routes/voronoiapp/editors-pick.ts new file mode 100644 index 00000000000000..1907f290f7e088 --- /dev/null +++ b/lib/routes/voronoiapp/editors-pick.ts @@ -0,0 +1,28 @@ +import type { Data, Route } from '@/types'; +import { getPostItems, CategoryParam, CommonRouteProperties, CommonDataProperties } from './common'; + +export const route: Route = { + ...CommonRouteProperties, + name: "Editor's Pick Posts", + path: '/editors-pick/:category?', + radar: [ + { + source: ['www.voronoiapp.com/posts/editors-pick'], + target: '/editors-pick', + }, + ], + example: '/voronoiapp/editors-pick', + parameters: { + category: CategoryParam, + }, + handler: async (ctx) => { + const { category = '' } = ctx.req.param(); + const items = await getPostItems({ swimlane: 'CURATED', category: category === '' ? undefined : category }); + return { + ...CommonDataProperties, + title: `Voronoi Editor's Pick Posts${category ? ` - ${category}` : ''}`, + link: 'https://www.voronoiapp.com/editors-pick', + item: items, + } as Data; + }, +}; diff --git a/lib/routes/voronoiapp/home.ts b/lib/routes/voronoiapp/home.ts new file mode 100644 index 00000000000000..3558e1e229d1ea --- /dev/null +++ b/lib/routes/voronoiapp/home.ts @@ -0,0 +1,29 @@ +import type { Data, Route } from '@/types'; +import { getPostItems, CategoryParam, CommonRouteProperties, CommonDataProperties } from './common'; + +export const route: Route = { + ...CommonRouteProperties, + name: 'Home Posts', + path: '/home/:category?', + description: 'This is the home page of Voronoi App', + radar: [ + { + source: ['www.voronoiapp.com', 'www.voronoiapp.com/posts/voronoi'], + target: '/home', + }, + ], + example: '/voronoiapp/home', + parameters: { + category: CategoryParam, + }, + handler: async (ctx) => { + const { category = '' } = ctx.req.param(); + const items = await getPostItems({ feed: 'VORONOI', category: category === '' ? undefined : category }); + return { + ...CommonDataProperties, + title: `Voronoi Home Posts${category ? ` - ${category}` : ''}`, + link: 'https://www.voronoiapp.com', + item: items, + } as Data; + }, +}; diff --git a/lib/routes/voronoiapp/latest.ts b/lib/routes/voronoiapp/latest.ts new file mode 100644 index 00000000000000..2ef90fc0d9b7f3 --- /dev/null +++ b/lib/routes/voronoiapp/latest.ts @@ -0,0 +1,28 @@ +import type { Data, Route } from '@/types'; +import { getPostItems, CategoryParam, CommonRouteProperties, CommonDataProperties } from './common'; + +export const route: Route = { + ...CommonRouteProperties, + name: 'Latest Posts', + path: '/latest/:category?', + radar: [ + { + source: ['www.voronoiapp.com/posts/latest'], + target: '/latest', + }, + ], + example: '/voronoiapp/latest', + parameters: { + category: CategoryParam, + }, + handler: async (ctx) => { + const { category = '' } = ctx.req.param(); + const items = await getPostItems({ swimlane: 'LATEST', category: category === '' ? undefined : category }); + return { + ...CommonDataProperties, + title: `Voronoi Latest Posts${category ? ` - ${category}` : ''}`, + link: 'https://www.voronoiapp.com/latest', + item: items, + } as Data; + }, +}; diff --git a/lib/routes/voronoiapp/namespace.ts b/lib/routes/voronoiapp/namespace.ts new file mode 100644 index 00000000000000..1f2edd1ef2064d --- /dev/null +++ b/lib/routes/voronoiapp/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Voronoi', + url: 'voronoiapp.com', + lang: 'en', +}; diff --git a/lib/routes/voronoiapp/popular.ts b/lib/routes/voronoiapp/popular.ts new file mode 100644 index 00000000000000..ab99de9f282ef2 --- /dev/null +++ b/lib/routes/voronoiapp/popular.ts @@ -0,0 +1,49 @@ +import type { Data, Route } from '@/types'; +import { CategoryParam, CommonDataProperties, CommonRouteProperties, getPostItems, TabMap, TabParam, TimeRangeParam } from './common'; + +export const route: Route = { + ...CommonRouteProperties, + name: 'Popular Posts', + path: '/popular/:tab?/:time_range?/:category?', + radar: [ + { + title: 'Most Liked Posts', + source: ['www.voronoiapp.com/posts/most-popular'], + target: '/popular/most-popular', + }, + { + title: 'Most Discussed Posts', + source: ['www.voronoiapp.com/posts/most-discussed'], + target: '/popular/most-discussed', + }, + { + title: 'Most Viewed Posts', + source: ['www.voronoiapp.com/posts/most-viewed'], + target: '/popular/most-viewed', + }, + ], + parameters: { + tab: TabParam, + time_range: TimeRangeParam, + category: CategoryParam, + }, + example: '/voronoiapp/popular/most-popular/MONTH', + handler: async (ctx) => { + const { tab = 'most-popular', time_range = 'MONTH', category = '' } = ctx.req.param(); + if (!TabMap[tab.toLowerCase()]) { + throw new Error(`Invalid tab: ${tab}`); + } + const items = await getPostItems({ + swimlane: 'POPULAR', + tab: TabMap[tab.toLowerCase()], + time_range: time_range === '' ? undefined : time_range.toUpperCase(), + category: category === '' ? undefined : category, + }); + return { + ...CommonDataProperties, + title: `Voronoi ${TabParam.options.find((option) => option.value === tab.toLowerCase())?.label} Posts in ${TimeRangeParam.options.find((option) => option.value === time_range.toUpperCase())?.label}${category ? ` - ${category}` : ''}`, + link: `https://www.voronoiapp.com/posts/${tab}`, + item: items, + } as Data; + }, +}; diff --git a/lib/routes/voronoiapp/search.ts b/lib/routes/voronoiapp/search.ts new file mode 100644 index 00000000000000..d04a995157db8c --- /dev/null +++ b/lib/routes/voronoiapp/search.ts @@ -0,0 +1,33 @@ +import type { Data, Route } from '@/types'; +import { CommonDataProperties, CommonRouteProperties, getPostItems } from './common'; + +export const route: Route = { + ...CommonRouteProperties, + name: 'Search Keyword Posts', + path: '/search/:keyword', + radar: [ + { + source: ['www.voronoiapp.com/explore'], + // 新版本中貌似不再支持 function 形式的 target + target: (_, url) => { + const parsedURL = new URL(url); + const keyword = parsedURL.searchParams.get('search'); + return `/voronoiapp/search/${keyword}`; + }, + }, + ], + example: '/voronoiapp/search/china', + parameters: { + keyword: 'The keyword to search for', + }, + handler: async (ctx) => { + const { keyword } = ctx.req.param(); + const items = await getPostItems({ search: keyword }); + return { + ...CommonDataProperties, + title: `Voronoi Posts for "${keyword}"`, + link: `https://www.voronoiapp.com/explore?search=${encodeURIComponent(keyword)}`, + item: items, + } as Data; + }, +}; diff --git a/lib/routes/voronoiapp/types.d.ts b/lib/routes/voronoiapp/types.d.ts new file mode 100644 index 00000000000000..e1453d6c9e97d0 --- /dev/null +++ b/lib/routes/voronoiapp/types.d.ts @@ -0,0 +1,78 @@ +export interface Social { + website?: string; + x?: string; + linkedin?: string; + instagram?: string; +} + +export interface Author { + first_name: string; + last_name: string; + preferred_username: string; + web_site: string; + email: string; + picture: string; + bio: string; + sub: string; + groups: string; + badge?: string; + rcv_mail: boolean; + rcv_push: boolean; + key?: string; + bookmarks?: string; + following?: string; + followers: number; + posts: number; + views: number; + wviews: number; + eviews: number; + imps: number; + wimps: number; + eimps: number; + social: Social; +} + +export interface Post { + pid: number; + uid: string; + completed: number; + step: number; + status: string; + curated: boolean; + headline: string; + link: string; + image: string; + webp_image: string; + preview_image: string; + cropper_data: string; + description: string; + category: string; + tags: string[]; + feeds?: string[]; + dataset: string; + dataset_settings: string; + sources: string[]; + note: string; + comments: number; + views: number; + wviews: number; + eviews: number; + imps: number; + wimps: number; + eimps: number; + commented: number; + shares: number; + bookmarks: number; + likes: number; + cools: number; + loves: number; + debatables: number; + insightfuls: number; + created_at: number; + updated_at: number; + author: Author; + subscribe_vc: boolean; + sponsored: boolean; + pinned: boolean; + published_at: number; +} diff --git a/lib/routes/wabei/index.ts b/lib/routes/wabei/index.ts new file mode 100644 index 00000000000000..e4bc1869fe1317 --- /dev/null +++ b/lib/routes/wabei/index.ts @@ -0,0 +1,75 @@ +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: '/hot-recommend', + categories: ['finance'], + example: '/wabei/hot-recommend', + url: 'www.wabei.cn', + name: '热门推荐', + maintainers: ['p3psi-boo'], + handler, +}; + +async function handler() { + const baseUrl = 'https://www.wabei.cn'; + + const response = await got({ + method: 'get', + url: baseUrl, + }); + + const $ = load(response.data); + + const list = $('.hot-news.visible-lg') + .toArray() + .map((item) => { + const elem = $(item); + const title = elem.find('h4 a').text().trim(); + const link = elem.find('h4 a').attr('href'); + const author = elem.find('span.author').text().trim(); + const description = elem.find('p.visible-lg a').text().trim(); + + const label = elem.find('span.lable').text().trim(); + const tags = elem + .find('span.blue-btn') + .toArray() + .map((tag) => $(tag).text().trim()); + const category = [label, ...tags]; + + return { + title, + link: `${baseUrl}${link}`, + category, + author, + description, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got({ + method: 'get', + url: item.link, + }); + + const content = load(detailResponse.data); + + item.description = content('.subject-content').html() || item.description; + item.pubDate = parseDate(content('.attr .time').text().trim(), 'YYYY/MM/DD HH:mm:ss'); + + return item; + }) + ) + ); + + return { + title: '挖贝网 - 热门推荐', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/wabei/namespace.ts b/lib/routes/wabei/namespace.ts new file mode 100644 index 00000000000000..0c80de411b92a4 --- /dev/null +++ b/lib/routes/wabei/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '挖贝网', + url: 'www.wabei.cn', + description: '挖贝网专注于新三板、A股和港股报道', + lang: 'zh-CN', +}; diff --git a/lib/routes/wainao/namespace.ts b/lib/routes/wainao/namespace.ts new file mode 100644 index 00000000000000..6910bd356c0cfb --- /dev/null +++ b/lib/routes/wainao/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '歪脑', + url: 'wainao.me', + description: '歪脑是为讲中文的年轻一代度身定制的新闻杂志。', + lang: 'zh-CN', +}; diff --git a/lib/routes/wainao/templates/description.art b/lib/routes/wainao/templates/description.art new file mode 100644 index 00000000000000..1cb5fc8fa6d1b5 --- /dev/null +++ b/lib/routes/wainao/templates/description.art @@ -0,0 +1,9 @@ +{{ if elements.length > 0 }} + {{ each elements element }} + {{ if element.type === 'text' }} + <p>{{ element.content }}</p> + {{ else if element.type === 'raw_html' }} + {{@ element.content }} + {{ /if }} + {{ /each }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/wainao/topics.ts b/lib/routes/wainao/topics.ts new file mode 100644 index 00000000000000..00ca95b29fd678 --- /dev/null +++ b/lib/routes/wainao/topics.ts @@ -0,0 +1,214 @@ +import { type Data, type DataItem, type Route, 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 { type CheerioAPI, 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<Data> => { + const { id = 'hotspot' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'https://www.wainao.me'; + const targetUrl: string = new URL(`topics/${id}`, baseUrl).href; + const apiUrl: string = new URL('pf/api/v3/content/fetch/story-feed-sections', baseUrl).href; + + const response = await ofetch(apiUrl, { + query: { + query: JSON.stringify({ + feedOffset: 0, + feedSize: limit, + includeSections: `/topics/${id}`, + }), + d: 81, + _website: 'wainao', + }, + }); + + const targetResponse = await ofetch(targetUrl); + const $: CheerioAPI = load(targetResponse); + const language = $('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + items = response.content_elements + .slice(0, limit) + .map((item): DataItem => { + const title: string = item.headlines.basic; + const description: string = art(path.join(__dirname, 'templates/description.art'), { + elements: item.content_elements, + }); + const pubDate: number | string = item.publish_date; + const linkUrl: string | undefined = item.website_url; + const categories: string[] = [item.taxonomy?.primary_section?.name].filter(Boolean); + const authors: DataItem['author'] = + item.credits?.by?.map((author) => ({ + name: author.name, + })) ?? []; + const guid: string = item.website_url; + const image: string | undefined = item.promo_items.basic.url; + const updated: number | string = item.last_updated_date; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDate ? parseDate(pubDate) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + category: categories, + author: authors, + guid, + id: guid, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: updated ? parseDate(updated) : undefined, + language, + }; + + return processedItem; + }) + .filter((_): _ is DataItem => true); + + return { + title: $('title').text(), + description: $('meta[property="og:title"]').attr('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: $('meta[property="og:image"]').attr('content'), + author: $('meta[property="og:site_name"]').attr('content'), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/topics/:id?', + name: '主题', + url: 'wainao.me', + maintainers: ['nczitzk'], + handler, + example: '/wainao/topics/hotspot', + parameters: { + id: { + description: '主题 id,默认为 `hotspot`,即热点,可在对应主题页 URL 中找到', + options: [ + { + label: '热点', + value: 'hotspot', + }, + { + label: '人物', + value: 'people', + }, + { + label: '身份', + value: 'identity', + }, + { + label: '政治', + value: 'politics', + }, + { + label: '社会', + value: 'society', + }, + { + label: '文化', + value: 'culture', + }, + { + label: '经济', + value: 'economics', + }, + { + label: '环境', + value: 'environment', + }, + { + label: 'FUN', + value: 'fun', + }, + ], + }, + }, + description: `:::tip +若订阅 [人物](https://www.wainao.me/topics/people),网址为 \`https://www.wainao.me/topics/people\`,请截取 \`https://www.wainao.me/topics/\` 到末尾的部分 \`people\` 作为 \`id\` 参数填入,此时目标路由为 [\`/wainao/topics/people\`](https://rsshub.app/wainao/topics/people)。 +::: + +| [热点](https://www.wainao.me/topics/hotspot) | [人物](https://www.wainao.me/topics/people) | [身份](https://www.wainao.me/topics/identity) | [政治](https://www.wainao.me/topics/politics) | [社会](https://www.wainao.me/topics/society) | [文化](https://www.wainao.me/topics/culture) | [经济](https://www.wainao.me/topics/economics) | [环境](https://www.wainao.me/topics/environment) | [FUN](https://www.wainao.me/topics/fun) | +| --------------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------- | +| [hotspot](https://rsshub.app/wainao/topics/hotspot) | [people](https://rsshub.app/wainao/topics/people) | [identity](https://rsshub.app/wainao/topics/identity) | [politics](https://rsshub.app/wainao/topics/politics) | [society](https://rsshub.app/wainao/topics/society) | [culture](https://rsshub.app/wainao/topics/culture) | [economics](https://rsshub.app/wainao/topics/economics) | [environment](https://rsshub.app/wainao/topics/environment) | [fun](https://rsshub.app/wainao/topics/fun) | +`, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.wainao.me/topics/:id'], + target: '/topics/:id', + }, + { + title: '热点', + source: ['www.wainao.me/topics/hotspot'], + target: '/topics/hotspot', + }, + { + title: '人物', + source: ['www.wainao.me/topics/people'], + target: '/topics/people', + }, + { + title: '身份', + source: ['www.wainao.me/topics/identity'], + target: '/topics/identity', + }, + { + title: '政治', + source: ['www.wainao.me/topics/politics'], + target: '/topics/politics', + }, + { + title: '社会', + source: ['www.wainao.me/topics/society'], + target: '/topics/society', + }, + { + title: '文化', + source: ['www.wainao.me/topics/culture'], + target: '/topics/culture', + }, + { + title: '经济', + source: ['www.wainao.me/topics/economics'], + target: '/topics/economics', + }, + { + title: '环境', + source: ['www.wainao.me/topics/environment'], + target: '/topics/environment', + }, + { + title: 'FUN', + source: ['www.wainao.me/topics/fun'], + target: '/topics/fun', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/wainao/wainao-reads.ts b/lib/routes/wainao/wainao-reads.ts new file mode 100644 index 00000000000000..ef918c81ca844a --- /dev/null +++ b/lib/routes/wainao/wainao-reads.ts @@ -0,0 +1,49 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/wainao-reads', + categories: ['new-media'], + example: '/wainao/wainao-reads', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + url: 'www.wainao.me', + name: '歪脑读', + maintainers: ['lucky13820'], + radar: [ + { + source: ['www.wainao.me', 'www.wainao.me/wainao-reads'], + target: '/wainao-reads', + }, + ], + handler, +}; + +async function handler() { + const apiUrl = 'https://www.wainao.me/pf/api/v3/content/fetch/content-api-collections?query={"content_alias":"wainao-hero"}&d=81&_website=wainao'; + const baseUrl = 'https://www.wainao.me'; + + const response = await got(apiUrl); + const data = JSON.parse(response.body); + + const items = data.content_elements.map((item) => ({ + title: item.headlines.basic, + description: item.description?.basic || '', + link: baseUrl + item.canonical_url, + pubDate: parseDate(item.publish_date), + image: item.promo_items?.basic?.url || '', + })); + + return { + title: '歪脑读 - 歪脑', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/wallhaven/index.ts b/lib/routes/wallhaven/index.ts index 4ec01b8375f414..a5ebd52fe0a815 100644 --- a/lib/routes/wallhaven/index.ts +++ b/lib/routes/wallhaven/index.ts @@ -28,11 +28,11 @@ export const route: Route = { maintainers: ['nczitzk', 'Fatpandac'], handler, url: 'wallhaven.cc/', - description: `:::tip + description: `::: tip Subscribe pages starting with \`https://wallhaven.cc/search\`, fill the text after \`?\` as \`filter\` in the route. The following is an example: The text after \`?\` is \`q=id%3A711&sorting=random&ref=fp&seed=8g0dgd\` for [Wallpaper Search: #landscape - wallhaven.cc](https://wallhaven.cc/search?q=id%3A711\&sorting=random\&ref=fp\&seed=8g0dgd), so the route is [/wallhaven/q=id%3A711\&sorting=random\&ref=fp\&seed=8g0dgd](https://rsshub.app/wallhaven/q=id%3A711\&sorting=random\&ref=fp\&seed=8g0dgd) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/wallhaven/namespace.ts b/lib/routes/wallhaven/namespace.ts index 82fc1032f6887a..e33d215708023e 100644 --- a/lib/routes/wallhaven/namespace.ts +++ b/lib/routes/wallhaven/namespace.ts @@ -3,11 +3,12 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'wallhaven', url: 'wallhaven.cc', - description: `:::tip + description: `::: tip When parameter **Need Details** is set to \`true\` \`yes\` \`t\` \`y\`, RSS will add the title, uploader, upload time, and category information of each image, which can support the filtering function of RSS reader. However, the number of requests to the site increases a lot when it is turned on, which causes the site to return \`Response code 429 (Too Many Requests)\`. So you need to specify a smaller \`limit\` parameter, i.e. add \`?limit=<the number of posts for a request>\` after the route, here is an example. For example [Latest Wallpapers](https://wallhaven.cc/latest), the route turning on **Need Details** is [/wallhaven/latest/true](https://rsshub.app/wallhaven/latest/true), and then specify a smaller \`limit\`. We can get [/wallhaven/latest/true?limit=5](https://rsshub.app/wallhaven/latest/true?limit=5). :::`, + lang: 'en', }; diff --git a/lib/routes/wallpaperhub/namespace.ts b/lib/routes/wallpaperhub/namespace.ts index fcdcf824943e38..1e11c19fe03ad0 100644 --- a/lib/routes/wallpaperhub/namespace.ts +++ b/lib/routes/wallpaperhub/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'WallpaperHub', url: 'wallpaperhub.app', + lang: 'en', }; diff --git a/lib/routes/wallstreetcn/calendar.ts b/lib/routes/wallstreetcn/calendar.ts new file mode 100644 index 00000000000000..5eae0a3cbfba1d --- /dev/null +++ b/lib/routes/wallstreetcn/calendar.ts @@ -0,0 +1,90 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/calendar/:section?', + categories: ['finance'], + example: '/wallstreetcn/calendar', + parameters: { section: '`macrodatas` 或 `report`,默认为 `macrodatas`' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['wallstreetcn.com/calendar'], + }, + ], + name: '财经日历', + maintainers: ['TonyRL'], + handler, + url: 'wallstreetcn.com/calendar', +}; + +const rootUrl = 'https://wallstreetcn.com'; + +const MacrodataSuffix = { + CA: 'CA10YR.OTC', + CN: 'USDCNH.OTC', + DE: 'DE30.OTC', + FR: 'FR40.OTC', + // HK: '', + IT: 'EURUSD.OTC', + JP: 'USDJPY.OTC', + UK: 'UK100.OTC', + US: 'DXY.OTC', +}; + +const getMacrodataUrl = (countryId, wscnTicker) => `${rootUrl}/data-analyse/${wscnTicker}/${MacrodataSuffix[countryId]}`; + +async function handler(ctx) { + const { section = 'macrodatas' } = ctx.req.param(); + + const link = `${rootUrl}/calendar`; + const apiRootUrl = section === 'macrodatas' ? 'https://api-one-wscn.awtmt.com' : 'https://api-ddc-wscn.awtmt.com'; + const apiUrl = section === 'macrodatas' ? `${apiRootUrl}/apiv1/finance/macrodatas` : `${apiRootUrl}/finance/report/list`; + + const response = await ofetch(apiUrl, { + query: + section === 'macrodatas' + ? { + start: new Date().setHours(0, 0, 0, 0) / 1000, + end: Math.trunc(new Date().setHours(23, 59, 59, 999) / 1000), + } + : undefined, + }); + + const items = + section === 'macrodatas' + ? response.data.items.map((item) => ({ + title: `${item.country}${item.title}`, + description: `${item.country}${item.title} 重要性: ${'★'.repeat(item.importance)} 今值: ${item.actual || '-'}${item.actual && item.unit} 预期: ${item.forecast || '-'}${item.forecast && item.unit} 前值: ${item.revised || item.previous || '-'}${(item.revised || item.previous) && item.unit}`, + link: item.uri && MacrodataSuffix[item.country_id] && getMacrodataUrl(item.country_id, item.wscn_ticker), + guid: item.id, + pubDate: parseDate(item.public_date, 'X'), + category: item.country, + })) + : // report + response.data.items + .map((item) => Object.fromEntries(response.data.fields.map((field, index) => [field, item[index]]))) + .map((item) => ({ + title: `${item.company_name} ${item.observation_date}`, + description: `${item.code} ${item.company_name} ${item.observation_date} 预期EPS: ${item.eps_estimate === 0 ? '-' : item.eps_estimate} 实际EPS: ${item.reported_eps === 0 ? '-' : item.reported_eps} 差异度: ${item.surprise === 0 || item.surprise === -1 ? '-' : (item.surprise * 100).toFixed(2) + '%'}`, + link, + guid: item.id, + pubDate: parseDate(item.public_date, 'X'), + })); + + return { + title: '财经日历 - 华尔街见闻', + link, + item: items, + itunes_author: '华尔街见闻', + image: 'https://static.wscn.net/wscn/_static/favicon.png', + }; +} diff --git a/lib/routes/wallstreetcn/hot.ts b/lib/routes/wallstreetcn/hot.ts index 9adc7ea9aa924f..5bdaeb9e27debc 100644 --- a/lib/routes/wallstreetcn/hot.ts +++ b/lib/routes/wallstreetcn/hot.ts @@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/hot/:period?', - categories: ['traditional-media'], + categories: ['finance'], example: '/wallstreetcn/hot', parameters: { period: '时期,可选 `day` 即 当日 或 `week` 即 当周,默认为当日' }, features: { @@ -77,6 +77,6 @@ async function handler(ctx) { link: rootUrl, item: items, itunes_author: '华尔街见闻', - image: 'https://static-alpha-wscn.awtmt.com/wscn-static/qrcode.jpg', + image: 'https://static.wscn.net/wscn/_static/favicon.png', }; } diff --git a/lib/routes/wallstreetcn/live.ts b/lib/routes/wallstreetcn/live.ts index 02a7c7f5f8a309..6044d8f93bf902 100644 --- a/lib/routes/wallstreetcn/live.ts +++ b/lib/routes/wallstreetcn/live.ts @@ -19,7 +19,7 @@ const titles = { export const route: Route = { path: '/live/:category?/:score?', - categories: ['traditional-media'], + categories: ['finance'], example: '/wallstreetcn/live', parameters: { category: '快讯分类,默认`global`,见下表', score: '快讯重要度,默认`1`全部快讯,可设置为`2`只看重要的' }, features: { @@ -40,8 +40,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 要闻 | A 股 | 美股 | 港股 | 外汇 | 商品 | 理财 | - | ------ | ------- | -------- | -------- | ----- | --------- | --------- | - | global | a-stock | us-stock | hk-stock | forex | commodity | financing |`, +| ------ | ------- | -------- | -------- | ----- | --------- | --------- | +| global | a-stock | us-stock | hk-stock | forex | commodity | financing |`, }; async function handler(ctx) { diff --git a/lib/routes/wallstreetcn/namespace.ts b/lib/routes/wallstreetcn/namespace.ts index 8e8622e80f5f79..7c9e900ae3924c 100644 --- a/lib/routes/wallstreetcn/namespace.ts +++ b/lib/routes/wallstreetcn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '华尔街见闻', url: 'wallstreetcn.com', + lang: 'zh-CN', }; diff --git a/lib/routes/wallstreetcn/news.ts b/lib/routes/wallstreetcn/news.ts index 30c1ef78161654..7f7893e313e000 100644 --- a/lib/routes/wallstreetcn/news.ts +++ b/lib/routes/wallstreetcn/news.ts @@ -18,28 +18,30 @@ const titles = { }; export const route: Route = { - path: ['/news/:category?', '/:category?'], + path: '/news/:category?', + categories: ['finance'], + example: '/wallstreetcn/news', radar: [ { source: ['wallstreetcn.com/news/:category', 'wallstreetcn.com/'], }, ], - name: 'Unknown', + name: '资讯', maintainers: ['nczitzk'], handler, description: `| id | 分类 | - | ------------ | ---- | - | global | 最新 | - | shares | 股市 | - | bonds | 债市 | - | commodities | 商品 | - | forex | 外汇 | - | enterprise | 公司 | - | asset-manage | 资管 | - | tmt | 科技 | - | estate | 地产 | - | car | 汽车 | - | medicine | 医药 |`, +| ------------ | ---- | +| global | 最新 | +| shares | 股市 | +| bonds | 债市 | +| commodities | 商品 | +| forex | 外汇 | +| enterprise | 公司 | +| asset-manage | 资管 | +| tmt | 科技 | +| estate | 地产 | +| car | 汽车 | +| medicine | 医药 |`, }; async function handler(ctx) { @@ -72,7 +74,14 @@ async function handler(ctx) { url: `${apiRootUrl}/apiv1/content/${item.type === 'live' ? `lives/${item.guid}` : `articles/${item.guid}?extract=0`}`, }); - const data = detailResponse.data.data; + const responseData = detailResponse.data; + + // 处理 { code: 60301, message: '内容不存在或已被删除', data: {} } + if (responseData.code !== 20000) { + return null; + } + + const data = responseData.data; item.title = data.title || data.content_text; item.author = data.source_name ?? data.author.display_name; @@ -93,11 +102,13 @@ async function handler(ctx) { ) ); + items = items.filter((item) => item !== null); + return { title: `华尔街见闻 - 资讯 - ${titles[category]}`, link: currentUrl, item: items, itunes_author: '华尔街见闻', - image: 'https://static-alpha-wscn.awtmt.com/wscn-static/qrcode.jpg', + image: 'https://static.wscn.net/wscn/_static/favicon.png', }; } diff --git a/lib/routes/wanqu/namespace.ts b/lib/routes/wanqu/namespace.ts index 8799f8b7ed073c..438b9b2fba0600 100644 --- a/lib/routes/wanqu/namespace.ts +++ b/lib/routes/wanqu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '湾区日报', url: 'wanqu.co', + lang: 'zh-CN', }; diff --git a/lib/routes/warthunder/namespace.ts b/lib/routes/warthunder/namespace.ts index 65641f3d2b5e84..8608dd188d68ab 100644 --- a/lib/routes/warthunder/namespace.ts +++ b/lib/routes/warthunder/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'War Thunder', url: 'warthunder.com', + lang: 'en', }; diff --git a/lib/routes/warthunder/news.ts b/lib/routes/warthunder/news.ts index cb7432d3177493..f86794648e3717 100644 --- a/lib/routes/warthunder/news.ts +++ b/lib/routes/warthunder/news.ts @@ -2,7 +2,7 @@ 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 { load } from 'cheerio'; import path from 'node:path'; import { art } from '@/utils/render'; @@ -33,20 +33,21 @@ export const route: Route = { handler, url: 'warthunder.com/en/news', description: `News data from [https://warthunder.com/en/news/](https://warthunder.com/en/news/) - The year, month and day provided under UTC time zone are the same as the official website, so please ignore the specific time!!!`, + The \`pubDate\` provided under UTC time zone, so please ignore the specific time!!!`, }; async function handler() { const rootUrl = 'https://warthunder.com/en/news/'; - const response = await got(rootUrl); + const response = await ofetch(rootUrl); - const $ = load(response.data); + const $ = load(response); const pageFace = $('div.showcase__item.widget') - .map((_, item) => { + .toArray() + .map((item) => { item = $(item); - let pubDate = parseDate(item.find('div.widget__content > ul > li.widget-meta__item.widget-meta__item--right').text(), 'D MMMM YYYY'); + let pubDate = parseDate(item.find('div.widget__content > ul > li.widget-meta__item.widget-meta__item--right').text(), 'D MMMM YYYY', 'en'); pubDate = timezone(pubDate, 0); const category = []; if (item.find('div.widget__pin').length !== 0) { @@ -68,8 +69,7 @@ async function handler() { }), category, }; - }) - .get(); + }); return { title: 'War Thunder News', diff --git a/lib/routes/washingtonpost/app.ts b/lib/routes/washingtonpost/app.ts new file mode 100644 index 00000000000000..40f4d3de3f7c6a --- /dev/null +++ b/lib/routes/washingtonpost/app.ts @@ -0,0 +1,118 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +import { FetchError } from 'ofetch'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; + +export const route: Route = { + path: '/app/:category{.+}?', + categories: ['traditional-media'], + example: '/washingtonpost/app/national', + parameters: { + category: 'Category from the path of the URL of the corresponding site, see below', + }, + features: { + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'App', + maintainers: ['quiniapiezoelectricity'], + radar: [ + { + source: ['www.washingtonpost.com/:category'], + target: '/app/:category', + }, + ], + handler, + description: `::: tip +For example, the category for https://www.washingtonpost.com/national/investigations would be /national/investigations. +:::`, +}; + +function handleDuplicates(array) { + const objects = {}; + for (const obj of array) { + objects[obj.id] = objects[obj.id] ? Object.assign(objects[obj.id], obj) : obj; + } + return Object.values(objects); +} + +async function handler(ctx) { + const category = ctx.req.param('category') ?? ''; + const __dirname = getCurrentPath(import.meta.url); + const headers = { + Accept: '*/*', + Connection: 'keep-alive', + 'User-Agent': 'Classic/6.70.0', + }; + dayjs.extend(utc); + dayjs.extend(timezone); + dayjs.extend(advancedFormat); + art.defaults.imports.dayjs = dayjs; + + const url = `https://jsonapp1.washingtonpost.com/fusion_prod/v2/${category}`; + const response = await got.get(url, { headers }); + const title = response.data.tracking.page_title.includes('Washington Post') ? response.data.tracking.page_title : `The Washington Post - ${response.data.tracking.page_title}`; + const link = 'https://washingtonpost.com' + response.data.tracking.page_path; + const mains = response.data.regions[0].items.filter((item) => item.items); + const list = mains.flatMap((main) => + main.items[0].items + .filter((item) => item.is_from_feed === true) + .map((item) => { + const object = { + id: item.id, + title: item.headline.text, + link: item.link.url, + pubDate: item.link.display_date, + updated: item.link.last_modified, + }; + if (item.blurbs?.items[0]?.text) { + object.description = item.blurbs?.items[0]?.text; + } + return object; + }) + ); + const feed = handleDuplicates(list); + const items = await Promise.all( + feed.map((item) => + cache.tryGet(item.link, async () => { + let response; + try { + response = await got(`https://rainbowapi-a.wpdigital.net/rainbow-data-service/rainbow/content-by-url.json?followLinks=false&url=${item.link}`, { headers }); + } catch (error) { + if (error instanceof FetchError && error.statusCode === 415) { + // Interactive or podcast contents will return 415 Unsupported Media Type. Keep calm and carry on. + return item; + } else { + throw error; + } + } + item.title = response.data.title ?? item.title; + item.author = + response.data.items + .filter((entry) => entry.type === 'byline') + ?.flatMap((entry) => entry.authors.map((author) => author.name)) + ?.join(', ') ?? ''; + item.description = art(path.join(__dirname, 'templates/description.art'), { + content: response.data.items, + }); + return item; + }) + ) + ); + + return { + title, + link, + item: items, + }; +} diff --git a/lib/routes/washingtonpost/namespace.ts b/lib/routes/washingtonpost/namespace.ts new file mode 100644 index 00000000000000..0ffdf8d3ae072c --- /dev/null +++ b/lib/routes/washingtonpost/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'The Washington Post', + url: 'www.washingtonpost.com', + lang: 'en', +}; diff --git a/lib/routes/washingtonpost/templates/description.art b/lib/routes/washingtonpost/templates/description.art new file mode 100644 index 00000000000000..fc7382329a7edb --- /dev/null +++ b/lib/routes/washingtonpost/templates/description.art @@ -0,0 +1,59 @@ +{{ if content }} +{{ each content }} + {{ if $value.type == 'title' && $value.subtype != 'h1'}} + <{{ if $value.subtype }}{{ $value.subtype }}{{ else }}h2{{ /if }}> + {{ if $value.mime == 'text/html' }}{{@ $value.content }}{{ /if }} + {{ if $value.mime == 'text/plain' }}{{ $value.content }}{{ /if }} + </h{{ if $value.subhead_level }}{{ $value.subhead_level }}{{ else }}1{{ /if }}> + {{ /if }} + {{ if $value.type == 'sanitized_html' }} + {{ if $value.subtype == 'paragraph' }}<p>{{ else if $value.subtype == 'subhead' }}<h{{ if $value.subhead_level }}{{ $value.subhead_level }}{{ else }}4{{ /if }}>{{ /if }} + {{ if $value.mime == 'text/html' }}{{@ $value.content }}{{ /if }} + {{ if $value.mime == 'text/plain' }}{{ $value.content }}{{ /if }} + {{ if $value.oembed }}{{@ $value.oembed }}{{ /if }} + {{ if $value.subtype == 'paragraph' }}</p>{{ else if $value.subtype == 'subhead' }}</h{{ if $value.subhead_level }}{{ $value.subhead_level }}{{ else }}4{{ /if }}>{{ /if }} + {{ /if }} + {{ if $value.type == 'deck' }} + <blockquote><p> + {{ if $value.mime == 'text/html' }}{{@ $value.content }}{{ /if }} + {{ if $value.mime == 'text/plain' }}{{ $value.content }}{{ /if }} + </p></blockquote> + {{ /if }} + {{ if $value.type == 'image' }} + <figure><img src="{{ $value.imageURL }}" alt="{{ $value.blurb }}"><figcaption>{{ $value.fullcaption }}</figcaption></figure> + {{ /if }} + {{ if $value.type == 'video' }} + {{ if $value.content && $value.content.html }}{{@ $value.content.html }} + {{ else if $value.mediaURL }} + <figure> + <video controls + {{ if $value.imageURL }}poster="{{ $value.imageURL }}"{{ /if }} + > + <source src="{{ $value.mediaURL }}"> + </video> + {{ if $value.fullcaption }}<figcaption>{{ $value.fullcaption }}</figcaption>{{ /if }} + </figure> + {{ /if }} + {{ /if }} + {{ if $value.type == 'list' }} + {{ if $value.subtype == 'ordered' }}<ol>{{ else }}<ul>{{ /if }} + {{ if $value.mime == 'text/html' }}{{ each $value.content }}<li>{{@ $value }}</li>{{ /each }}{{ /if }} + {{ if $value.mime == 'text/plain' }}{{ each $value.content }}<li>{{ $value }}</li>{{ /each }}{{ /if }} + {{ if $value.subtype == 'ordered' }}</ol>{{ else }}</ul>{{ /if }} + {{ /if }} + {{ if $value.type == 'divider' }}<br><hr><br>{{ /if }} + {{ if $value.type == 'byline' }} + {{ if $value.subtype == 'live-update' || $value.subtype == 'live-reporter-insight' }} + <p><i> + {{ if $value.mime == 'text/html' }}{{@ $value.content }}{{ /if }} + {{ if $value.mime == 'text/plain' }}{{ $value.content }}{{ /if }} + </i></p> + {{ /if}} + {{ /if }} + {{ if $value.type == 'date' }} + {{ if $value.subtype == 'live-update'}} + {{ if $value.content }}{{ $imports.dayjs.tz($value.content,"America/New_York").locale('en').format('dddd, MMMM D, YYYY h:mm A z') }}{{ /if }} + {{ /if }} + {{ /if }} +{{ /each }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/watasuke/blog.ts b/lib/routes/watasuke/blog.ts deleted file mode 100644 index 2d85f507df63e6..00000000000000 --- a/lib/routes/watasuke/blog.ts +++ /dev/null @@ -1,42 +0,0 @@ -import got from '@/utils/got'; -import { parseDate } from '@/utils/parse-date'; -import markdownit from 'markdown-it'; - -import { Route } from '@/types'; - -const handler = async () => { - const baseUrl = 'https://watasuke.net'; - const apiUrl = `${baseUrl}/page-data/blog/page-data.json`; - const response = await got(apiUrl); - const md = markdownit({ linkify: true, breaks: true }); - const articles = response.data.result.data.allArticles.nodes.map((node) => ({ - title: node.title, - description: md.render(node.body), - pubDate: parseDate(node.published_at), - updated: parseDate(node.updated_at), - link: `${baseUrl}/blog/article/${node.slug}/`, - category: node.tags.map((tag) => tag.name), - })); - - return { - title: 'ブログ - わたすけのへや' || baseUrl, - link: `${baseUrl}/blog/`, - item: articles, - language: 'ja', - icon: `${baseUrl}/icons/icon-512x512.png`, - logo: `${baseUrl}/icons/icon-512x512.png`, - }; -}; -export const route: Route = { - path: '/blog', - example: '/watasuke/blog', - radar: [ - { - source: ['watasuke.net/blog/', 'watasuke.net/'], - }, - ], - name: 'Blog', - maintainers: ['honahuku'], - handler, - url: 'watasuke.net/blog/', -}; diff --git a/lib/routes/wchscu/namespace.ts b/lib/routes/wchscu/namespace.ts new file mode 100644 index 00000000000000..5672aa3c039432 --- /dev/null +++ b/lib/routes/wchscu/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '华西医院', + url: 'www.wchscu.cn', + lang: 'zh-CN', +}; diff --git a/lib/routes/wchscu/recruit.ts b/lib/routes/wchscu/recruit.ts new file mode 100644 index 00000000000000..da7dc6b6570a32 --- /dev/null +++ b/lib/routes/wchscu/recruit.ts @@ -0,0 +1,73 @@ +import { Route } from '@/types'; + +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +const handler = async () => { + const rootUrl = 'https:///www.wchscu.cn'; + const currentUrl = 'https://www.wchscu.cn/public/notice/recruit'; + const { data: response } = await got(currentUrl); + + const $ = load(response); + const list = $('div#datalist div.list div.item') + .toArray() + .map((item) => { + item = $(item); + return { + title: item.find('span.s1').text(), + pubDate: parseDate(item.find('span.s2').text()), + link: new URL(item.find('a').prop('href'), rootUrl).href, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const { data: response } = await got(item.link); + const $ = load(response); + const newLocal = $('div.xxy3 .content'); + // 选择类名为“comment-body”的第一个元素 + item.description = newLocal.html(); + + // 上面每个列表项的每个属性都在此重用, + // 并增加了一个新属性“description” + return item; + }) + ) + ); + + return { + title: $('title').text(), + link: currentUrl, + item: items, + allowEmpty: true, + }; +}; + +export const route: Route = { + name: '招聘公告', + path: '/recruit', + example: '/wchscu/recruit', + url: 'www.wchscu.cn', + maintainers: ['ViggoC'], + categories: ['other'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + + radar: [ + { + source: ['www.wchscu.cn/public/notice/recruit'], + }, + ], + handler, +}; diff --git a/lib/routes/wdc/namespace.ts b/lib/routes/wdc/namespace.ts index bc0a53465f63f1..558f1ea6a55a27 100644 --- a/lib/routes/wdc/namespace.ts +++ b/lib/routes/wdc/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Western Digital', url: 'support.wdc.com', + lang: 'en', }; diff --git a/lib/routes/web/namespace.ts b/lib/routes/web/namespace.ts index 97a7dd7cbbebd5..34180b9eb25b08 100644 --- a/lib/routes/web/namespace.ts +++ b/lib/routes/web/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'web.dev', url: 'web.dev', + lang: 'en', }; diff --git a/lib/routes/web/series.ts b/lib/routes/web/series.ts index f6771b8fe7e98e..3536b41a16d419 100644 --- a/lib/routes/web/series.ts +++ b/lib/routes/web/series.ts @@ -15,7 +15,7 @@ export const route: Route = { name: 'Series', maintainers: ['KarasuShin'], handler, - description: `:::tip + description: `::: tip The \`seriesName\` can be extracted from the Series page URL: \`https://web.dev/series/:seriesName\` :::`, }; diff --git a/lib/routes/web3caff/namespace.ts b/lib/routes/web3caff/namespace.ts index e64d1f16383752..8e1e538741a28b 100644 --- a/lib/routes/web3caff/namespace.ts +++ b/lib/routes/web3caff/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Web3Caff', url: 'web3caff.com', + lang: 'en', }; diff --git a/lib/routes/webcatalog/changelog.ts b/lib/routes/webcatalog/changelog.ts new file mode 100644 index 00000000000000..c5b9788115ab6b --- /dev/null +++ b/lib/routes/webcatalog/changelog.ts @@ -0,0 +1,59 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/changelog', + categories: ['program-update'], + example: '/webcatalog/changelog', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['desktop.webcatalog.io/:lang/changelog'], + }, + ], + name: 'Changelog', + maintainers: ['Tsuyumi25'], + handler, + url: 'desktop.webcatalog.io/en/changelog', +}; + +async function handler() { + const url = 'https://desktop.webcatalog.io/en/changelog'; + const response = await ofetch(url); + const $ = load(response); + + // remove What's new + $('.container article div.mb-20').remove(); + const items = $('.container article') + .html() + ?.split('<hr>') + ?.map((section) => { + const $section = load(section); + const month = $section('h1').remove().text(); + const title = $section('h2').first().remove().text(); + return { + title: `${month} - ${title}`, + description: $section.html(), + link: url, + pubDate: parseDate(month), + guid: `webcatalog-${month}-${title}`, + }; + }); + + return { + title: 'WebCatalog Changelog', + link: url, + item: items, + language: 'en', + }; +} diff --git a/lib/routes/webcatalog/namespace.ts b/lib/routes/webcatalog/namespace.ts new file mode 100644 index 00000000000000..b9c4fb8bde1dcb --- /dev/null +++ b/lib/routes/webcatalog/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'WebCatalog', + url: 'desktop.webcatalog.io', + lang: 'en', +}; diff --git a/lib/routes/wechat/ershcimi.ts b/lib/routes/wechat/ershcimi.ts index 6b72dd125005e0..cbf89218c1a5ca 100644 --- a/lib/routes/wechat/ershcimi.ts +++ b/lib/routes/wechat/ershcimi.ts @@ -31,7 +31,8 @@ async function handler(ctx) { const response = await got(url); const $ = load(response.data); const items = $('.weui_media_box') - .map((_, ele) => { + .toArray() + .map((ele) => { const $item = load(ele); const link = $item('.weui_media_title a').attr('href'); return { @@ -40,15 +41,14 @@ async function handler(ctx) { link, pubDate: timezone(parseDate($item('.weui_media_extra_info').attr('title')), +8), }; - }) - .get(); - - await Promise.all(items.map((item) => finishArticleItem(item))); + }); return { title: `微信公众号 - ${$('span.name').text()}`, link: url, description: $('div.Profile-sideColumnItemValue').text(), - item: items, + item: await Promise.all(items.map((item) => finishArticleItem(item))).catch((error) => { + throw error; + }), }; } diff --git a/lib/routes/wechat/namespace.ts b/lib/routes/wechat/namespace.ts index 8562552c75718f..38987cd0480155 100644 --- a/lib/routes/wechat/namespace.ts +++ b/lib/routes/wechat/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '微信小程序', url: 'posts.careerengine.us', - description: `:::tip + description: `::: tip 公众号直接抓取困难,故目前提供几种间接抓取方案,请自行选择 :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/wechat/tgchannel.ts b/lib/routes/wechat/tgchannel.ts index f97eaa0eb5187d..f15f8840835f7d 100644 --- a/lib/routes/wechat/tgchannel.ts +++ b/lib/routes/wechat/tgchannel.ts @@ -20,18 +20,18 @@ export const route: Route = { maintainers: ['LogicJake', 'Rongronggg9'], handler, description: `| 搜索查询类型 | 将使用的搜索关键字 | 适用于 | - | :----------: | :----------------: | :-------------------------: | - | \`0\` | (禁用搜索) | 所有情况 (默认) | - | \`1\` | 公众号全名 | 未启用 efb-patch-middleware | - | \`2\` | #公众号全名 | 已启用 efb-patch-middleware | +| :----------: | :----------------: | :-------------------------: | +| \`0\` | (禁用搜索) | 所有情况 (默认) | +| \`1\` | 公众号全名 | 未启用 efb-patch-middleware | +| \`2\` | #公众号全名 | 已启用 efb-patch-middleware | - :::tip +::: tip 启用搜索有助于在订阅了过多公众号的频道里有效筛选,不易因为大量公众号同时推送导致一些公众号消息被遗漏,但必须正确选择搜索查询类型,否则会搜索失败。 - ::: +::: - :::warning +::: warning 该方法需要通过 efb 进行频道绑定,具体操作见 [https://github.com/DIYgod/RSSHub/issues/2172](https://github.com/DIYgod/RSSHub/issues/2172) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/weekendhk/namespace.ts b/lib/routes/weekendhk/namespace.ts index 2ef177506ad7a9..29d0d9f495fb23 100644 --- a/lib/routes/weekendhk/namespace.ts +++ b/lib/routes/weekendhk/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '新假期周刊', url: 'weekendhk.com', + lang: 'zh-HK', }; diff --git a/lib/routes/weibo/friends.ts b/lib/routes/weibo/friends.ts index 3cb5cbe34752ba..119615649f63d4 100644 --- a/lib/routes/weibo/friends.ts +++ b/lib/routes/weibo/friends.ts @@ -16,6 +16,7 @@ export const route: Route = { requireConfig: [ { name: 'WEIBO_COOKIES', + optional: true, description: '', }, ], @@ -35,13 +36,13 @@ export const route: Route = { maintainers: ['CaoMeiYouRen'], handler, url: 'weibo.com/', - description: `:::warning + description: `::: warning 此方案必须使用用户\`Cookie\`进行抓取 因微博 cookies 的过期与更新方案未经验证,部署一次 Cookie 的有效时长未知 微博用户 Cookie 的配置可参照部署文档 - :::`, +:::`, }; async function handler(ctx) { @@ -140,7 +141,7 @@ async function handler(ctx) { } if (displayComments === '1') { - description = await weiboUtils.formatComments(ctx, description, item); + description = await weiboUtils.formatComments(ctx, description, item, '0'); } if (displayArticle === '1') { diff --git a/lib/routes/weibo/group.ts b/lib/routes/weibo/group.ts index ad00e817d07b88..cfbd988fb1df35 100644 --- a/lib/routes/weibo/group.ts +++ b/lib/routes/weibo/group.ts @@ -16,6 +16,7 @@ export const route: Route = { requireConfig: [ { name: 'WEIBO_COOKIES', + optional: true, description: '', }, ], @@ -28,13 +29,13 @@ export const route: Route = { name: '自定义分组', maintainers: ['monologconnor', 'Rongronggg9'], handler, - description: `:::warning + description: `::: warning 由于微博官方未提供自定义分组相关 api, 此方案必须使用用户\`Cookie\`进行抓取 因微博 cookies 的过期与更新方案未经验证,部署一次 Cookie 的有效时长未知 微博用户 Cookie 的配置可参照部署文档 - :::`, +:::`, }; async function handler(ctx) { @@ -95,7 +96,7 @@ async function handler(ctx) { } if (displayComments === '1') { - description = await weiboUtils.formatComments(ctx, description, item); + description = await weiboUtils.formatComments(ctx, description, item, '0'); } if (displayArticle === '1') { diff --git a/lib/routes/weibo/keyword.ts b/lib/routes/weibo/keyword.ts index df8b1b2cd27bab..27baeea1aa6eb8 100644 --- a/lib/routes/weibo/keyword.ts +++ b/lib/routes/weibo/keyword.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import querystring from 'querystring'; import got from '@/utils/got'; @@ -9,8 +9,9 @@ import { config } from '@/config'; export const route: Route = { path: '/keyword/:keyword/:routeParams?', - categories: ['social-media'], - example: '/weibo/keyword/DIYgod', + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, + example: '/weibo/keyword/RSSHub', parameters: { keyword: '你想订阅的微博关键词', routeParams: '额外参数;请参阅上面的说明和表格' }, features: { requireConfig: false, diff --git a/lib/routes/weibo/namespace.ts b/lib/routes/weibo/namespace.ts index 2b09d4bb0250d5..f1d07f3870f654 100644 --- a/lib/routes/weibo/namespace.ts +++ b/lib/routes/weibo/namespace.ts @@ -1,9 +1,9 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: '微博绿洲', + name: '微博', url: 'weibo.com', - description: `:::warning + description: `::: warning 微博会针对请求的来源地区返回不同的结果。\ 一个已知的例子为:部分视频因未知原因仅限中国大陆境内访问 (CDN 域名为 \`locallimit.us.sinaimg.cn\` 而非 \`f.video.weibocdn.com\`)。若一条微博含有这种视频且 RSSHub 实例部署在境外,抓取到的微博可能不含视频。将 RSSHub 部署在境内有助于抓取这种视频,但阅读器也必须处于境内网络环境以加载视频。 ::: @@ -30,6 +30,8 @@ export const namespace: Namespace = { | showEmojiInDescription | 是否展示正文中的微博表情,关闭则替换为 \`[表情名]\` | 0/1/true/false | true | | showLinkIconInDescription | 是否展示正文中的链接图标 | 0/1/true/false | true | | preferMobileLink | 是否使用移动版链接(默认使用 PC 版) | 0/1/true/false | false | +| showRetweeted | 是否显示转发的微博 | 0/1/true/false | true | +| showBloggerIcons | 是否显示评论中博主的标志,只在显示热门评论时有效 | 0/1/true/false | false | 指定更多与默认值不同的参数选项可以改善 RSS 的可读性,如 @@ -38,4 +40,5 @@ export const namespace: Namespace = { 的效果为 <img loading="lazy" src="/img/readable-weibo.png" alt="微博小秘书的可读微博 RSS" />`, + lang: 'zh-CN', }; diff --git a/lib/routes/weibo/oasis/user.ts b/lib/routes/weibo/oasis/user.ts index b413232d8dfcfe..e277be3864cd39 100644 --- a/lib/routes/weibo/oasis/user.ts +++ b/lib/routes/weibo/oasis/user.ts @@ -21,7 +21,7 @@ export const route: Route = { target: '/user/:uid', }, ], - name: '用户', + name: '绿洲用户', maintainers: ['kt286'], handler, }; diff --git a/lib/routes/weibo/search/hot.ts b/lib/routes/weibo/search/hot.ts index 3656a4f718a55c..833baa82be2e78 100644 --- a/lib/routes/weibo/search/hot.ts +++ b/lib/routes/weibo/search/hot.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); @@ -16,9 +16,18 @@ let fullpic = 'false'; export const route: Route = { path: '/search/hot/:fulltext?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/weibo/search/hot', - parameters: { fulltext: 'N' }, + parameters: { + fulltext: { + description: ` +- 使用\`/weibo/search/hot\`可以获取热搜条目列表; +- 使用\`/weibo/search/hot/fulltext\`可以进一步获取热搜条目下的摘要信息(不含图片视频); +- 使用\`/weibo/search/hot/fulltext?pic=true\`可以获取图片缩略(但需要配合额外的手段,例如浏览器上的 Header Editor 等来修改 referer 参数为\`https://weibo.com\`,以规避微博的外链限制,否则图片无法显示。) +- 使用\`/weibo/search/hot/fulltext?pic=true&fullpic=true\`可以获取 Original 图片(但需要配合额外的手段,例如浏览器上的 Header Editor 等来修改 referer 参数为\`https://weibo.com\`,以规避微博的外链限制,否则图片无法显示。)`, + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -36,10 +45,6 @@ export const route: Route = { maintainers: ['xyqfer', 'shinemoon'], handler, url: 's.weibo.com/top/summary', - description: `- 使用\`/weibo/search/hot\`可以获取热搜条目列表; -- 使用\`/weibo/search/hot/fulltext\`可以进一步获取热搜条目下的摘要信息(不含图片视频); -- 使用\`/weibo/search/hot/fulltext?pic=true\`可以获取图片缩略(但需要配合额外的手段,例如浏览器上的 Header Editor 等来修改 referer 参数为\`https://weibo.com\`,以规避微博的外链限制,否则图片无法显示。) -- 使用\`/weibo/search/hot/fulltext?pic=true&fullpic=true\`可以获取 Original 图片(但需要配合额外的手段,例如浏览器上的 Header Editor 等来修改 referer 参数为\`https://weibo.com\`,以规避微博的外链限制,否则图片无法显示。)`, }; async function handler(ctx) { diff --git a/lib/routes/weibo/super-index.ts b/lib/routes/weibo/super-index.ts index f4da6960f39cd7..c414d9ce5c4940 100644 --- a/lib/routes/weibo/super-index.ts +++ b/lib/routes/weibo/super-index.ts @@ -37,11 +37,15 @@ export const route: Route = { | feed | 最新评论 |`, }; +interface Card { + card_group?: Card[]; +} + async function handler(ctx) { const id = ctx.req.param('id'); const type = ctx.req.param('type') ?? 'feed'; - const containerData = await cache.tryGet( + const containerData = (await cache.tryGet( `weibo:super_index:container:${id}:${type}`, async () => { const _r = await got('https://m.weibo.cn/api/container/getIndex', { @@ -61,27 +65,35 @@ async function handler(ctx) { }, config.cache.routeExpire, false - ); + )) as { + cards?: Card[]; + pageInfo?: { + page_title: string; + }; + }; const resultItems = []; - for (const card of containerData.cards) { + function handleCard(ctx, card, resultItems) { + if (card.card_type === '9' && 'mblog' in card) { + const formatExtended = weiboUtils.formatExtended(ctx, card.mblog, undefined); + resultItems.push(formatExtended); + } + } + for (const card of containerData?.cards ?? []) { + handleCard(ctx, card, resultItems); if (!('card_group' in card)) { continue; } - for (const mblogCard of card.card_group) { - if (mblogCard.card_type === '9' && 'mblog' in mblogCard) { - const mblog = mblogCard.mblog; - const formatExtended = weiboUtils.formatExtended(ctx, mblog); - resultItems.push(formatExtended); - } + for (const mblogCard of card.card_group!) { + handleCard(ctx, mblogCard, resultItems); } } return weiboUtils.sinaimgTvax({ - title: `微博超话 - ${containerData.pageInfo.page_title}`, + title: `微博超话 - ${containerData?.pageInfo?.page_title}`, link: `https://weibo.com/p/${id}/super_index`, - description: `#${containerData.pageInfo.page_title}# 的超话`, + description: `#${containerData?.pageInfo?.page_title}# 的超话`, item: resultItems, }); } diff --git a/lib/routes/weibo/timeline.ts b/lib/routes/weibo/timeline.ts index 1acdacc594cdef..5303bc23afd233 100644 --- a/lib/routes/weibo/timeline.ts +++ b/lib/routes/weibo/timeline.ts @@ -32,11 +32,11 @@ export const route: Route = { name: '个人时间线', maintainers: ['zytomorrow', 'DIYgod', 'Rongronggg9'], handler, - description: `:::warning + description: `::: warning 需要对应用户打开页面进行授权生成 token 才能生成内容 自部署需要申请并配置微博 key,具体见部署文档 - :::`, +:::`, }; async function handler(ctx) { @@ -47,6 +47,7 @@ async function handler(ctx) { let displayVideo = '1'; let displayArticle = '0'; let displayComments = '0'; + let showBloggerIcons = '0'; if (routeParams) { if (routeParams === '1' || routeParams === '0') { displayVideo = routeParams; @@ -55,6 +56,7 @@ async function handler(ctx) { displayVideo = fallback(undefined, queryToBoolean(routeParams.displayVideo), true) ? '1' : '0'; displayArticle = fallback(undefined, queryToBoolean(routeParams.displayArticle), false) ? '1' : '0'; displayComments = fallback(undefined, queryToBoolean(routeParams.displayComments), false) ? '1' : '0'; + showBloggerIcons = fallback(undefined, queryToBoolean(routeParams.showBloggerIcons), false) ? '1' : '0'; } } @@ -95,7 +97,8 @@ async function handler(ctx) { ctx.set({ 'Cache-Control': 'no-cache', }); - ctx.redirect(`https://api.weibo.com/oauth2/authorize?client_id=${app_key}&redirect_uri=${redirect_url}${routeParams ? `&state=${routeParams}` : ''}`); + ctx.set('redirect', `https://api.weibo.com/oauth2/authorize?client_id=${app_key}&redirect_uri=${redirect_url}${routeParams ? `&state=${routeParams}` : ''}`); + return; } const resultItem = await Promise.all( response.statuses.map(async (item) => { @@ -132,7 +135,7 @@ async function handler(ctx) { // 评论的处理 if (displayComments === '1') { - description = await weiboUtils.formatComments(ctx, description, item); + description = await weiboUtils.formatComments(ctx, description, item, showBloggerIcons); } // 文章的处理 @@ -183,6 +186,6 @@ async function handler(ctx) { ctx.set({ 'Cache-Control': 'no-cache', }); - ctx.redirect(`https://api.weibo.com/oauth2/authorize?client_id=${app_key}&redirect_uri=${redirect_url}${routeParams ? `&state=${feature}/${routeParams.replaceAll('&', '%26')}` : ''}`); + ctx.set('redirect', `https://api.weibo.com/oauth2/authorize?client_id=${app_key}&redirect_uri=${redirect_url}${routeParams ? `&state=${feature}/${routeParams.replaceAll('&', '%26')}` : ''}`); } } diff --git a/lib/routes/weibo/user-bookmarks.ts b/lib/routes/weibo/user-bookmarks.ts new file mode 100644 index 00000000000000..8ce63c4b63512e --- /dev/null +++ b/lib/routes/weibo/user-bookmarks.ts @@ -0,0 +1,191 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import querystring from 'querystring'; +import got from '@/utils/got'; +import { config } from '@/config'; +import weiboUtils from './utils'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; +import { fallback, queryToBoolean } from '@/utils/readable-social'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; + +export const route: Route = { + path: '/user_bookmarks/:uid/:routeParams?', + categories: ['social-media'], + example: '/weibo/user_bookmarks/1195230310', + parameters: { uid: '用户 id, 博主主页打开控制台执行 `$CONFIG.oid` 获取', routeParams: '额外参数;请参阅上面的说明和表格;特别地,当 `routeParams=1` 时开启微博视频显示' }, + features: { + requireConfig: [ + { + name: 'WEIBO_COOKIES', + optional: true, + description: '', + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['weibo.com/'], + target: '/user_bookmarks/:uid', + }, + ], + name: '用户收藏动态', + maintainers: ['cztchoice'], + handler, + url: 'weibo.com/', + description: `::: warning + 此方案必须使用用户\`Cookie\`进行抓取,只可以获取本人的收藏动态 + + 因微博 cookies 的过期与更新方案未经验证,部署一次 Cookie 的有效时长未知 + + 微博用户 Cookie 的配置可参照部署文档 +:::`, +}; + +async function handler(ctx) { + if (!config.weibo.cookies) { + throw new ConfigNotFoundError('Weibo user bookmarks is not available due to the absense of [Weibo Cookies]. Check <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config tutorial</a>'); + } + + let displayVideo = '1'; + let displayArticle = '0'; + let displayComments = '0'; + if (ctx.req.param('routeParams')) { + if (ctx.req.param('routeParams') === '1' || ctx.req.param('routeParams') === '0') { + displayVideo = ctx.req.param('routeParams'); + } else { + const routeParams = querystring.parse(ctx.req.param('routeParams')); + displayVideo = fallback(undefined, queryToBoolean(routeParams.displayVideo), true) ? '1' : '0'; + displayArticle = fallback(undefined, queryToBoolean(routeParams.displayArticle), false) ? '1' : '0'; + displayComments = fallback(undefined, queryToBoolean(routeParams.displayComments), false) ? '1' : '0'; + } + } + + const uid = await cache.tryGet( + 'weibo:user_bookmarks:login-user', + async () => { + const _r = await got({ + method: 'get', + url: 'https://m.weibo.cn/api/config', + headers: { + Referer: 'https://m.weibo.cn/', + 'MWeibo-Pwa': 1, + 'X-Requested-With': 'XMLHttpRequest', + Cookie: config.weibo.cookies, + }, + }); + return _r.data.data.uid; + }, + config.cache.routeExpire, + false + ); + + const containerData = await cache.tryGet( + `weibo:user_bookmarks:index:${uid}`, + async () => { + const _r = await got({ + method: 'get', + url: `https://m.weibo.cn/api/container/getIndex?type=uid&value=${uid}`, + headers: { + Referer: `https://m.weibo.cn/u/${uid}`, + 'MWeibo-Pwa': 1, + 'X-Requested-With': 'XMLHttpRequest', + Cookie: config.weibo.cookies, + }, + }); + return _r.data; + }, + config.cache.routeExpire, + false + ); + + const name = containerData.data.userInfo.screen_name; + const title = `${name} 的 最新收藏时间线`; + const schemeString = containerData.data.scheme; + const url = new URL(`http://example.com/${schemeString.replace('://', '?')}`); + const params = new URLSearchParams(url.search); + const bookmarkContainerId = params.get('lfid'); + + const cards = await cache.tryGet( + `weibo:user_bookmarks:cards:${uid}`, + async () => { + const _r = await got({ + method: 'get', + url: `https://m.weibo.cn/api/container/getIndex?containerid=${bookmarkContainerId}&openApp=0`, + headers: { + Referer: 'https://m.weibo.cn/', + 'MWeibo-Pwa': 1, + 'X-Requested-With': 'XMLHttpRequest', + Cookie: config.weibo.cookies, + }, + }); + return _r.data.data.cards; + }, + config.cache.routeExpire, + false + ); + const resultItems = await Promise.all( + cards + .filter((item) => item.mblog) + .map(async (item) => { + const key = 'weibo:user_bookmarks:' + item.mblog.bid; + const data = await cache.tryGet(key, () => weiboUtils.getShowData(uid, item.mblog.bid)); + + if (data?.text) { + item.mblog.text = data.text; + item.mblog.created_at = parseDate(data.created_at); + item.mblog.pics = data.pics; + if (item.mblog.retweeted_status && data.retweeted_status) { + item.mblog.retweeted_status.created_at = data.retweeted_status.created_at; + } + } else { + item.mblog.created_at = timezone(item.mblog.created_at, +8); + } + + // 转发的长微博处理 + const retweet = item.mblog.retweeted_status; + if (retweet?.isLongText) { + // TODO: unify cache key and ... + const retweetData = await cache.tryGet(`weibo:retweeted:${retweet.user.id}:${retweet.bid}`, () => weiboUtils.getShowData(retweet.user.id, retweet.bid)); + if (retweetData !== undefined && retweetData.text) { + item.mblog.retweeted_status.text = retweetData.text; + } + } + const formatExtended = weiboUtils.formatExtended(ctx, item.mblog, uid); + let description = formatExtended.description; + + // 视频的处理 + if (displayVideo === '1') { + // 含被转发微博时需要从被转发微博中获取视频 + description = item.mblog.retweeted_status ? weiboUtils.formatVideo(description, item.mblog.retweeted_status) : weiboUtils.formatVideo(description, item.mblog); + } + + // 评论的处理 + if (displayComments === '1') { + description = await weiboUtils.formatComments(ctx, description, item.mblog, '0'); + } + + // 文章的处理 + if (displayArticle === '1') { + // 含被转发微博时需要从被转发微博中获取文章 + description = await (item.mblog.retweeted_status ? weiboUtils.formatArticle(ctx, description, item.mblog.retweeted_status) : weiboUtils.formatArticle(ctx, description, item.mblog)); + } + + return { + ...formatExtended, + description, + }; + }) + ); + + return weiboUtils.sinaimgTvax({ + title, + link: 'https://weibo.com', + item: resultItems, + }); +} diff --git a/lib/routes/weibo/user.ts b/lib/routes/weibo/user.ts index 153baaad4fe494..0c4ad18976fd5b 100644 --- a/lib/routes/weibo/user.ts +++ b/lib/routes/weibo/user.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import querystring from 'querystring'; import got from '@/utils/got'; @@ -10,11 +10,18 @@ import { fallback, queryToBoolean } from '@/utils/readable-social'; export const route: Route = { path: '/user/:uid/:routeParams?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.SocialMedia, example: '/weibo/user/1195230310', parameters: { uid: '用户 id, 博主主页打开控制台执行 `$CONFIG.oid` 获取', routeParams: '额外参数;请参阅上面的说明和表格;特别地,当 `routeParams=1` 时开启微博视频显示' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'WEIBO_COOKIES', + optional: true, + description: '', + }, + ], requirePuppeteer: false, antiCrawler: true, supportBT: false, @@ -26,17 +33,21 @@ export const route: Route = { source: ['m.weibo.cn/u/:uid', 'm.weibo.cn/profile/:uid'], target: '/user/:uid', }, + { + source: ['weibo.com/u/:uid'], + target: '/user/:uid', + }, + { + source: ['www.weibo.com/u/:uid'], + target: '/user/:uid', + }, ], name: '博主', - maintainers: ['DIYgod', 'iplusx', 'Rongronggg9'], + maintainers: ['DIYgod', 'iplusx', 'Rongronggg9', 'Konano'], handler, - description: `:::warning - 部分博主仅登录可见,未提供 Cookie 的情况下不支持订阅,可以通过打开 \`https://m.weibo.cn/u/:uid\` 验证。如需要订阅该部分博主,可配置 Cookie 后订阅。 - - 未提供 Cookie 的情况下偶尔会触发反爬限制,提供 Cookie 可缓解该情况。 - - 微博用户 Cookie 的配置可参照部署文档 - :::`, + description: `::: warning + 部分博主仅登录可见,未提供 Cookie 的情况下不支持订阅,可以通过打开 \`https://m.weibo.cn/u/:uid\` 验证 +:::`, }; async function handler(ctx) { @@ -44,6 +55,8 @@ async function handler(ctx) { let displayVideo = '1'; let displayArticle = '0'; let displayComments = '0'; + let showRetweeted = '1'; + let showBloggerIcons = '0'; if (ctx.req.param('routeParams')) { if (ctx.req.param('routeParams') === '1' || ctx.req.param('routeParams') === '0') { displayVideo = ctx.req.param('routeParams'); @@ -52,6 +65,8 @@ async function handler(ctx) { displayVideo = fallback(undefined, queryToBoolean(routeParams.displayVideo), true) ? '1' : '0'; displayArticle = fallback(undefined, queryToBoolean(routeParams.displayArticle), false) ? '1' : '0'; displayComments = fallback(undefined, queryToBoolean(routeParams.displayComments), false) ? '1' : '0'; + showRetweeted = fallback(undefined, queryToBoolean(routeParams.showRetweeted), false) ? '1' : '0'; + showBloggerIcons = fallback(undefined, queryToBoolean(routeParams.showBloggerIcons), false) ? '1' : '0'; } } const containerData = await cache.tryGet( @@ -99,7 +114,15 @@ async function handler(ctx) { let resultItems = await Promise.all( cards - .filter((item) => item.mblog) + .filter((item) => { + if (item.mblog === undefined) { + return false; + } + if (showRetweeted === '0' && item.mblog.retweeted_status) { + return false; + } + return true; + }) .map(async (item) => { // TODO: unify cache key and let weiboUtils.getShowData() handle the cache? It seems safe to do so. // Need more investigation, pending for now since the current version works fine. @@ -140,7 +163,7 @@ async function handler(ctx) { // 评论的处理 if (displayComments === '1') { - description = await weiboUtils.formatComments(ctx, description, item.mblog); + description = await weiboUtils.formatComments(ctx, description, item.mblog, showBloggerIcons); } // 文章的处理 @@ -177,5 +200,6 @@ async function handler(ctx) { description, image: profileImageUrl, item: resultItems, + allowEmpty: true, }); } diff --git a/lib/routes/weibo/utils.ts b/lib/routes/weibo/utils.ts index 4efa0a31e516bb..9de9514dbd5f9b 100644 --- a/lib/routes/weibo/utils.ts +++ b/lib/routes/weibo/utils.ts @@ -74,6 +74,10 @@ const weiboUtils = { if (!showLinkIconInDescription) { htmlNewLineUnreplaced = htmlNewLineUnreplaced.replaceAll(/(<a\s[^>]*>)<span class=["']?url-icon["']?><img\s[^>]*><\/span>[^<>]*?<span class=["']?surl-text["']?>([^<>]*?)<\/span><\/a>/g, '$1$2</a>'); } + + // 提取 话题作为 category + const category: string[] = htmlNewLineUnreplaced.match(/<span class=["']?surl-text["']?>#([^<>]*?)#<\/span>/g)?.map((e) => e?.match(/#([^#]+)#/)?.[1]); + // 去掉乱七八糟的图标 // 不需要,上述的替换应该已经把所有的图标都替换掉了,且这条 regex 会破坏上述替换不发生时的输出 // htmlNewLineUnreplaced = htmlNewLineUnreplaced.replace(/<span class=["']?url-icon["']?>(<img\s[^>]*?>)<\/span>/g, ''); // 将行内图标的高度设置为一行,改善阅读体验。但有些阅读器删除了 style 属性,无法生效 // 不需要,微博已经作此设置 @@ -128,8 +132,8 @@ const weiboUtils = { } // drop live photo - const livePhotoCount = status.pics ? status.pics.filter((pic) => pic.type === 'livephotos').length : 0; - const pics = status.pics && status.pics.filter((pic) => pic.type !== 'livephotos'); + const livePhotoCount = status.pics ? status.pics.filter((pic) => pic.type === 'livephoto').length : 0; + const pics = status.pics && status.pics.filter((pic) => pic.type !== 'livephoto'); // 添加微博配图 if (pics) { @@ -167,10 +171,6 @@ const weiboUtils = { html += '</a>'; } - if (!readable) { - html += '<br><br>'; - } - htmlNewLineUnreplaced += '<img src="" />'; } } @@ -238,10 +238,16 @@ const weiboUtils = { const guid = uid ? `https://weibo.com/${uid}/${bid}` : `https://m.weibo.cn/status/${bid}`; const link = preferMobileLink ? `https://m.weibo.cn/status/${bid}` : guid; - const author = status.user?.screen_name; + const author = [ + { + name: status.user?.screen_name, + url: `https://weibo.com/${uid}`, + avatar: status.user?.avatar_hd, + }, + ]; const pubDate = status.created_at; - return { description: html, title, link, guid, author, pubDate }; + return { description: html, title, link, guid, author, pubDate, category }; }, getShowData: async (uid, bid) => { const link = `https://m.weibo.cn/statuses/show?id=${bid}`; @@ -257,7 +263,7 @@ const weiboUtils = { }, formatVideo: (itemDesc, status) => { const pageInfo = status.page_info; - const livePhotos = status.pics && status.pics.filter((pic) => pic.type === 'livephotos' && pic.videoSrc); + const livePhotos = status.pics && status.pics.filter((pic) => pic.type === 'livephoto' && pic.videoSrc); let video = '<br clear="both" /><div style="clear: both"></div>'; let anyVideo = false; if (livePhotos) { @@ -393,7 +399,7 @@ const weiboUtils = { } return itemDesc; }, - formatComments: async (ctx, itemDesc, status) => { + formatComments: async (ctx, itemDesc, status, showBloggerIcons) => { if (status && status.comments_count && status.id && status.mid) { const id = status.id; const mid = status.mid; @@ -415,18 +421,46 @@ const weiboUtils = { itemDesc += '<h3>热门评论</h3>'; for (const comment of comments) { itemDesc += '<p style="margin-bottom: 0.5em;margin-top: 0.5em">'; - itemDesc += `<a href="https://weibo.com/${comment.user.id}" target="_blank">${comment.user.screen_name}</a>: ${comment.text}`; + let name = comment.user.screen_name; + if (showBloggerIcons === '1' && comment.blogger_icons) { + name += comment.blogger_icons[0].name; + } + itemDesc += `<a href="https://weibo.com/${comment.user.id}" target="_blank">${name}</a>: ${comment.text}`; + // 带有图片的评论直接输出图片 + if ('pic' in comment) { + itemDesc += `<br><img src="${comment.pic.url}">`; + } if (comment.comments) { itemDesc += '<blockquote style="border-left:0.2em solid #80808080; margin-left: 0.3em; padding-left: 0.5em; margin-bottom: 0.5em; margin-top: 0.25em">'; for (const com of comment.comments) { + // 评论的带有图片的评论直接输出图片 + const pattern = /<a\s+href="https:\/\/weibo\.cn\/sinaurl\?u=([^"]+)"[^>]*><span class='url-icon'><img[^>]*><\/span><span class="surl-text">(查看图片|评论配图|查看动图)<\/span><\/a>/g; + const matches = com.text.match(pattern); + if (matches) { + for (const match of matches) { + const hrefMatch = match.match(/href="https:\/\/weibo\.cn\/sinaurl\?u=([^"]+)"/); + if (hrefMatch) { + // 获取并解码 href 中的图片 URL + const imgSrc = decodeURIComponent(hrefMatch[1]); + const imgTag = `<img src="${imgSrc}" style="width: 1rem; height: 1rem;">`; + // 用替换后的 img 标签替换原来的 <a> 标签部分 + com.text = com.text.replaceAll(match, imgTag); + } + } + } itemDesc += '<div style="font-size: 0.9em">'; - itemDesc += `<a href="https://weibo.com/${com.user.id}" target="_blank">${com.user.screen_name}</a>: ${com.text}`; + let name = com.user.screen_name; + if (showBloggerIcons === '1' && com.blogger_icons) { + name += com.blogger_icons[0].name; + } + itemDesc += `<a href="https://weibo.com/${com.user.id}" target="_blank">${name}</a>: ${com.text}`; itemDesc += '</div>'; } itemDesc += '</blockquote>'; } itemDesc += '</p>'; } + itemDesc += '</div>'; } } diff --git a/lib/routes/wellcee/namespace.ts b/lib/routes/wellcee/namespace.ts index 8641c71079161b..0d26e4e4e51a46 100644 --- a/lib/routes/wellcee/namespace.ts +++ b/lib/routes/wellcee/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: 'Wellcee 唯心所寓', url: 'wellcee.com', categories: ['other'], + lang: 'zh-CN', }; diff --git a/lib/routes/wellcee/rent.ts b/lib/routes/wellcee/rent.ts index dc67f30c91bfef..9c092b40def83d 100644 --- a/lib/routes/wellcee/rent.ts +++ b/lib/routes/wellcee/rent.ts @@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date'; import { baseUrl, getCitys, getDistricts } from './utils'; import InvalidParameterError from '@/errors/types/invalid-parameter'; import { art } from '@/utils/render'; -import path from 'path'; +import path from 'node:path'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); diff --git a/lib/routes/wenku8/namespace.ts b/lib/routes/wenku8/namespace.ts index f038d2e87cace0..bbe8a00ccc7594 100644 --- a/lib/routes/wenku8/namespace.ts +++ b/lib/routes/wenku8/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '轻小说文库', url: 'www.wenku8.net', + lang: 'zh-CN', }; diff --git a/lib/routes/wfdf/namespace.ts b/lib/routes/wfdf/namespace.ts index 12b293edbef65a..9c320595c0f021 100644 --- a/lib/routes/wfdf/namespace.ts +++ b/lib/routes/wfdf/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'WFDF', url: 'wfdf.sport', + lang: 'en', }; diff --git a/lib/routes/wfu/namespace.ts b/lib/routes/wfu/namespace.ts index 7564b8be46e101..f6eb66f51cf936 100644 --- a/lib/routes/wfu/namespace.ts +++ b/lib/routes/wfu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '潍坊学院', url: 'jwc.wfu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/wfu/news.ts b/lib/routes/wfu/news.ts index b0bf76ff61936f..d90b35bc20d922 100644 --- a/lib/routes/wfu/news.ts +++ b/lib/routes/wfu/news.ts @@ -70,10 +70,10 @@ export const route: Route = { handler, url: 'news.wfu.edu.cn/', description: `| **内容** | **参数** | - | :------: | :------: | - | 潍院要闻 | wyyw | - | 综合新闻 | zhxw | - | 学术纵横 | xszh |`, +| :------: | :------: | +| 潍院要闻 | wyyw | +| 综合新闻 | zhxw | +| 学术纵横 | xszh |`, }; async function handler(ctx) { @@ -111,7 +111,7 @@ async function handler(ctx) { }; // 对于列表的每一项, 单独获取 时间与详细内容 - // eslint-disable-next-line no-return-await + const other = await cache.tryGet($item_url, () => loadContent($item_url)); // 合并解析后的结果集作为该篇文章最终的输出结果 return { ...single, ...other }; diff --git a/lib/routes/whitehouse/briefing-room.ts b/lib/routes/whitehouse/briefing-room.ts deleted file mode 100644 index ef2c241d94d42b..00000000000000 --- a/lib/routes/whitehouse/briefing-room.ts +++ /dev/null @@ -1,79 +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: '/briefing-room/:category?', - categories: ['government'], - example: '/whitehouse/briefing-room', - parameters: { category: 'Category, see below, all by default' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['whitehouse.gov/briefing-room/:category', 'whitehouse.gov/'], - target: '/briefing-room/:category', - }, - ], - name: 'Briefing Room', - maintainers: ['nczitzk'], - handler, - description: `| All | Blog | Legislation | Presidential Actions | Press Briefings | Speeches and Remarks | Statements and Releases | - | --- | ---- | ----------- | -------------------- | --------------- | -------------------- | ----------------------- | - | | blog | legislation | presidential-actions | press-briefings | speeches-remarks | statements-releases |`, -}; - -async function handler(ctx) { - const category = ctx.req.param('category') ?? ''; - - const rootUrl = 'https://www.whitehouse.gov'; - const currentUrl = `${rootUrl}/briefing-room/${category}`; - const response = await got({ - method: 'get', - url: currentUrl, - }); - - const $ = load(response.data); - - const list = $('.news-item__title') - .slice(0, 10) - .map((_, item) => { - item = $(item); - return { - title: item.text(), - link: item.attr('href'), - }; - }) - .get(); - - const items = await Promise.all( - list.map((item) => - cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - const content = load(detailResponse.data); - - item.description = content('.body-content').html(); - item.pubDate = parseDate(content('meta[property="article:published_time"]').attr('content')); - - return item; - }) - ) - ); - - return { - title: $('title').text(), - link: currentUrl, - item: items, - }; -} diff --git a/lib/routes/whitehouse/namespace.ts b/lib/routes/whitehouse/namespace.ts index f474dfac6f1c72..ee32b09b0800f0 100644 --- a/lib/routes/whitehouse/namespace.ts +++ b/lib/routes/whitehouse/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The White House', url: 'whitehouse.gov', + lang: 'en', }; diff --git a/lib/routes/whitehouse/news.ts b/lib/routes/whitehouse/news.ts new file mode 100644 index 00000000000000..9e347a296056bb --- /dev/null +++ b/lib/routes/whitehouse/news.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'; + +export const route: Route = { + path: '/news/:category?', + categories: ['government'], + example: '/whitehouse/news', + parameters: { category: 'Category, see below, all by default' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['whitehouse.gov/:category', 'whitehouse.gov/'], + target: '/news/:category', + }, + ], + name: 'News', + maintainers: ['nczitzk', 'hkamran80'], + handler, + description: `| All | Articles | Briefings and Statements | Presidential Actions | Remarks | +| --- | -------- | ------------------------ | -------------------- | ------- | +| | articles | briefings-statements | presidential-actions | remarks |`, +}; + +async function handler(ctx) { + const category = ctx.req.param('category') ?? 'news'; + + const rootUrl = 'https://www.whitehouse.gov'; + const currentUrl = `${rootUrl}/${category}/`; + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + + const list = $('.post') + .toArray() + .map((item) => { + item = $(item); + + const a = item.find('a').first(); + return { + title: a.text(), + link: a.attr('href'), + pubDate: parseDate(item.find('time').attr('datetime')), + category: [item.find('a[rel^=tag]').first().text()], + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await got({ + method: 'get', + url: item.link, + }); + const $ = load(response.data); + + $('.wp-block-whitehouse-topper').remove(); + item.description = $('.entry-content').html(); + + return item; + }) + ) + ); + + return { + title: $('title').text(), + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/whitehouse/ostp.ts b/lib/routes/whitehouse/ostp.ts deleted file mode 100644 index fcf549f5216045..00000000000000 --- a/lib/routes/whitehouse/ostp.ts +++ /dev/null @@ -1,76 +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: '/ostp', - categories: ['government'], - example: '/whitehouse/ostp', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['whitehouse.gov/ostp', 'whitehouse.gov/'], - }, - ], - name: 'Office of Science and Technology Policy', - maintainers: ['LyleLee'], - handler, - url: 'whitehouse.gov/ostp', -}; - -async function handler() { - const rootUrl = 'https://www.whitehouse.gov'; - const currentUrl = `${rootUrl}/ostp/news-updates`; - const response = await got({ - method: 'get', - url: currentUrl, - }); - - const $ = load(response.data); - - const list = $('.news-item__title') - .slice(0, 10) - .map((_, item) => { - item = $(item); - return { - title: item.text(), - link: item.attr('href'), - }; - }) - .get(); - - const items = await Promise.all( - list.map((item) => - cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - const content = load(detailResponse.data); - - item.description = content('.body-content').html(); - item.pubDate = parseDate(content('time.published').attr('datetime')); - item.author = content('span.tax-links.cat-links > a').text(); - - return item; - }) - ) - ); - - return { - title: 'Whitehouse OSTP', - link: currentUrl, - description: 'Whitehouse Office of Science and Technology Policy', - item: items, - }; -} diff --git a/lib/routes/who/namespace.ts b/lib/routes/who/namespace.ts index 0fa4d238e3cdb8..6eac9c31c3cfa6 100644 --- a/lib/routes/who/namespace.ts +++ b/lib/routes/who/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'World Health Organization | WHO', url: 'who.int', + lang: 'en', }; diff --git a/lib/routes/who/news-room.ts b/lib/routes/who/news-room.ts index d26b832f575d4c..bf4575fe805b2f 100644 --- a/lib/routes/who/news-room.ts +++ b/lib/routes/who/news-room.ts @@ -29,15 +29,15 @@ export const route: Route = { url: 'who.int/news', description: `Category - | Feature stories | Commentaries | - | --------------- | ------------ | - | feature-stories | commentaries | +| Feature stories | Commentaries | +| --------------- | ------------ | +| feature-stories | commentaries | Language - | English | العربية | 中文 | Français | Русский | Español | Português | - | ------- | ------- | ---- | -------- | ------- | ------- | --------- | - | en | ar | zh | fr | ru | es | pt |`, +| English | العربية | 中文 | Français | Русский | Español | Português | +| ------- | ------- | ---- | -------- | ------- | ------- | --------- | +| en | ar | zh | fr | ru | es | pt |`, }; async function handler(ctx) { diff --git a/lib/routes/who/news.ts b/lib/routes/who/news.ts index eb12506ba21ae7..05bfc2451daec1 100644 --- a/lib/routes/who/news.ts +++ b/lib/routes/who/news.ts @@ -28,9 +28,9 @@ export const route: Route = { url: 'who.int/news', description: `Language - | English | العربية | 中文 | Français | Русский | Español | Português | - | ------- | ------- | ---- | -------- | ------- | ------- | --------- | - | en | ar | zh | fr | ru | es | pt |`, +| English | العربية | 中文 | Français | Русский | Español | Português | +| ------- | ------- | ---- | -------- | ------- | ------- | --------- | +| en | ar | zh | fr | ru | es | pt |`, }; async function handler(ctx) { diff --git a/lib/routes/who/speeches.ts b/lib/routes/who/speeches.ts index 0a4cd246dc9376..6855b2795c05a6 100644 --- a/lib/routes/who/speeches.ts +++ b/lib/routes/who/speeches.ts @@ -29,9 +29,9 @@ export const route: Route = { url: 'who.int/director-general/speeches', description: `Language - | English | العربية | 中文 | Français | Русский | Español | Português | - | ------- | ------- | ---- | -------- | ------- | ------- | --------- | - | en | ar | zh | fr | ru | es | pt |`, +| English | العربية | 中文 | Français | Русский | Español | Português | +| ------- | ------- | ---- | -------- | ------- | ------- | --------- | +| en | ar | zh | fr | ru | es | pt |`, }; async function handler(ctx) { diff --git a/lib/routes/whu/cs.ts b/lib/routes/whu/cs.ts index 8b22e8caaf4765..6cc609273001d8 100644 --- a/lib/routes/whu/cs.ts +++ b/lib/routes/whu/cs.ts @@ -24,8 +24,8 @@ export const route: Route = { maintainers: ['ttyfly'], handler, description: `| 公告类型 | 学院新闻 | 学术交流 | 通知公告 | 科研进展 | - | -------- | -------- | -------- | -------- | -------- | - | 参数 | 0 | 1 | 2 | 3 |`, +| -------- | -------- | -------- | -------- | -------- | +| 参数 | 0 | 1 | 2 | 3 |`, }; async function handler(ctx) { @@ -71,10 +71,16 @@ async function handler(ctx) { }; }); - const items = await Promise.all( + let items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { - const response = await got(item.link); + let response; + try { + // 实测发现有些链接无法访问 + response = await got(item.link); + } catch { + return null; + } const $ = load(response.data); if ($('.prompt').length) { @@ -87,7 +93,8 @@ async function handler(ctx) { content.find('img').each((_, e) => { e = $(e); if (e.attr('orisrc')) { - e.attr('src', new URL(e.attr('orisrc'), response.url).href); + const newUrl = new URL(e.attr('orisrc'), 'https://cs.whu.edu.cn'); + e.attr('src', newUrl.href); e.removeAttr('orisrc'); e.removeAttr('vurl'); } @@ -100,6 +107,7 @@ async function handler(ctx) { }) ) ); + items = items.filter((item) => item !== null); return { title: $('title').first().text(), diff --git a/lib/routes/whu/gs/index.ts b/lib/routes/whu/gs/index.ts index 250f0102388de0..63b130919b2c02 100644 --- a/lib/routes/whu/gs/index.ts +++ b/lib/routes/whu/gs/index.ts @@ -40,13 +40,14 @@ export const route: Route = { handler, url: 'gs.whu.edu.cn/index.htm', description: `| 公告类型 | 新闻动态 | 学术探索 | 院系风采 | 通知 (全部) | 通知 (招生) | 通知 (培养) | 通知 (学位) | 通知 (质量与专业学位) | 通知 (综合) | - | -------- | -------- | -------- | -------- | ----------- | ----------- | ----------- | ----------- | --------------------- | ----------- | - | 参数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |`, +| -------- | -------- | -------- | -------- | ----------- | ----------- | ----------- | ----------- | --------------------- | ----------- | +| 参数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |`, }; async function handler(ctx) { const host = 'https://gs.whu.edu.cn/'; - const type = (ctx.params && Number.parseInt(ctx.req.param('type'))) || 0; + const paremType = ctx.req.param('type'); + const type = paremType ? Number.parseInt(paremType) : 0; const response = await got(host + gsIndexMap.get(type)); const $ = load(response.data); diff --git a/lib/routes/whu/namespace.ts b/lib/routes/whu/namespace.ts index 5975ea2cbe441b..8c4002fbf301cd 100644 --- a/lib/routes/whu/namespace.ts +++ b/lib/routes/whu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '武汉大学', url: 'cs.whu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/whu/news.ts b/lib/routes/whu/news.ts index e544656bb69fd1..5830ef4033db3c 100644 --- a/lib/routes/whu/news.ts +++ b/lib/routes/whu/news.ts @@ -13,15 +13,40 @@ import { domain, processMeta, getMeta, processItems } from './util'; export const route: Route = { path: '/news/:category{.+}?', - name: 'Unknown', + categories: ['university'], + example: '/whu/news', + parameters: { category: '新闻栏目,可选' }, + name: '新闻网', maintainers: [], handler, + description: ` +category 参数可选,范围如下: + +| 新闻栏目 | 武大资讯 | 学术动态 | 珞珈影像 | 武大视频 | +| -------- | -------- | -------- | -------- | -------- | +| 参数 | 0 或 \`wdzx/wdyw\` | 1 或 \`kydt\` | 2 或 \`stkj/ljyx\` | 3 或 \`stkj/wdsp\` | + +此外 route 后可以加上 \`?limit=n\` 的查询参数,表示只获取前 n 条新闻;如果不指定默认为 10。 +`, +}; + +const parseCategory = (category: string | number) => { + const outputs = ['wdzx/wdyw', 'kydt', 'stkj/ljyx', 'stkj/wdsp']; + if (['0', '1', '2', '3'].includes(category)) { + return outputs[category]; + } + if (outputs.includes(category)) { + return category; + } + return 'wdzx/wdyw'; }; async function handler(ctx) { - const { category = 'wdzx/wdyw' } = ctx.req.param(); + let { category } = ctx.req.param(); const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + category = parseCategory(category); + const rootUrl = `https://news.${domain}`; const currentUrl = new URL(`${category}.htm`, rootUrl).href; diff --git a/lib/routes/whu/rsgis.ts b/lib/routes/whu/rsgis.ts new file mode 100644 index 00000000000000..3eaaff9664787a --- /dev/null +++ b/lib/routes/whu/rsgis.ts @@ -0,0 +1,277 @@ +import { Route, DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load, Cheerio, AnyNode } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import { Context } from 'hono'; +import cache from '@/utils/cache'; + +interface Post extends DataItem { + external: boolean; +} + +const baseUrl = 'https://rsgis.whu.edu.cn'; +const categoryMap = { + index: { + name: '首页', + path: '', + }, + xyxw: { + name: '学院新闻', + path: 'xyxw1', + sub: { + xyyw: { + name: '学院要闻', + path: 'xyyw2', + }, + hzjl: { + name: '合作交流', + path: 'hzjl', + }, + mtjj: { + name: '媒体聚焦', + path: 'mtjj', + }, + xgyw: { + name: '学工要闻', + path: 'xgyw', + }, + }, + }, + kxyj: { + name: '科学研究', + path: 'kxyj', + sub: { + xsbg: { + name: '学术报告', + path: 'xsbg', + }, + xsjl: { + name: '学术交流', + path: 'xsjl', + }, + kycg: { + name: '学术成果', + path: 'kycg', + }, + sbxx: { + name: '申报信息', + path: 'sbxx', + }, + }, + }, + tzgg: { + name: '通知公告', + path: 'tzgg1', + sub: { + xytz: { + name: '学院通知', + path: 'xytz', + }, + jxdt: { + name: '教学动态', + path: 'jxdt', + }, + xsdt: { + name: '学术动态', + path: 'xsdt', + }, + rcyj: { + name: '人才引进', + path: 'rcyj', + }, + }, + }, +}; + +/** + * Check whether the link is external. + * + * @param link Post link + * @returns Whether or not weixin post + */ +function checkExternal(link: string): boolean { + const matchWeixin = link.match(/^((http:\/\/)|(https:\/\/))?([\dA-Za-z]([\dA-Za-z-]{0,61}[\dA-Za-z])?\.)+[A-Za-z]{2,6}(\/)/); + return !!matchWeixin?.length; +} + +/** + * Get information from a list of paired link and date. + * + * @param element + * @returns A list of RSS meta node. + */ +function parseListLinkDateItem(element: Cheerio<AnyNode>, currentUrl: string) { + const linkElement = element.find('a').first(); + const title = linkElement.text(); + const href = linkElement.attr('href'); + if (href === undefined) { + throw new Error('Cannot get link'); + } + const external = checkExternal(href); + const link = external ? href : new URL(href, currentUrl).href; + const pubDate = element.find('div.date1').first().text(); + return { + title, + link, + pubDate: timezone(parseDate(pubDate, 'YYYY-MM-DD'), +8), + description: title, + external, + }; +} + +async function getDetail(item: Post): Promise<DataItem | any> { + const link = item.link; + return link + ? await cache.tryGet(`whu:rsgis:${link}`, async () => { + if (item.external) { + item.description = `<a href="${link}">阅读原文</a>`; + } else { + const response = await ofetch(link); + const $ = load(response); + const title = $('div.content div.content_title h1').first().text(); + const content = $('div.content div.v_news_content').first().html(); + item.title = title; + item.description = content || ''; + } + return item; + }) + : item; +} + +/** + * Process index type. + */ +async function handleIndex(): Promise<Array<Post>> { + const url = `${baseUrl}/index.htm`; + const response = await ofetch(url); + const $ = load(response); + // 学院新闻 + const xyxwList: Array<Post> = $('div.main1 > div.newspaper:nth-child(1) > div.newspaper_list > ul > li') + .toArray() + .map((item) => parseListLinkDateItem($(item), baseUrl)); + // 通知公告 + const tzggList: Array<Post> = $('div.main1 > div.newspaper:nth-child(2) > div.newspaper_list > ul > li') + .toArray() + .map((item) => parseListLinkDateItem($(item), baseUrl)); + // 学术动态 + const xsdtList: Array<Post> = $('div.main3 div.inner > div.newspaper:nth-child(1) > ul.newspaper_list2 > li:nth-child(1) > ul > li') + .toArray() + .map((item) => parseListLinkDateItem($(item), baseUrl)); + // 学术进展 + const xsjzList: Array<Post> = $('div.main3 div.inner > div.newspaper:nth-child(1) > ul.newspaper_list2 > li:nth-child(2) > ul > li') + .toArray() + .map((item) => parseListLinkDateItem($(item), baseUrl)); + // 教学动态 + const jxdtList: Array<Post> = $('div.main3 div.inner > div.newspaper:nth-child(2) > div.newspaper_list2 > ul > li') + .toArray() + .map((item) => parseListLinkDateItem($(item), baseUrl)); + // 学工动态 + const xgdtList: Array<Post> = $('div.main3 div.inner > div.newspaper:nth-child(3) > div.newspaper_list2 > ul > li') + .toArray() + .map((item) => parseListLinkDateItem($(item), baseUrl)); + // 组合所有新闻 + const fullList = await Promise.all([...xyxwList, ...tzggList, ...xsdtList, ...xsjzList, ...jxdtList, ...xgdtList].map(async (item) => await getDetail(item))); + return fullList; +} + +/** + * Process non-index types. + * + * @param type Level 1 type + * @param sub Level 2 type + */ +async function handlePostList(type: string, sub: string): Promise<Array<DataItem>> { + let urlList: Array<{ url: string; base: string }> = []; + const category = categoryMap[type]; + if (sub === 'all') { + const subMap = category.sub; + urlList = Object.keys(subMap).map((key) => { + const subType = subMap[key]; + return { + url: `${baseUrl}/${category.path}/${subType.path}.htm`, + base: `${baseUrl}/${category.path}`, + }; + }); + } else if (sub in category.sub) { + urlList.push({ + url: `${baseUrl}/${category.path}/${category.sub[sub].path}.htm`, + base: `${baseUrl}/${category.path}`, + }); + } else { + throw new Error('No such sub type.'); + } + const urlPosts = await Promise.all( + urlList.map(async (url) => { + const response = await ofetch(url.url); + const $ = load(response); + return $('div.neiinner > div.nav_right > div.right_inner > div.list > ul > li') + .toArray() + .map((item) => parseListLinkDateItem($(item), url.base)); + }) + ); + const fullList = await Promise.all(urlPosts.flat().map(async (item) => await getDetail(item))); + return fullList; +} + +export const route: Route = { + path: '/rsgis/:type/:sub?', + categories: ['university'], + example: '/whu/rsgis/index', + parameters: { + type: '栏目,详见表格', + sub: '子栏目。当 `type` 为 `index` 的时候不起效;当 `type` 为其他合法值时,默认为 `all` 表示所有子类,其他可选项见下表。', + }, + radar: [ + { + source: ['rsgis.whu.edu.cn/index.htm'], + target: '/rsgis/index', + }, + ], + name: '武汉大学遥感信息工程学院', + maintainers: ['HPDell'], + description: ` + +| 分类名 | \`type\` 值 | 子类名 | \`sub\` 值 | +| :------: | :-------- | :------: | :------- | +| 首页 | \`index\` | | | +| 学院新闻 | \`xyxw\` | 全部 | \`all\` | +| | | 学院要闻 | \`xyyw\` | +| | | 合作交流 | \`hzjl\` | +| | | 媒体聚焦 | \`mtjj\` | +| | | 学工要闻 | \`xgyw\` | +| 科学研究 | \`kxyj\` | 全部 | \`all\` | +| | | 学术报告 | \`xsbg\` | +| | | 学术交流 | \`xsjl\` | +| | | 学术成果 | \`kycg\` | +| | | 申报信息 | \`sbxx\` | +| 通知公告 | \`tzgg\` | 全部 | \`all\` | +| | | 学院通知 | \`xytz\` | +| | | 教学动态 | \`jxdt\` | +| | | 学术动态 | \`xsdt\` | +| | | 人才引进 | \`rcyj\` | +`, + handler: async (ctx: Context) => { + const { type = 'index', sub = 'all' } = ctx.req.param(); + let itemList: Array<DataItem> = []; + switch (type) { + case 'index': + itemList = await handleIndex(); + break; + case 'xyxw': + case 'kxyj': + case 'tzgg': + itemList = await handlePostList(type, sub); + break; + default: + throw new Error('No such type'); + } + + return { + title: `${categoryMap[type].name} - 武汉大学遥感信息工程学院`, + link: baseUrl, + description: `${categoryMap[type].name} - 武汉大学遥感信息工程学院`, + item: itemList, + }; + }, +}; diff --git a/lib/routes/whu/swrh.ts b/lib/routes/whu/swrh.ts new file mode 100644 index 00000000000000..277714e007467c --- /dev/null +++ b/lib/routes/whu/swrh.ts @@ -0,0 +1,115 @@ +// 修改自计算机学院route +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'; +import { load } from 'cheerio'; +import { fetchArticle } from '@/utils/wechat-mp'; + +const baseUrl = 'https://swrh.whu.edu.cn'; + +export const route: Route = { + path: '/swrh/:type', + categories: ['university'], + example: '/whu/swrh/2', + radar: [ + { + source: ['swrh.whu.edu.cn/:type'], + target: '/swrh/:type', + }, + ], + parameters: { type: '公告类型,详见表格' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '水利水电学院公告', + maintainers: ['FanofZY'], + handler, + description: `| 公告类型 | 学院新闻 | 学术科研 | 通知公告 | +| -------- | -------- | -------- | -------- | +| 参数 | 0 | 1 | 2 |`, +}; + +async function handler(ctx) { + const type = Number.parseInt(ctx.req.param('type')); + + let link; + switch (type) { + case 0: + link = `${baseUrl}/index/xyxw.htm`; // 学院新闻 + break; + + case 1: + link = `${baseUrl}/index/xsky.htm`; // 学术科研 + break; + + case 2: + link = `${baseUrl}/xxgk/tzgg.htm`; // 通知公告 + break; + + default: + throw new Error(`Unknown type: ${type}`); + } + + const response = await got(link); + const $ = load(response.data); + + const list = + type === 0 + ? $('div.my_box_nei') + .toArray() + .map((item) => { + item = $(item); + return { + title: item.find('a b.am-text-truncate').text().trim(), + pubDate: item.find('a i').text().trim(), + link: new URL(item.find('a').attr('href'), baseUrl).href, + }; + }) + : $('div.list_txt.am-fr ul.am-list li') + .toArray() + .map((item) => { + item = $(item); + return { + title: item.find('a span').text().trim(), + pubDate: item.find('a i').text().trim(), + link: new URL(item.find('a').attr('href'), baseUrl).href, + }; + }); + + let items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + // 首先检查是否是微信公众号 + item.description = item.link.includes('weixin') + ? await fetchArticle(item.link).then((article) => article.description) + : await (async () => { + try { + const response = await got(item.link); + const $ = load(response.data); + + return $('.v_news_content').length ? $('.v_news_content').html().trim() : ($('.prompt').length ? $('.prompt').html() : item.title); + } catch { + return item.title; + } + })(); + + return item; + }) + ) + ); + + items = items.filter((item) => item !== null); + + return { + title: $('title').first().text(), + link, + item: items, + }; +} diff --git a/lib/routes/wikinews/index.ts b/lib/routes/wikinews/index.ts index 0a35733aa933e8..454cbce7bc2c31 100644 --- a/lib/routes/wikinews/index.ts +++ b/lib/routes/wikinews/index.ts @@ -37,10 +37,10 @@ async function handler() { .map((item) => { item = $(item); return { - title: item.find('news\\:title').text(), - pubDate: parseDate(item.find('news\\:publication_date').text()), + title: item.find(String.raw`news\:title`).text(), + pubDate: parseDate(item.find(String.raw`news\:publication_date`).text()), category: item - .find('news\\:keywords') + .find(String.raw`news\:keywords`) .text() .split(',') .map((item) => item.trim()), diff --git a/lib/routes/wikinews/namespace.ts b/lib/routes/wikinews/namespace.ts index d78d93212e0696..699533c204a70f 100644 --- a/lib/routes/wikinews/namespace.ts +++ b/lib/routes/wikinews/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '维基新闻', url: 'zh.wikinews.org', + lang: 'zh-CN', }; diff --git a/lib/routes/winstall/namespace.ts b/lib/routes/winstall/namespace.ts index 6a6f5ac8667863..41a80d9cadfba4 100644 --- a/lib/routes/winstall/namespace.ts +++ b/lib/routes/winstall/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'winstall', url: 'winstall.app', + lang: 'en', }; diff --git a/lib/routes/wired/namespace.ts b/lib/routes/wired/namespace.ts new file mode 100644 index 00000000000000..d7ba3e6fffd233 --- /dev/null +++ b/lib/routes/wired/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'WIRED', + url: 'www.wired.com', + categories: ['traditional-media'], + lang: 'en', +}; diff --git a/lib/routes/wired/tag.ts b/lib/routes/wired/tag.ts new file mode 100644 index 00000000000000..8347d7d199d143 --- /dev/null +++ b/lib/routes/wired/tag.ts @@ -0,0 +1,97 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import { Item } from './types'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/tag/:tag', + example: '/wired/tag/facebook', + parameters: { tag: 'Tag name' }, + radar: [ + { + source: ['www.wired.com/tag/:tag/'], + }, + ], + name: 'Tags', + maintainers: ['Naiqus'], + handler, +}; + +async function handler(ctx) { + const baseUrl = 'https://www.wired.com'; + const { tag } = ctx.req.param() as { tag: string }; + const link = `${baseUrl}/tag/${tag}/`; + + const response = await ofetch(link); + const $ = cheerio.load(response); + const preloadedState = JSON.parse( + $('script:contains("window.__PRELOADED_STATE__")') + .text() + .match(/window\.__PRELOADED_STATE__ = (.*);/)?.[1] ?? '{}' + ); + + const list = (preloadedState.transformed.tag.items as Item[]).map((item) => ({ + title: item.dangerousHed, + description: item.dangerousDek, + link: `${baseUrl}${item.url}`, + pubDate: parseDate(item.date), + author: item.contributors.author.items.map((author) => author.name).join(', '), + category: [item.rubric.name], + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = cheerio.load(response); + const preloadedState = JSON.parse( + $('script:contains("window.__PRELOADED_STATE__")') + .text() + .match(/window\.__PRELOADED_STATE__ = (.*);/)?.[1] ?? '{}' + ); + + const headerLeadAsset = $('div[data-testid*="ContentHeaderLeadAsset"]'); + headerLeadAsset.find('button').remove(); + // false postive: 'some' does not exist on type 'Cheerio<Element>' + // eslint-disable-next-line unicorn/prefer-array-some + if (headerLeadAsset.find('video')) { + headerLeadAsset.find('video').attr('src', $('link[rel="preload"][as="video"]').attr('href')); + headerLeadAsset.find('video').attr('controls', ''); + headerLeadAsset.find('video').attr('preload', 'metadata'); + headerLeadAsset.find('video').removeAttr('autoplay'); + } + + const content = $('.body__inner-container') + .toArray() + .map((el) => { + const $el = $(el); + $el.find('noscript').each((_, el) => { + const $e = $(el); + $e.replaceWith($e.html() || ''); + }); + + return $el.html(); + }) + .join(''); + + item.description = ($('div[class^=ContentHeaderDek]').prop('outerHTML') || '') + headerLeadAsset.prop('outerHTML') + content; + + item.category = [...new Set([...item.category, ...preloadedState.transformed.article.tagCloud.tags.map((t: { tag: string }) => t.tag)])]; + + return item; + }) + ) + ); + + return { + title: preloadedState.transformed['head.title'], + description: preloadedState.transformed['head.description'], + link, + image: `${baseUrl}${preloadedState.transformed.logo.sources.sm.url}`, + language: 'en', + item: items, + }; +} diff --git a/lib/routes/wired/types.ts b/lib/routes/wired/types.ts new file mode 100644 index 00000000000000..7d92b1258bfa3f --- /dev/null +++ b/lib/routes/wired/types.ts @@ -0,0 +1,81 @@ +interface ImageSource { + aspectRatio: string; + width: number; + url: string; + srcset: string; +} + +interface Image { + contentType: string; + id: string; + altText: string; + credit: string; + caption: string; + metaData: string; + modelName: string; + sources: { + sm: ImageSource; + md: ImageSource; + lg: ImageSource; + xl: ImageSource; + xxl: ImageSource; + }; + segmentedSources: { + sm: ImageSource[]; + lg: ImageSource[]; + }; + showImageWithoutLink: boolean; + isLazy: boolean; + brandDetail: { + brandIcon: string; + brandName: string; + }; +} + +interface Contributor { + author: { + items: { name: string }[]; + }; + photographer: { + items: any[]; + }; +} + +interface Rubric { + name: string; + url: string; +} + +interface RecircMostPopular { + contentType: string; + dangerousHed: string; + dangerousDek: string; + url: string; + rubric: { name: string }; + tout: Image; + image: Image; + contributors: { + author: { + brandName: string; + brandSlug: string; + preamble: string; + items: { name: string }[]; + }; + }; +} + +export interface Item { + contributors: Contributor; + contentType: string; + date: string; + dangerousDek: string; + dangerousHed: string; + id: string; + image: Image; + imageLabels: any[]; + hasNoFollowOnSyndicated: boolean; + rating: string; + rubric: Rubric; + url: string; + recircMostPopular: RecircMostPopular[]; +} diff --git a/lib/routes/wise/namespace.ts b/lib/routes/wise/namespace.ts index 698a62f2db80c1..2defa382161eb0 100644 --- a/lib/routes/wise/namespace.ts +++ b/lib/routes/wise/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Wise', url: 'wise.com', + lang: 'en', }; diff --git a/lib/routes/withgoogle/explorables.ts b/lib/routes/withgoogle/explorables.ts new file mode 100644 index 00000000000000..4dd5489156d5b7 --- /dev/null +++ b/lib/routes/withgoogle/explorables.ts @@ -0,0 +1,54 @@ +import type { Route, DataItem } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +export const route: Route = { + name: 'PAIR - AI Exploreables', + url: 'pair.withgoogle.com/explorables', + path: '/explorables', + maintainers: ['cesaryuan'], + example: '/withgoogle/explorables', + categories: ['blog'], + radar: [ + { + source: ['pair.withgoogle.com/explorables'], + target: '', + }, + ], + handler: async () => { + const baseUrl = 'https://pair.withgoogle.com'; + const response = await ofetch(baseUrl + '/explorables', { + method: 'GET', + }); + const $ = load(response); + const items = await Promise.all( + $('div.explorable-card') + .toArray() + .map(async (el) => { + const title = $(el).find('h3').text(); + const image = $(el).find('img').attr('src'); + const link = baseUrl + $(el).find('a').attr('href'); + return (await cache.tryGet(link, async () => { + const response = await ofetch(link); + const $item = load(response); + let description = $item('body').html(); + if (!description || description.trim() === '') { + description = $('p').text(); + } + return { + title, + link, + description, + image, + }; + })) as DataItem; + }) + ); + return { + title: 'PAIR - AI Exploreables', + link: 'https://pair.withgoogle.com/explorables', + item: items, + }; + }, +}; diff --git a/lib/routes/withgoogle/namespace.ts b/lib/routes/withgoogle/namespace.ts new file mode 100644 index 00000000000000..cd844c6501e1e1 --- /dev/null +++ b/lib/routes/withgoogle/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'People + AI Research (PAIR)', + url: 'pair.withgoogle.com', + lang: 'en', +}; diff --git a/lib/routes/wizfile/namespace.ts b/lib/routes/wizfile/namespace.ts index 35e60772b01ad6..ac7f6958088eec 100644 --- a/lib/routes/wizfile/namespace.ts +++ b/lib/routes/wizfile/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'WziFile', url: 'antibody-software.com', + lang: 'en', }; diff --git a/lib/routes/wmc-bj/namespace.ts b/lib/routes/wmc-bj/namespace.ts index 388d297b8b003f..f53ae8fa368bd9 100644 --- a/lib/routes/wmc-bj/namespace.ts +++ b/lib/routes/wmc-bj/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'World Meteorological Centre Beijing', url: 'wmc-bj.net', + lang: 'en', }; diff --git a/lib/routes/wmpvp/index.ts b/lib/routes/wmpvp/index.ts index 855638085dd3f6..52adb09734d833 100644 --- a/lib/routes/wmpvp/index.ts +++ b/lib/routes/wmpvp/index.ts @@ -20,8 +20,8 @@ export const route: Route = { maintainers: ['tssujt'], handler, description: `| DOTA2 | CS2 | - | ----- | --- | - | 1 | 2 |`, +| ----- | --- | +| 1 | 2 |`, }; const TYPE_MAP = { diff --git a/lib/routes/wmpvp/namespace.ts b/lib/routes/wmpvp/namespace.ts index 827076dda659d9..e580189f6fc973 100644 --- a/lib/routes/wmpvp/namespace.ts +++ b/lib/routes/wmpvp/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '完美世界电竞', url: 'wmpvp.com', + lang: 'zh-CN', }; diff --git a/lib/routes/wnacg/category.ts b/lib/routes/wnacg/category.ts new file mode 100644 index 00000000000000..8408e247ac7246 --- /dev/null +++ b/lib/routes/wnacg/category.ts @@ -0,0 +1,17 @@ +import { Route } from '@/types'; +import { handler } from './common'; + +export const route: Route = { + name: '分类更新', + maintainers: ['Gandum2077'], + path: '/category/:cid', + example: '/wnacg/category/6', + radar: [ + { + source: ['wnacg.com/*'], + target: (_, url) => `/wnacg/category/${new URL(url).pathname.match(/albums-index-cate-(\d+)\.html$/)[1]}`, + }, + ], + handler, + url: 'wnacg.com/albums.html', +}; diff --git a/lib/routes/wnacg/common.ts b/lib/routes/wnacg/common.ts new file mode 100644 index 00000000000000..7c88e5d45cc2a3 --- /dev/null +++ b/lib/routes/wnacg/common.ts @@ -0,0 +1,111 @@ +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 InvalidParameterError from '@/errors/types/invalid-parameter'; + +const categories = { + 1: '同人誌 漢化', + 2: '同人誌 CG畫集', + 3: '同人誌 Cosplay', + 5: '同人誌', + 6: '單行本', + 7: '雜誌&短篇', + 9: '單行本 漢化', + 10: '雜誌&短篇 漢化', + 12: '同人誌 日語', + 13: '單行本 日語', + 14: '雜誌&短篇 日語', + 16: '同人誌 English', + 17: '單行本 English', + 18: '雜誌&短篇 English', + 19: '韓漫', + 20: '韓漫 漢化', + 21: '韓漫 生肉', + 22: '同人誌 3D漫畫', +}; + +const baseUrl = 'https://www.wnacg.com'; + +export async function handler(ctx) { + const { cid, tag } = ctx.req.param(); + if (cid && !Object.keys(categories).includes(cid)) { + throw new InvalidParameterError('此分类不存在'); + } + + const url = `${baseUrl}/albums${cid ? `-index-cate-${cid}` : ''}${tag ? `-index-tag-${tag}` : ''}.html`; + const { data } = await got(url); + const $ = load(data); + + const list = $('.gallary_item') + .toArray() + .map((item) => { + item = $(item); + const href = item.find('a').attr('href'); + const aid = href.match(/^\/photos-index-aid-(\d+)\.html$/)[1]; + return { + title: item.find('a').attr('title'), + link: `${baseUrl}${href}`, + pubDate: parseDate( + item + .find('.info_col') + .text() + .replace(/\d+張照片,\n創建於/, ''), + 'YYYY-MM-DD' + ), + aid, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const { data: descRes } = await got(item.link, { + headers: { + referer: encodeURI(url), + }, + }); + let $ = load(descRes); + const author = $('.uwuinfo p').first().text(); + const category = $('.tagshow') + .toArray() + .map((item) => $(item).text()); + $('.addtags').remove(); + const description = $('.uwconn').html(); + + const { data } = await got(`${baseUrl}/photos-gallery-aid-${item.aid}.html`, { + headers: { + referer: `${baseUrl}/photos-slide-aid-${item.aid}.html`, + }, + }); + $ = load(data); + + const imgListMatch = $('script') + .text() + .match(/var imglist = (\[.*]);"\);/)[1]; + + const imgList = JSON.parse(imgListMatch.replaceAll('url:', '"url":').replaceAll('caption:', '"caption":').replaceAll('fast_img_host+\\', '').replaceAll('\\', '')); + + item.author = author; + item.category = category; + item.description = art(path.join(__dirname, 'templates/manga.art'), { + description, + imgList, + }); + + return item; + }) + ) + ); + + return { + title: $('head title').text(), + link: url, + item: items, + }; +} diff --git a/lib/routes/wnacg/index.ts b/lib/routes/wnacg/index.ts index bb877eabcfc9b7..ec09c602f29b17 100644 --- a/lib/routes/wnacg/index.ts +++ b/lib/routes/wnacg/index.ts @@ -1,126 +1,16 @@ 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 InvalidParameterError from '@/errors/types/invalid-parameter'; - -const categories = { - 1: '同人誌 漢化', - 2: '同人誌 CG畫集', - 3: '同人誌 Cosplay', - 5: '同人誌', - 6: '單行本', - 7: '雜誌&短篇', - 9: '單行本 漢化', - 10: '雜誌&短篇 漢化', - 12: '同人誌 日語', - 13: '單行本 日語', - 14: '雜誌&短篇 日語', - 16: '同人誌 English', - 17: '單行本 English', - 19: '韓漫', - 20: '韓漫 漢化', - 21: '韓漫 生肉', -}; - -const baseUrl = 'https://www.wnacg.com'; +import { handler } from './common'; export const route: Route = { - path: ['/', '/category/:cid', '/tag/:tag'], + name: '最新', + maintainers: ['KenMizz'], + path: '/', + example: '/wnacg', radar: [ { - source: ['wnacg.org/albums.html', 'wnacg.org/'], - target: '', + source: ['wnacg.com/albums.html', 'wnacg.com/'], }, ], - name: 'Unknown', - maintainers: ['KenMizz'], handler, - url: 'wnacg.org/albums.html', - url: 'wnacg.org/albums.html', - url: 'wnacg.org/albums.html', + url: 'wnacg.com/albums.html', }; - -async function handler(ctx) { - const { cid, tag } = ctx.req.param(); - if (cid && !Object.keys(categories).includes(cid)) { - throw new InvalidParameterError('此分类不存在'); - } - - const url = `${baseUrl}/albums${cid ? `-index-cate-${cid}` : ''}${tag ? `-index-tag-${tag}` : ''}.html`; - const { data } = await got(url); - const $ = load(data); - - const list = $('.gallary_item') - .toArray() - .map((item) => { - item = $(item); - const href = item.find('a').attr('href'); - const aid = href.match(/^\/photos-index-aid-(\d+)\.html$/)[1]; - return { - title: item.find('a').attr('title'), - link: `${baseUrl}${href}`, - pubDate: parseDate( - item - .find('.info_col') - .text() - .replace(/\d+張照片,\n創建於/, ''), - 'YYYY-MM-DD' - ), - aid, - }; - }); - - const items = await Promise.all( - list.map((item) => - cache.tryGet(item.link, async () => { - const { data: descRes } = await got(item.link, { - headers: { - referer: encodeURI(url), - }, - }); - let $ = load(descRes); - const author = $('.uwuinfo p').first().text(); - const category = $('.tagshow') - .toArray() - .map((item) => $(item).text()); - $('.addtags').remove(); - const description = $('.uwconn').html(); - - const { data } = await got(`${baseUrl}/photos-gallery-aid-${item.aid}.html`, { - headers: { - referer: `${baseUrl}/photos-slide-aid-${item.aid}.html`, - }, - }); - $ = load(data); - - const imgListMatch = $('script') - .text() - .match(/var imglist = (\[.*]);"\);/)[1]; - - const imgList = JSON.parse(imgListMatch.replaceAll('url:', '"url":').replaceAll('caption:', '"caption":').replaceAll('fast_img_host+\\', '').replaceAll('\\', '')); - - item.author = author; - item.category = category; - item.description = art(path.join(__dirname, 'templates/manga.art'), { - description, - imgList, - }); - - return item; - }) - ) - ); - - return { - title: $('head title').text(), - link: url, - item: items, - }; -} diff --git a/lib/routes/wnacg/namespace.ts b/lib/routes/wnacg/namespace.ts index 5b1dbac6c14d28..e16e02fd82dd1f 100644 --- a/lib/routes/wnacg/namespace.ts +++ b/lib/routes/wnacg/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '紳士漫畫', url: 'wnacg.org', + lang: 'zh-TW', }; diff --git a/lib/routes/wnacg/tag.ts b/lib/routes/wnacg/tag.ts new file mode 100644 index 00000000000000..c43e5dc1f763f6 --- /dev/null +++ b/lib/routes/wnacg/tag.ts @@ -0,0 +1,17 @@ +import { Route } from '@/types'; +import { handler } from './common'; + +export const route: Route = { + name: '標籤更新', + maintainers: ['Gandum2077'], + path: '/tag/:tag', + example: '/wnacg/tag/漢化', + radar: [ + { + source: ['wnacg.com/*'], + target: (_, url) => `/wnacg/tag/${new URL(url).pathname.match(/albums-index-tag-(.+?)\.html$/)[1]}`, + }, + ], + handler, + url: 'wnacg.com/albums.html', +}; diff --git a/lib/routes/wohnnet/index.ts b/lib/routes/wohnnet/index.ts new file mode 100644 index 00000000000000..b99290d59b2b1e --- /dev/null +++ b/lib/routes/wohnnet/index.ts @@ -0,0 +1,129 @@ +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import type { Data, DataItem, Route } from '@/types'; + +const FEED_TITLE = 'wohnnet.at' as const; +const FEED_LOGO = 'https://www.wohnnet.at/media/images/wohnnet/icon_192_192.png' as const; +const FEED_LANGUAGE = 'de' as const; +const ROUTE_PATH_PREFIX = '/wohnnet/' as const; +const BASE_URL = 'https://www.wohnnet.at/immobilien/' as const; + +export const route: Route = { + name: 'Immobiliensuche', + path: '/:category/:region/*', + maintainers: ['sk22'], + categories: ['other'], + description: ` +Only returns the first page of search results, allowing you to keep track of +newly added apartments. If you're looking for an apartment, make sure to also +look through the other pages on the website. + +:::tip +Note that the parameter \`&sortierung=neueste-zuerst\` for chronological order +is automatically appended. +::: + +:::tip +To get your query URL, go to https://www.wohnnet.at/immobilien/suche, apply +all desired filters (but at least a category and a region!) and click the +"… Treffer anzeigen" link. From the resulting URL, cut off the +\`https://www.wohnnet.at/immobilien/\` part at the beginning and replace only +the \`?\` (the \`&\`s stay as is!) after the region name with a \`/\`. + +Examples: + +* \`${BASE_URL}mietwohnungen/wien\` + - → \`${ROUTE_PATH_PREFIX}mietwohnungen/wien\` +* \`${BASE_URL}mietwohnungen/wien?unterregionen=g90101\` + - → \`${ROUTE_PATH_PREFIX}mietwohnungen/wien/unterregionen=g90101\` +* \`${BASE_URL}mietwohnungen/wien?unterregionen=g90101&merkmale=balkon\` + - → \`${ROUTE_PATH_PREFIX}mietwohnungen/wien/unterregionen=g90101&merkmale=balkon\` +::: +`, + example: ROUTE_PATH_PREFIX + 'mietwohnungen/wien/unterregionen=g90101--g90201--g90301--g90401--g90501&flaeche=40&preis=-1000', + parameters: { + category: 'Category (`mietwohnungen`, `eigentumswohnungen`, `grundstuecke`, …)', + region: 'Region (`wien`, `oesterreich`, …)', + unterregionen: 'Unterregionen (e.g. `g90101--g90201--g90301`)', + intention: 'Intention (`kauf` | `miete`)', + zimmer: 'Zimmer (at least number, e.g. `2`)', + flaeche: 'Fläche (m², `40-` = at least 40 m², `40-60` = between 40 m² and 60 m²)', + preis: 'Preis (€, `-1000` = at most 1,000 €, `500-1000` = between 500 € and 1,000 €)', + merkmale: 'Merkmale (multiple, delimited by `--`, e.g. `balkon--garten--kurzzeitmiete--moebliert--parkplatz--provisionsfrei--sofort-beziehbar`)', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + async handler(ctx) { + const category = ctx.req.param('category'); + const region = ctx.req.param('region'); + + // /wohnnet/mietwohnungen/wien/&preis=… -> preis=…&sortierung=neueste-zuerst + let path = + ctx.req.path.slice(`${ROUTE_PATH_PREFIX}${category}/${region}/`.length) + + // provide chronological fallback sort (wohnnet.at will use the first one) + '&sortierung=neueste-zuerst'; + if (path.startsWith('&')) { + path = path.slice(1); + } + + const link = `${BASE_URL}${category}/${region}/?${path}`; + const response = await ofetch(link); + const $ = load(response); + + const items = $('a:has(> .realty)') + .toArray() + .map((el) => { + const $el = $(el); + const href = $el.attr('href'); + const [title, address] = $el + .find('.realty-detail-title-address') + .text() + .split('\n') + .map((p) => p.trim()) + .filter((p) => p.length); + const price = $el.find('.realty-detail-area-rooms .text-right').text().trim(); + const details = $el + .find('.realty-detail-area-rooms') + .text() + .split('\n') + .map((p) => p.trim()) + .filter((p) => p.length); + const badges = $el + .find('.realty-detail-badges .badge') + .toArray() + .map((b) => $(b).text().trim()); + const agency = $el.find('.realty-detail-agency').text(); + const imgSrc = $el.find('.realty-image img').attr('src'); + + const itemTitle = `${address} · ${price} | ${title}`; + const itemLink = new URL(href ?? '', BASE_URL).href; + const itemDescription = `${details.join(' · ')} | ${badges.join(' · ')} | ${agency}`; + const itemCategories = badges.filter((b) => !b.endsWith(' Bilder')); + const itemImage = imgSrc ? new URL(imgSrc, BASE_URL).href : undefined; + + return { + title: itemTitle, + link: itemLink, + description: itemDescription, + category: itemCategories, + image: itemImage, + // pubDate is not available on wohnnet.at + } satisfies DataItem; + }); + + return { + title: FEED_TITLE, + language: FEED_LANGUAGE, + logo: FEED_LOGO, + allowEmpty: true, + item: items, + link, + } satisfies Data; + }, +}; diff --git a/lib/routes/wohnnet/namespace.ts b/lib/routes/wohnnet/namespace.ts new file mode 100644 index 00000000000000..df4ed50694f4fe --- /dev/null +++ b/lib/routes/wohnnet/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'wohnnet.at', + url: 'wohnnet.at', + description: 'Austrian search engine for real estate', + lang: 'de', +}; diff --git a/lib/routes/wordpress/index.ts b/lib/routes/wordpress/index.ts index 05f0e4e0f41264..fe5db34701066f 100644 --- a/lib/routes/wordpress/index.ts +++ b/lib/routes/wordpress/index.ts @@ -142,7 +142,7 @@ export const route: Route = { parameters: { url: 'URL, <https://wordpress.org/news> by default', filter: 'Filter, see below' }, description: `If you subscribe to [WordPress News](https://wordpress.org/news/),where the URL is \`https://wordpress.org/news/\`, Encode the URL using \`encodeURIComponent()\` and then use it as the parameter. Therefore, the route will be [\`/wordpress/https%3A%2F%2Fwordpress.org%2Fnews\`](https://rsshub.app/wordpress/https%3A%2F%2Fwordpress.org%2Fnews). - :::tip +::: tip If you wish to subscribe to specific categories or tags, you can fill in the "filter" parameter in the route. \`/category/Podcast\` to subscribe to the Podcast category. In this case, the route would be [\`/wordpress/https%3A%2F%2Fwordpress.org%2Fnews/category/Podcast\`](https://rsshub.app/wordpress/https%3A%2F%2Fwordpress.org%2Fnews/category/Podcast). You can also subscribe to multiple categories. \`/category/Podcast,Community\` to subscribe to both the Podcast and Community categories. In this case, the route would be [\`/wordpress/https%3A%2F%2Fwordpress.org%2Fnews/category/Podcast,Community\`](https://rsshub.app/wordpress/https%3A%2F%2Fwordpress.org%2Fnews/category/Podcast,Community). @@ -150,7 +150,7 @@ export const route: Route = { Categories and tags can be combined as well. \`/category/Releases/tag/tagging\` to subscribe to the Releases category and the tagging tag. In this case, the route would be [\`/wordpress/https%3A%2F%2Fwordpress.org%2Fnews/category/Releases/tag/tagging\`](https://rsshub.app/wordpress/https%3A%2F%2Fwordpress.org%2Fnews/category/Releases/tag/tagging). You can also search for keywords. \`/search/Blog\` to search for the keyword "Blog". In this case, the route would be [\`/wordpress/https%3A%2F%2Fwordpress.org%2Fnews/search/Blog\`](https://rsshub.app/wordpress/https%3A%2F%2Fwordpress.org%2Fnews/search/Blog). - :::`, +:::`, categories: ['blog'], features: { diff --git a/lib/routes/wordpress/namespace.ts b/lib/routes/wordpress/namespace.ts index 9711e33afb8141..76ed7dd4f8c4c7 100644 --- a/lib/routes/wordpress/namespace.ts +++ b/lib/routes/wordpress/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: 'wordpress.org', categories: ['blog'], description: '', + lang: 'en', }; diff --git a/lib/routes/wordpress/util.ts b/lib/routes/wordpress/util.ts index b7152a536510c7..c57cfffa20ee70 100644 --- a/lib/routes/wordpress/util.ts +++ b/lib/routes/wordpress/util.ts @@ -194,7 +194,7 @@ const fetchData = async (url: string, rootUrl: string): Promise<object> => { const $ = load(response); const title = $('title').first().text(); - const image = new URL('wp-content/uploads/site_logo.png', rootUrl).href; + const image = new URL($('link[rel="icon"]').last().attr('href') ?? 'wp-content/uploads/site_logo.png', rootUrl).href; return { title, @@ -239,7 +239,7 @@ const getFilterByKeyAndKeyword = async (key: string, keyword: string, rootUrl: s const getFilterKeyForSearchParams = (key: string, isApi: boolean = false): string | undefined => { const keys = isApi ? filterApiKeys : filterKeys; - return Object.hasOwn(keys, key) ? keys[key] ?? key : undefined; + return Object.hasOwn(keys, key) ? (keys[key] ?? key) : undefined; }; /** diff --git a/lib/routes/worldjournal/namespace.ts b/lib/routes/worldjournal/namespace.ts index fec6b357aa05f0..b9ed306edc50e0 100644 --- a/lib/routes/worldjournal/namespace.ts +++ b/lib/routes/worldjournal/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '世界新聞網', url: 'worldjournal.com', + lang: 'zh-TW', }; diff --git a/lib/routes/worldofwarships/devblog.ts b/lib/routes/worldofwarships/devblog.ts new file mode 100644 index 00000000000000..a8e7fd479354e2 --- /dev/null +++ b/lib/routes/worldofwarships/devblog.ts @@ -0,0 +1,76 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +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: '/devblog', + categories: ['game'], + example: '/worldofwarships/devblog', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['blog.worldofwarships.com'], + target: '/devblog', + }, + ], + name: 'Development Blog', + maintainers: ['SinCerely023'], + handler, +}; + +async function handler() { + const url = 'https://blog.worldofwarships.com/'; + + const { data: response } = await got(url); + const $ = load(response); + + const face = $('[rel=apple-touch-icon]').last(); + + const list = $('article') + .toArray() + .map((item) => { + item = $(item); + const time = item.find('div').first().find('time').first(); + const tag = item.find('div').first().find('ul').first().find('li').first(); + const title = item.find('h2').first().find('a').first(); + const content = item.find('h2').first().next(); + return { + title: title.attr('title'), + link: title.attr('href'), + pubDate: timezone(parseDate(time.attr('data-timestamp') * 1000), 0), + category: tag.text(), + author: 'Wargaming', + description: content.html(), + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const { data: response } = await got(item.link); + const $ = load(response); + item.description = $('.article__content').first().html(); + return item; + }) + ) + ); + + return { + title: 'World of Warships - Development Blog', + link: url, + item: items, + image: 'https:' + face.attr('href'), + language: 'en', + author: 'Wargaming', + }; +} diff --git a/lib/routes/worldofwarships/namespace.ts b/lib/routes/worldofwarships/namespace.ts new file mode 100644 index 00000000000000..09df36739d966f --- /dev/null +++ b/lib/routes/worldofwarships/namespace.ts @@ -0,0 +1,14 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'World of Warships', + url: 'worldofwarships.com', + zh: { + name: '战舰世界', + }, + 'zh-TW': { + name: '戰艦世界', + }, + description: 'Tip: use proxy if necessary.', + lang: 'en', +}; diff --git a/lib/routes/woshipm/namespace.ts b/lib/routes/woshipm/namespace.ts index 00062097f997fa..e56f0f38ce2871 100644 --- a/lib/routes/woshipm/namespace.ts +++ b/lib/routes/woshipm/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '人人都是产品经理', url: 'woshipm.com', + lang: 'zh-CN', }; diff --git a/lib/routes/woshipm/popular.ts b/lib/routes/woshipm/popular.ts index 49d43fdf717474..99ab658ecef51a 100644 --- a/lib/routes/woshipm/popular.ts +++ b/lib/routes/woshipm/popular.ts @@ -34,8 +34,8 @@ export const route: Route = { handler, url: 'woshipm.com/', description: `| 日榜 | 周榜 | 月榜 | - | ----- | ------ | ------- | - | daily | weekly | monthly |`, +| ----- | ------ | ------- | +| daily | weekly | monthly |`, }; async function handler(ctx) { diff --git a/lib/routes/wsj/namespace.ts b/lib/routes/wsj/namespace.ts index 24ddb9913fb704..55f4784553497e 100644 --- a/lib/routes/wsj/namespace.ts +++ b/lib/routes/wsj/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'The Wall Street Journal (WSJ) 华尔街日报', url: 'cn.wsj.com', + lang: 'zh-CN', }; diff --git a/lib/routes/wsj/news.ts b/lib/routes/wsj/news.ts index 9e0d86e35f3928..801ccadf43c60d 100644 --- a/lib/routes/wsj/news.ts +++ b/lib/routes/wsj/news.ts @@ -25,15 +25,15 @@ export const route: Route = { handler, description: `en\_us - | World | U.S. | Politics | Economy | Business | Tech | Markets | Opinion | Books & Arts | Real Estate | Life & Work | Sytle | Sports | - | ----- | ---- | -------- | ------- | -------- | ---------- | ------- | ------- | ------------ | ----------- | ----------- | ------------------- | ------ | - | world | us | politics | economy | business | technology | markets | opinion | books-arts | realestate | life-work | style-entertainment | sports | +| World | U.S. | Politics | Economy | Business | Tech | Markets | Opinion | Books & Arts | Real Estate | Life & Work | Sytle | Sports | +| ----- | ---- | -------- | ------- | -------- | ---------- | ------- | ------- | ------------ | ----------- | ----------- | ------------------- | ------ | +| world | us | politics | economy | business | technology | markets | opinion | books-arts | realestate | life-work | style-entertainment | sports | zh-cn / zh-tw - | 国际 | 中国 | 金融市场 | 经济 | 商业 | 科技 | 派 | 专栏与观点 | - | ----- | ----- | -------- | ------- | -------- | ---------- | --------- | ---------- | - | world | china | markets | economy | business | technology | life-arts | opinion | +| 国际 | 中国 | 金融市场 | 经济 | 商业 | 科技 | 派 | 专栏与观点 | +| ----- | ----- | -------- | ------- | -------- | ---------- | --------- | ---------- | +| world | china | markets | economy | business | technology | life-arts | opinion | Provide full article RSS for WSJ topics.`, }; diff --git a/lib/routes/wsyu/namespace.ts b/lib/routes/wsyu/namespace.ts index b35752d22871ff..59b80eab019200 100644 --- a/lib/routes/wsyu/namespace.ts +++ b/lib/routes/wsyu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '武昌首义学院', url: 'www.wsyu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/wsyu/news.ts b/lib/routes/wsyu/news.ts index 0aa8aa0503e551..7ef45144188cfb 100644 --- a/lib/routes/wsyu/news.ts +++ b/lib/routes/wsyu/news.ts @@ -38,8 +38,8 @@ export const route: Route = { maintainers: ['Derekmini'], handler, description: `| 学校要闻 | 综合新闻 | 媒体聚焦 | - | -------- | -------- | -------- | - | xxyw | zhxw | mtjj |`, +| -------- | -------- | -------- | +| xxyw | zhxw | mtjj |`, }; async function handler(ctx) { diff --git a/lib/routes/wtu/index.ts b/lib/routes/wtu/index.ts index b6e8ad518969c6..216fbcb12c5e6f 100644 --- a/lib/routes/wtu/index.ts +++ b/lib/routes/wtu/index.ts @@ -20,8 +20,8 @@ export const route: Route = { maintainers: ['loyio'], handler, description: `| 公告类型 | 通知公告 | 教务信息 | 科研动态 | - | -------- | -------- | -------- | -------- | - | 参数 | 1 | 2 | 3 |`, +| -------- | -------- | -------- | -------- | +| 参数 | 1 | 2 | 3 |`, }; async function handler(ctx) { diff --git a/lib/routes/wtu/job.ts b/lib/routes/wtu/job.ts index a49071b9cd2532..1b25025af00a1c 100644 --- a/lib/routes/wtu/job.ts +++ b/lib/routes/wtu/job.ts @@ -57,8 +57,8 @@ export const route: Route = { maintainers: ['ticks-tan'], handler, description: `| 信息类型 | 消息通知 | 通知公告 | 新闻快递 | - | -------- | -------- | -------- | -------- | - | 参数 | xxtz | tzgg | xwkd |`, +| -------- | -------- | -------- | -------- | +| 参数 | xxtz | tzgg | xwkd |`, }; async function handler(ctx) { diff --git a/lib/routes/wtu/namespace.ts b/lib/routes/wtu/namespace.ts index c4c1d1e8f45d0d..8a76be10fe39b6 100644 --- a/lib/routes/wtu/namespace.ts +++ b/lib/routes/wtu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '武汉纺织大学', url: 'wtu.91wllm.com', + lang: 'zh-CN', }; diff --git a/lib/routes/wufazhuce/namespace.ts b/lib/routes/wufazhuce/namespace.ts new file mode 100644 index 00000000000000..fdf8007ade98f9 --- /dev/null +++ b/lib/routes/wufazhuce/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '「ONE · 一个」', + url: 'wufazhuce.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/wufazhuce/one.ts b/lib/routes/wufazhuce/one.ts new file mode 100644 index 00000000000000..72e4407ad21cd0 --- /dev/null +++ b/lib/routes/wufazhuce/one.ts @@ -0,0 +1,90 @@ +import { Route, Data, DataItem } from '@/types'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; + +const apiUrl = 'https://wufazhuce.com/'; +const NAME = '「ONE · 一个」'; + +export const route: Route = { + path: '/one', + categories: ['new-media'], + example: '/wufazhuce/one', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['wufazhuce.com'], + target: '/one', + }, + ], + name: NAME, + maintainers: ['sicheng1806'], + handler, +}; + +async function handler(): Promise<Data> { + const resp = await got(apiUrl); + const $ = load(resp.body); + let items: DataItem[] = [ + ...$('#carousel-one div.item') + .toArray() + .map((item) => { + const a = $(item).find('.fp-one-cita a').first(); + return { + title: a.text(), + link: a.attr('href'), + description: '', + category: '摄影', + }; + }), + ...$('.fp-one-articulo a') + .toArray() + .map((item) => { + const a = $(item); + return { + title: a.text(), + link: a.attr('href'), + description: '', + category: '文章', + }; + }), + ...$('.fp-one-cuestion a') + .toArray() + .map((item) => { + const a = $(item); + return { + title: a.text(), + link: a.attr('href'), + description: '', + category: '问题', + }; + }), + ]; + + // 添加全文 + items = await Promise.all( + items.map((item: DataItem) => + cache.tryGet(item.link, async () => { + const rsp = await got(item.link); + const content = load(rsp.body); + item.description = content('.tab-content').html() || ''; + return item; + }) + ) + ); + + // 生成rss + return { + title: NAME, + link: apiUrl, + item: items, + description: '复杂世界里, 一个就够了. One is all.', + }; +} diff --git a/lib/routes/wyzxwk/article.ts b/lib/routes/wyzxwk/article.ts index 8dd4e8d990bb06..33f0a8de4c9fe5 100644 --- a/lib/routes/wyzxwk/article.ts +++ b/lib/routes/wyzxwk/article.ts @@ -27,45 +27,45 @@ export const route: Route = { handler, description: `时政 - | 时代观察 | 舆论战争 | - | -------- | -------- | - | shidai | yulun | +| 时代观察 | 舆论战争 | +| -------- | -------- | +| shidai | yulun | 经济 - | 经济视点 | 社会民生 | 三农关注 | 产业研究 | - | -------- | -------- | -------- | -------- | - | jingji | shehui | sannong | chanye | +| 经济视点 | 社会民生 | 三农关注 | 产业研究 | +| -------- | -------- | -------- | -------- | +| jingji | shehui | sannong | chanye | 国际 - | 国际纵横 | 国防外交 | - | -------- | -------- | - | guoji | guofang | +| 国际纵横 | 国防外交 | +| -------- | -------- | +| guoji | guofang | 思潮 - | 理想之旅 | 思潮碰撞 | 文艺新生 | 读书交流 | - | -------- | -------- | -------- | -------- | - | lixiang | sichao | wenyi | shushe | +| 理想之旅 | 思潮碰撞 | 文艺新生 | 读书交流 | +| -------- | -------- | -------- | -------- | +| lixiang | sichao | wenyi | shushe | 历史 - | 历史视野 | 中华文化 | 中华医药 | 共产党人 | - | -------- | -------- | -------- | -------- | - | lishi | zhonghua | zhongyi | cpers | +| 历史视野 | 中华文化 | 中华医药 | 共产党人 | +| -------- | -------- | -------- | -------- | +| lishi | zhonghua | zhongyi | cpers | 争鸣 - | 风华正茂 | 工农之声 | 网友杂谈 | 网友时评 | - | -------- | -------- | -------- | -------- | - | qingnian | gongnong | zatan | shiping | +| 风华正茂 | 工农之声 | 网友杂谈 | 网友时评 | +| -------- | -------- | -------- | -------- | +| qingnian | gongnong | zatan | shiping | 活动 - | 乌有公告 | 红色旅游 | 乌有讲堂 | 书画欣赏 | - | -------- | -------- | --------- | -------- | - | gonggao | lvyou | jiangtang | shuhua |`, +| 乌有公告 | 红色旅游 | 乌有讲堂 | 书画欣赏 | +| -------- | -------- | --------- | -------- | +| gonggao | lvyou | jiangtang | shuhua |`, }; async function handler(ctx) { diff --git a/lib/routes/wyzxwk/namespace.ts b/lib/routes/wyzxwk/namespace.ts index 17510ac901008c..0a0de5edb4226a 100644 --- a/lib/routes/wyzxwk/namespace.ts +++ b/lib/routes/wyzxwk/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '乌有之乡', url: 'wyzxwk.com', + lang: 'zh-CN', }; diff --git a/lib/routes/wzu/namespace.ts b/lib/routes/wzu/namespace.ts index c53638a63efe83..a9ff5f16236c83 100644 --- a/lib/routes/wzu/namespace.ts +++ b/lib/routes/wzu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '温州大学', url: 'wzu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/x-mol/namespace.ts b/lib/routes/x-mol/namespace.ts index 76ad6cbd2dd563..b8969e850c75a0 100644 --- a/lib/routes/x-mol/namespace.ts +++ b/lib/routes/x-mol/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'X-MOL', url: 'x-mol.com', + lang: 'zh-CN', }; diff --git a/lib/routes/x6d/index.ts b/lib/routes/x6d/index.ts index 6320c74283628d..180210f4ce4255 100644 --- a/lib/routes/x6d/index.ts +++ b/lib/routes/x6d/index.ts @@ -15,28 +15,28 @@ export const route: Route = { example: '/x6d/34', parameters: { id: '分类 id,可在对应分类页面的 URL 中找到,默认为首页最近更新' }, description: `| 技巧分享 | QQ 技巧 | 微信技巧 | 其他教程 | 其他分享 | - | -------- | ------- | -------- | -------- | -------- | - | 31 | 55 | 112 | 33 | 88 | +| -------- | ------- | -------- | -------- | -------- | +| 31 | 55 | 112 | 33 | 88 | - | 宅家自学 | 健身养生 | 摄影剪辑 | 长点知识 | 自我提升 | 两性相关 | 编程办公 | 职场关系 | 新媒体运营 | 其他教程 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------- | -------- | - | 18 | 98 | 94 | 93 | 99 | 100 | 21 | 22 | 19 | 44 | +| 宅家自学 | 健身养生 | 摄影剪辑 | 长点知识 | 自我提升 | 两性相关 | 编程办公 | 职场关系 | 新媒体运营 | 其他教程 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------- | -------- | +| 18 | 98 | 94 | 93 | 99 | 100 | 21 | 22 | 19 | 44 | - | 活动线报 | 流量话费 | 免费会员 | 实物活动 | 游戏活动 | 红包活动 | 空间域名 | 其他活动 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | 34 | 35 | 91 | 92 | 39 | 38 | 37 | 36 | +| 活动线报 | 流量话费 | 免费会员 | 实物活动 | 游戏活动 | 红包活动 | 空间域名 | 其他活动 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| 34 | 35 | 91 | 92 | 39 | 38 | 37 | 36 | - | 值得一看 | 找点乐子 | 热门事件 | 节目推荐 | - | -------- | -------- | -------- | -------- | - | 65 | 50 | 77 | 101 | +| 值得一看 | 找点乐子 | 热门事件 | 节目推荐 | +| -------- | -------- | -------- | -------- | +| 65 | 50 | 77 | 101 | - | 值得一听 | 每日一听 | 歌单推荐 | - | -------- | -------- | -------- | - | 71 | 87 | 79 | +| 值得一听 | 每日一听 | 歌单推荐 | +| -------- | -------- | -------- | +| 71 | 87 | 79 | - | 资源宝库 | 书籍资料 | 设计资源 | 剪辑资源 | 办公资源 | 壁纸资源 | 编程资源 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | 106 | 107 | 108 | 109 | 110 | 111 | 113 |`, +| 资源宝库 | 书籍资料 | 设计资源 | 剪辑资源 | 办公资源 | 壁纸资源 | 编程资源 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| 106 | 107 | 108 | 109 | 110 | 111 | 113 |`, categories: ['new-media'], features: { diff --git a/lib/routes/x6d/namespace.ts b/lib/routes/x6d/namespace.ts index 7a428f278a1d09..45ce70377a9fdf 100644 --- a/lib/routes/x6d/namespace.ts +++ b/lib/routes/x6d/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '小刀娱乐网', url: 'xd.x6d.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xaufe/jiaowu.ts b/lib/routes/xaufe/jiaowu.ts index 8da10d9a4a80d8..d478b7d8899aac 100644 --- a/lib/routes/xaufe/jiaowu.ts +++ b/lib/routes/xaufe/jiaowu.ts @@ -34,8 +34,8 @@ export const route: Route = { maintainers: ['shaokeyibb'], handler, description: `| 通知公告 | - | :------: | - | tzgg |`, +| :------: | +| tzgg |`, }; async function handler(ctx) { diff --git a/lib/routes/xaufe/namespace.ts b/lib/routes/xaufe/namespace.ts index e313c3286d91a9..a87102c0cd9dcf 100644 --- a/lib/routes/xaufe/namespace.ts +++ b/lib/routes/xaufe/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '西安财经大学', url: 'jiaowu.xaufe.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/xaut/index.ts b/lib/routes/xaut/index.ts index 7768d41c964885..298ece3e57abd4 100644 --- a/lib/routes/xaut/index.ts +++ b/lib/routes/xaut/index.ts @@ -1,6 +1,7 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; +import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; import { load } from 'cheerio'; @@ -8,7 +9,7 @@ export const route: Route = { path: '/index/:category?', categories: ['university'], example: '/xaut/index/tzgg', - parameters: { category: '通知类别,默认为通知公告' }, + parameters: { category: '通知类别,默认为学校新闻' }, features: { requireConfig: false, requirePuppeteer: false, @@ -20,19 +21,19 @@ export const route: Route = { name: '学校主页', maintainers: ['mocusez'], handler, - description: `| 通知公告 | 校园要闻 | 媒体播报 | 学术活动 | - | :------: | :------: | :------: | :------: | - | tzgg | xyyw | mtbd | xshd |`, + description: `| 学校新闻 | 砥志研思 | 立德树人 | 传道授业 | 校闻周知 | +| :------: | :------: | :------: | :------: | :------: | +| xxxw | dzys | ldsr | cdsy | xwzz |`, }; async function handler(ctx) { let category = ctx.req.param('category'); - const dic_html = { tzgg: 'tzgg.htm', xyyw: 'xyyw.htm', mtbd: 'mtbd1.htm', xshd: 'xshd.htm' }; - const dic_title = { tzgg: '通知公告', xyyw: '校园要闻', mtbd: '媒体播报', xshd: '学术活动' }; + const dic_html = { xxxw: 'xxxw.htm', dzys: 'dzys.htm', ldsr: 'ldsr.htm', cdsy: 'cdsy.htm', xwzz: 'xwzz.htm' }; + const dic_title = { xxxw: '学校新闻', dzys: '砥志研思', ldsr: '立德树人', cdsy: '传道授业', xwzz: '校闻周知' }; // 设置默认值 if (dic_title[category] === undefined) { - category = 'tzgg'; + category = 'xxxw'; } const response = await got({ @@ -42,26 +43,18 @@ async function handler(ctx) { const data = response.body; const $ = load(data); - // 这个列表指通知公告详情列表 - const list = $('.newslist_block ul a') + const list = $('div.nlist ul li') .map((_, item) => { item = $(item); - const temp = item.find('span').text(); - // link原来长这样:'../info/1196/13990.htm' - const link = item.attr('href').replace(/^\.\./, 'http://www.xaut.edu.cn'); - let date = parseDate(temp.slice(-10, -1), 'YYYY-MM-DD'); - let title = temp.slice(0, -10); - - if (category === 'xshd') { - date = parseDate(temp.slice(-11, -1).replace('年', '-').replace('月', '-').replace('日', ''), 'YYYY-MM-DD'); - title = temp.slice(0, -11); - } + const link = item.find('a').attr('href').replace(/^\.\./, 'http://www.xaut.edu.cn'); + const pubDate = timezone(parseDate(item.find('div.time').text().trim()), +8); + const title = item.find('h5').text(); return { title, link, - pubDate: date, + pubDate, }; }) .get(); diff --git a/lib/routes/xaut/jwc.ts b/lib/routes/xaut/jwc.ts index 46206389c7fe95..80e1a81efd7cd4 100644 --- a/lib/routes/xaut/jwc.ts +++ b/lib/routes/xaut/jwc.ts @@ -20,13 +20,13 @@ export const route: Route = { name: '教务处', maintainers: ['mocusez'], handler, - description: `:::warning + description: `::: warning 有些内容需使用校园网或 VPN 访问知行网获取 - ::: +::: - | 通知公告 | 新闻动态 | 规章制度 | 竞赛结果公示 | 竞赛获奖通知 | 竞赛信息 | 公开公示 | - | :------: | :------: | :------: | :----------: | :----------: | :------: | :------: | - | tzgg | xwdt | gzzd | jggs | jsjg | jsxx | gkgs |`, +| 通知公告 | 新闻动态 | 规章制度 | 竞赛结果公示 | 竞赛获奖通知 | 竞赛信息 | 公开公示 | +| :------: | :------: | :------: | :----------: | :----------: | :------: | :------: | +| tzgg | xwdt | gzzd | jggs | jsjg | jsxx | gkgs |`, }; async function handler(ctx) { diff --git a/lib/routes/xaut/namespace.ts b/lib/routes/xaut/namespace.ts index ca23d46f71262d..f12c27718ab17d 100644 --- a/lib/routes/xaut/namespace.ts +++ b/lib/routes/xaut/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '西安理工大学', - url: 'index.xaut.edu.cn', + url: 'www.xaut.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/xaut/rsc.ts b/lib/routes/xaut/rsc.ts index 11c6e579a6afd8..b69894b6a0e07d 100644 --- a/lib/routes/xaut/rsc.ts +++ b/lib/routes/xaut/rsc.ts @@ -20,13 +20,13 @@ export const route: Route = { name: '人事处', maintainers: ['mocusez', 'light0926'], handler, - description: `:::warning + description: `::: warning 有些内容指向外部链接,目前只提供这些链接,不提供具体内容,去除 jwc 和 index 的修改 - ::: +::: - | 通知公告 | 工作动态 | - | :------: | :------: | - | tzgg | gzdt |`, +| 通知公告 | 工作动态 | +| :------: | :------: | +| tzgg | gzdt |`, }; async function handler(ctx) { diff --git a/lib/routes/xbmu/academic.ts b/lib/routes/xbmu/academic.ts new file mode 100644 index 00000000000000..adc99f71b0aef4 --- /dev/null +++ b/lib/routes/xbmu/academic.ts @@ -0,0 +1,91 @@ +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'; + +const BASE_URL = 'https://www.xbmu.edu.cn/xwzx/xsxx.htm'; + +const handler: Route['handler'] = async () => { + try { + // Fetch the academic page + const { data: listResponse } = await got(BASE_URL); + const $ = load(listResponse); + + // Select all list items containing academic information + const ITEM_SELECTOR = 'body > div.container.list-container.ny_mani > div > div.news_list > ul > li'; + const listItems = $(ITEM_SELECTOR); + + // Map through each list item to extract details + const academicLinkList = await Promise.all( + listItems.toArray().map((element) => { + const rawDate = $(element).find('span').text().trim(); + const [day, yearMonth] = rawDate.split('/').map((s) => s.trim()); + const formattedDate = parseDate(`${yearMonth}-${day}`).toUTCString(); + + const title = $(element).find('a').attr('title') || '学术信息'; + const relativeHref = $(element).find('a').attr('href') || ''; + const link = `https://www.xbmu.edu.cn/${relativeHref.replaceAll('../', '')}`; + + return { + date: formattedDate, + title, + link, + }; + }) + ); + + return { + title: '西北民族大学学术信息', + description: '西北民族大学近日学术信息', + link: BASE_URL, + image: 'http://210.26.0.114:9090/mdxg/img/weex/default_img.jpg', + item: (await Promise.all( + academicLinkList.map((item) => + cache.tryGet(item.link, async () => { + const CONTENT_SELECTOR = '#vsb_content > div'; + const { data: contentResponse } = await got(item.link); + const contentPage = load(contentResponse); + const content = contentPage(CONTENT_SELECTOR).html() || ''; + return { + title: item.title, + pubDate: item.date, + link: item.link, + description: content, + category: ['university'], + guid: item.link, + id: item.link, + image: 'http://210.26.0.114:9090/mdxg/img/weex/default_img.jpg', + content, + updated: item.date, + language: 'zh-cn', + }; + }) + ) + )) as DataItem[], + allowEmpty: true, + language: 'zh-cn', + feedLink: 'https://rsshub.app/xbmu/academic', + id: 'https://rsshub.app/xbmu/academic', + }; + } catch (error) { + throw new Error(`Error fetching academic information: ${error}`); + } +}; + +export const route: Route = { + path: '/academic', + name: '学术信息', + maintainers: ['PrinOrange'], + handler, + categories: ['university'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + example: '/xbmu/academic', +}; diff --git a/lib/routes/xbmu/announcement.ts b/lib/routes/xbmu/announcement.ts new file mode 100644 index 00000000000000..56d4486c0a701f --- /dev/null +++ b/lib/routes/xbmu/announcement.ts @@ -0,0 +1,91 @@ +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'; + +const BASE_URL = 'https://www.xbmu.edu.cn/xwzx/tzgg.htm'; + +const handler: Route['handler'] = async () => { + try { + // Fetch the announcements page + const { data: listResponse } = await got(BASE_URL); + const $ = load(listResponse); + + // Select all list items containing announcement information + const ITEM_SELECTOR = 'body > div.container.list-container.ny_mani > div > div.news_list > ul > li'; + const listItems = $(ITEM_SELECTOR); + + // Map through each list item to extract details + const announcementLinkList = await Promise.all( + listItems.toArray().map((element) => { + const rawDate = $(element).find('span').text().trim(); + const [day, yearMonth] = rawDate.split('/').map((s) => s.trim()); + const formattedDate = parseDate(`${yearMonth}-${day}`).toUTCString(); + + const title = $(element).find('a').attr('title') || '通知公告'; + const relativeHref = $(element).find('a').attr('href') || ''; + const link = `https://www.xbmu.edu.cn/${relativeHref.replaceAll('../', '')}`; + + return { + date: formattedDate, + title, + link, + }; + }) + ); + + return { + title: '西北民族大学通知公告', + description: '西北民族大学近日通知公告', + link: BASE_URL, + image: 'http://210.26.0.114:9090/mdxg/img/weex/default_img.jpg', + item: (await Promise.all( + announcementLinkList.map((item) => + cache.tryGet(item.link, async () => { + const CONTENT_SELECTOR = '#vsb_content > div'; + const { data: contentResponse } = await got(item.link); + const contentPage = load(contentResponse); + const content = contentPage(CONTENT_SELECTOR).html() || ''; + return { + title: item.title, + pubDate: item.date, + link: item.link, + description: content, + category: ['university'], + guid: item.link, + id: item.link, + image: 'http://210.26.0.114:9090/mdxg/img/weex/default_img.jpg', + content, + updated: item.date, + language: 'zh-cn', + }; + }) + ) + )) as DataItem[], + allowEmpty: true, + language: 'zh-cn', + feedLink: 'https://rsshub.app/xbmu/announcement', + id: 'https://rsshub.app/xbmu/announcement', + }; + } catch (error) { + throw new Error(`Error fetching announcements: ${error}`); + } +}; + +export const route: Route = { + path: '/announcement', + name: '通知公告', + maintainers: ['PrinOrange'], + handler, + categories: ['university'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + example: '/xbmu/announcement', +}; diff --git a/lib/routes/xbmu/namespace.ts b/lib/routes/xbmu/namespace.ts new file mode 100644 index 00000000000000..08c824cda68fa0 --- /dev/null +++ b/lib/routes/xbmu/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '西北民族大学', + url: 'www.xbmu.edu.cn', +}; diff --git a/lib/routes/xbookcn/blog.ts b/lib/routes/xbookcn/blog.ts new file mode 100644 index 00000000000000..98e90790131736 --- /dev/null +++ b/lib/routes/xbookcn/blog.ts @@ -0,0 +1,66 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/:label?', // 路由路径 + categories: ['reading'], // 分类 + example: '/xbookcn/精选作品', // 示例路径 + parameters: { label: '按名称分类,详见https://blog.xbookcn.net/p/all.html' }, // 参数 + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '短篇', // 源名称 + maintainers: ['Lyunvy'], // 维护者 + handler: async (ctx) => { + const { label = '精选作品' } = ctx.req.param(); // 从请求参数中获取 label + const url = `https://blog.xbookcn.net/search/label/${label}`; // 使用反引号构建 URL + const response = await ofetch(url); // 请求源链接 + const $ = load(response); // 加载 HTML + + const articles = $('.blog-posts.hfeed .date-outer').find('.post'); // 查找文章 + + const list = articles.toArray().map((elem) => { + const a = $(elem).find('.post-title a'); // 获取标题链接 + return { + title: a.text().trim(), // 标题 + link: a.attr('href'), // 链接 + category: [], // 分类 + }; + }); + + const items = await Promise.all( + list.map( + async (item) => + // 使用缓存以避免重复请求 + await cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); // 请求文章链接 + const $ = load(response); // 加载文章页面 + + // 获取文章的完整描述 + item.description = $('.post-body.entry-content').html() || '无内容'; // 抓取指定内容 + + // 获取分类信息 + const categories = $('.post-labels a') + .toArray() + .map((el) => $(el).text().trim()); + item.category = categories; // 添加多个分类信息 + + return item; // 返回带有描述和分类的文章对象 + }) + ) + ); + + return { + title: 'xbookcn', // 源标题 + link: url, // 源链接 + item: items, // 源文章 + }; + }, +}; diff --git a/lib/routes/xbookcn/namespace.ts b/lib/routes/xbookcn/namespace.ts new file mode 100644 index 00000000000000..b64c899641970a --- /dev/null +++ b/lib/routes/xbookcn/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中文成人文學網', + url: 'www.xbookcn.net', + lang: 'zh-TW', +}; diff --git a/lib/routes/xboxfan/namespace.ts b/lib/routes/xboxfan/namespace.ts index d6a2831f83f7a1..5fd937cbc59906 100644 --- a/lib/routes/xboxfan/namespace.ts +++ b/lib/routes/xboxfan/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '盒心光环', url: 'xboxfan.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xianbao/namespace.ts b/lib/routes/xianbao/namespace.ts index 2491698de29dc6..2d61760205b678 100644 --- a/lib/routes/xianbao/namespace.ts +++ b/lib/routes/xianbao/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '线报酷', url: 'new.xianbao.fun', + lang: 'zh-CN', }; diff --git a/lib/routes/xiaoheihe/discount.ts b/lib/routes/xiaoheihe/discount.ts index ee6afbd46af022..82ff45b028115e 100644 --- a/lib/routes/xiaoheihe/discount.ts +++ b/lib/routes/xiaoheihe/discount.ts @@ -1,5 +1,6 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; +import { calculate } from './util'; export const route: Route = { path: '/discount/:platform', @@ -18,25 +19,64 @@ export const route: Route = { maintainers: ['tssujt'], handler, description: `| PC | Switch | PSN | Xbox | - | ----- | ------ | ----- | ----- | - | pc | switch | psn | xbox |`, +| ----- | ------ | ----- | ----- | +| pc | switch | psn | xbox |`, }; const PLATFORM_MAP = { - pc: 'PC', - switch: 'Switch', - psn: 'PSN', - xbox: 'Xbox', + pc: { + key: 'pc', + desc: 'PC', + }, + switch: { + key: 'switch', + desc: 'Switch', + }, + psn: { + key: 'ps4', + desc: 'PSN', + }, + xbox: { + key: 'xbox', + desc: 'Xbox', + }, }; +function getDiscountDesc(discount) { + return `${(100 - discount) / 10}折`; +} + +function getLowestDesc(priceInfo, isSuperLowest = false) { + if (!('is_lowest' in priceInfo) || priceInfo.is_lowest === 0) { + return ''; + } else if (isSuperLowest) { + return '[超史低]'; + } else if (priceInfo.is_lowest && priceInfo.is_lowest === 1 && priceInfo.new_lowest && priceInfo.new_lowest === 1) { + return '[新史低]'; + } else if (priceInfo.is_lowest && priceInfo.is_lowest === 1) { + return '[史低]'; + } +} + +function getHeyboxPriceDesc(heyboxPriceInfo) { + if (heyboxPriceInfo.coupon_info) { + let discountPrice = heyboxPriceInfo.cost_coin / 1000; + discountPrice = discountPrice - heyboxPriceInfo.coupon_info.max_reduce; + const formatPrice = Number.isInteger(discountPrice) ? discountPrice.toFixed(0) : discountPrice.toFixed(2); + return `| 券后价: ${formatPrice} [${heyboxPriceInfo.coupon_info.coupon_desc}]`; + } else { + return ''; + } +} + async function handler(ctx) { - const platform = ctx.req.param('platform'); + const platformInfo = PLATFORM_MAP[ctx.req.param('platform')]; - const response = await got({ - method: 'get', - url: `https://api.xiaoheihe.cn/game/get_game_list_v3?sort_type=discount&filter_head=${platform}&offset=0&limit=30&os_type=web`, - }); - const data = response.data.result.games; + const dataUrl = calculate( + `https://api.xiaoheihe.cn/game/get_game_list_v3/?filter_head=${platformInfo.key}&offset=0&limit=30&os_type=web&app=heybox&client_type=mobile&version=999.0.3&x_client_type=web&x_os_type=Mac&x_app=heybox&heybox_id=-1&include_filter=-1` + ); + const response = await ofetch(dataUrl); + const data = response.result.games; const items = data.map((item) => { const title = `${item.name}${item.name_en ? '/' + item.name_en : ''}`; @@ -47,44 +87,48 @@ async function handler(ctx) { for (const platform of item.platform_infos) { if (platform.price) { if (platform.key) { - description += `平台: ${platform.key}<br/>>`; + description += `平台: ${platform.key.toUpperCase()}<br/>`; } if (platform.price.current) { - description += `当前价格: ${platform.price.current}${platform.price.discount === platform.price.lowest_discount ? '[史低]' : ''}<br/>`; + description += `当前价格: ${platform.price.current} ${getLowestDesc(platform.price)}<br/>`; } if (platform.price.initial) { description += `原价: ${platform.price.initial}<br/>`; } - if (platform.price.discount_desc) { - description += `折扣力度: ${platform.price.discount_desc}<br/>`; + if (platform.price.discount && platform.price.discount > 0) { + description += `折扣力度: ${getDiscountDesc(platform.price.discount)}<br/>`; } if (platform.price.deadline_date) { description += `截止时间: ${platform.price.deadline_date}<br/>`; } - description += '<br/>'; } } } else { if (item.price) { - description += `平台: ${platform.toUpperCase()}<br/>`; - if (item.price.discount) { - description += `折扣力度: ${(100 - item.price.discount) / 10}折<br/>`; - } - if (item.price.initial && item.price.discount) { - const current = Math.round((item.price.initial * (100 - item.price.discount)) / 100); - description += `当前价格: ${current}${item.price.discount === item.price.lowest_discount ? '[史低]' : ''}  `; + description += `平台: ${platformInfo.desc}<br/>`; + if (item.heybox_price) { + description += `当前价格: ${item.price.current} ${getHeyboxPriceDesc(item.heybox_price)} ${getLowestDesc(item.price, item.heybox_price.super_lowest)}<br/>`; + } else if (item.price.current) { + description += `当前价格: ${item.price.current} ${getLowestDesc(item.price)}<br/>`; } if (item.price.initial) { description += `原价: ${item.price.initial}<br/>`; } - if (item.score) { - description += `评分: ${item.score}<br/>`; + if (item.price.discount && item.price.discount > 0) { + description += `折扣力度: ${getDiscountDesc(item.price.discount)}<br/>`; + } + if (item.price.deadline_date) { + description += `截止时间: ${item.price.deadline_date}<br/>`; } - description += '<br/>'; } } + if (item.score) { + description += `评分: ${item.score}<br/>`; + } + description += '<br/>'; + let link = `https://api.xiaoheihe.cn/game/share_game_detail?appid=${item.steam_appid}`; - if (platform === 'pc') { + if (platformInfo.key === 'pc') { link = `https://store.steampowered.com/app/${item.steam_appid}`; } return { @@ -95,7 +139,7 @@ async function handler(ctx) { }); return { - title: `小黑盒 ${PLATFORM_MAP[platform]} 游戏折扣`, + title: `小黑盒 ${platformInfo.desc} 游戏折扣`, link: `https://xiaoheihe.cn`, item: items, }; diff --git a/lib/routes/xiaoheihe/namespace.ts b/lib/routes/xiaoheihe/namespace.ts index d38e39b49a9a2f..c9d88d34fd1eb5 100644 --- a/lib/routes/xiaoheihe/namespace.ts +++ b/lib/routes/xiaoheihe/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '小黑盒', url: 'xiaoheihe.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/xiaoheihe/news.ts b/lib/routes/xiaoheihe/news.ts index a48c9105a9b105..9c2e50316817b5 100644 --- a/lib/routes/xiaoheihe/news.ts +++ b/lib/routes/xiaoheihe/news.ts @@ -22,9 +22,10 @@ export const route: Route = { }; async function handler() { + const feedUrl = calculate(`https://api.xiaoheihe.cn/bbs/app/feeds/news?os_type=web&app=heybox&client_type=mobile&version=999.0.3&x_client_type=web&x_os_type=Mac&x_app=heybox&heybox_id=-1&appid=900018355&offset=0&limit=20`); const response = await got({ method: 'get', - url: `https://api.xiaoheihe.cn/bbs/app/feeds/news?lang=zh-cn&limit=20&offset=0&tag=-1&version=1.3.303`, + url: feedUrl, }); const data = response.data.result.links.filter((item) => item.linkid !== undefined); let items = data.map((item) => ({ diff --git a/lib/routes/xiaoheihe/util.ts b/lib/routes/xiaoheihe/util.ts index fe131fc1203852..1572ae782ca3bd 100644 --- a/lib/routes/xiaoheihe/util.ts +++ b/lib/routes/xiaoheihe/util.ts @@ -27,7 +27,7 @@ function c3(v) { } function convertByte(v) { - return v & 0x80 ? 0xff & ((v << 1) ^ 0x1b) : v << 1; + return v & 0x80 ? 0xFF & ((v << 1) ^ 0x1B) : v << 1; } /** diff --git a/lib/routes/xiaohongshu/namespace.ts b/lib/routes/xiaohongshu/namespace.ts index 6dc6c7cc712246..07cea7d73cd1cd 100644 --- a/lib/routes/xiaohongshu/namespace.ts +++ b/lib/routes/xiaohongshu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '小红书', url: 'xiaohongshu.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xiaohongshu/notes.ts b/lib/routes/xiaohongshu/notes.ts deleted file mode 100644 index fa02673228ee47..00000000000000 --- a/lib/routes/xiaohongshu/notes.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Route } from '@/types'; -import cache from '@/utils/cache'; -import { getNotes, formatText, formatNote } from './util'; - -export const route: Route = { - path: '/user/:user_id/notes/fulltext', - radar: [ - { - source: ['xiaohongshu.com/user/profile/:user_id'], - target: '/user/:user_id/notes', - }, - ], - name: 'Unknown', - maintainers: [], - handler, -}; - -async function handler(ctx) { - const userId = ctx.req.param('user_id'); - const url = `https://www.xiaohongshu.com/user/profile/${userId}`; - - const { user, notes } = await getNotes(url, cache); - - return { - title: `${user.nickname} - 笔记 • 小红书 / RED`, - description: formatText(user.desc), - image: user.imageb || user.images, - link: url, - item: notes.map((item) => formatNote(url, item)), - }; -} diff --git a/lib/routes/xiaohongshu/user.ts b/lib/routes/xiaohongshu/user.ts index 96b03405fb6bb4..bf082925f3f33c 100644 --- a/lib/routes/xiaohongshu/user.ts +++ b/lib/routes/xiaohongshu/user.ts @@ -1,35 +1,104 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; -import { getUser } from './util'; +import querystring from 'querystring'; +import { getUser, renderNotesFulltext, getUserWithCookie } from './util'; import InvalidParameterError from '@/errors/types/invalid-parameter'; - +import { config } from '@/config'; +import { fallback, queryToBoolean } from '@/utils/readable-social'; export const route: Route = { - path: '/user/:user_id/:category', - name: 'Unknown', - maintainers: [], + path: '/user/:user_id/:category/:routeParams?', + name: '用户笔记/收藏', + categories: ['social-media', 'popular'], + view: ViewType.Articles, + maintainers: ['lotosbin', 'howerhe', 'rien7', 'dddaniel1', 'pseudoyu'], handler, + radar: [ + { + source: ['xiaohongshu.com/user/profile/:user_id'], + target: '/user/:user_id/notes', + }, + ], + example: '/xiaohongshu/user/593032945e87e77791e03696/notes', + features: { + antiCrawler: true, + requirePuppeteer: true, + requireConfig: [ + { + name: 'XIAOHONGSHU_COOKIE', + optional: true, + description: '小红书 cookie 值,可在网络里面看到。', + }, + ], + }, + parameters: { + user_id: 'user id, length 24 characters', + category: { + description: 'category, notes or collect', + options: [ + { + value: 'notes', + label: 'notes', + }, + { + value: 'collect', + label: 'collect', + }, + ], + default: 'notes', + }, + routeParams: { + description: 'displayLivePhoto,`/user/:user_id/notes/displayLivePhoto=0`,不限时LivePhoto显示为图片,`/user/:user_id/notes/displayLivePhoto=1`,取值不为0时LivePhoto显示为视频', + default: '0', + }, + }, }; async function handler(ctx) { const userId = ctx.req.param('user_id'); const category = ctx.req.param('category'); + const routeParams = querystring.parse(ctx.req.param('routeParams')); + const displayLivePhoto = !!fallback(undefined, queryToBoolean(routeParams.displayLivePhoto), false); const url = `https://www.xiaohongshu.com/user/profile/${userId}`; + const cookie = config.xiaohongshu.cookie; + + if (cookie && category === 'notes') { + try { + const urlNotePrefix = 'https://www.xiaohongshu.com/explore'; + const user = await getUserWithCookie(url, cookie); + const notes = await renderNotesFulltext(user.notes, urlNotePrefix, displayLivePhoto); + return { + title: `${user.userPageData.basicInfo.nickname} - 笔记 • 小红书 / RED`, + description: user.userPageData.basicInfo.desc, + image: user.userPageData.basicInfo.imageb || user.userPageData.basicInfo.images, + link: url, + item: notes, + }; + } catch { + // Fallback to normal logic if cookie method fails + return await getUserFeeds(url, category); + } + } else { + return await getUserFeeds(url, category); + } +} +async function getUserFeeds(url: string, category: string) { const { userPageData: { basicInfo, interactions, tags }, notes, collect, } = await getUser(url, cache); - const title = `${basicInfo.nickname} - ${category === 'notes' ? '笔记' : '收藏'} • 小红书 / RED`; + const title = `${basicInfo.nickname} - 小红书${category === 'notes' ? '笔记' : '收藏'}`; const description = `${basicInfo.desc} ${tags.map((t) => t.name).join(' ')} ${interactions.map((i) => `${i.count} ${i.name}`).join(' ')}`; const image = basicInfo.imageb || basicInfo.images; const renderNote = (notes) => notes.flatMap((n) => - n.map(({ noteCard }) => ({ + n.map(({ id, noteCard }) => ({ title: noteCard.displayTitle, - link: `${url}/${noteCard.noteId}`, + link: new URL(noteCard.noteId || id, url).toString(), + guid: noteCard.noteId || id || noteCard.displayTitle, description: `<img src ="${noteCard.cover.infoList.pop().url}"><br>${noteCard.displayTitle}`, author: noteCard.user.nickname, upvotes: noteCard.interactInfo.likedCount, diff --git a/lib/routes/xiaohongshu/util.ts b/lib/routes/xiaohongshu/util.ts index 7e3cbd87f0c703..d35d48a0193f49 100644 --- a/lib/routes/xiaohongshu/util.ts +++ b/lib/routes/xiaohongshu/util.ts @@ -2,12 +2,38 @@ import { config } from '@/config'; import logger from '@/utils/logger'; import { parseDate } from '@/utils/parse-date'; import puppeteer from '@/utils/puppeteer'; +import { ofetch } from 'ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; + +// Common headers for requests +const getHeaders = (cookie?: string) => ({ + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'en-US,en;q=0.9', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + Host: 'www.xiaohongshu.com', + Pragma: 'no-cache', + 'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + ...(cookie ? { Cookie: cookie } : {}), +}); const getUser = (url, cache) => cache.tryGet( url, async () => { - const browser = await puppeteer(); + const browser = await puppeteer({ + stealth: true, + }); try { const page = await browser.newPage(); await page.setRequestInterception(true); @@ -21,7 +47,7 @@ const getUser = (url, cache) => }); await page.waitForSelector('div.reds-tab-item:nth-child(2)'); - const initialState = await page.evaluate(() => window.__INITIAL_STATE__); + const initialState = await page.evaluate(() => (window as any).__INITIAL_STATE__); if (!(await page.$('.lock-icon'))) { await page.click('div.reds-tab-item:nth-child(2)'); @@ -45,7 +71,7 @@ const getUser = (url, cache) => return { userPageData, notes, collect }; } finally { - browser.close(); + await browser.close(); } }, config.cache.routeExpire, @@ -66,113 +92,11 @@ const getBoard = (url, cache) => logger.http(`Requesting ${url}`); await page.goto(url); await page.waitForSelector('.pc-container'); - const initialSsrState = await page.evaluate(() => window.__INITIAL_SSR_STATE__); + const initialSsrState = await page.evaluate(() => (window as any).__INITIAL_SSR_STATE__); return initialSsrState.Main; - } finally { - browser.close(); - } - }, - config.cache.routeExpire, - false - ); - -const setPageFilter = async (page) => { - await page.setRequestInterception(true); - page.on('request', (req) => { - req.resourceType() === 'document' || req.resourceType() === 'script' || req.resourceType() === 'xhr' || req.resourceType() === 'other' ? req.continue() : req.abort(); - }); -}; - -const getNotes = (url, cache) => - cache.tryGet( - url + '/notes', // To avoid mixing with the cache for `user.js` - async () => { - let user = ''; - let notes = []; - - const browser = await puppeteer({ stealth: true }); - try { - const page = await browser.newPage(); - await setPageFilter(page); - - logger.http(`Requesting ${url}`); - await page.goto(url); - - let otherInfo = {}; - let userPosted = {}; - try { - [otherInfo, userPosted] = await Promise.all( - ['/api/sns/web/v1/user/otherinfo', '/api/sns/web/v1/user_posted'].map((p) => - page - .waitForResponse((res) => { - const req = res.request(); - return req.url().includes(p) && req.method() === 'GET'; - }) - .then((r) => r.json()) - ) - ); - } catch (error) { - throw new Error(`Could not get user information and note list\n${error}`); - } - - await page.close(); - - // Get full text for each note - const notesPromise = userPosted.data.notes.map((n) => { - const noteUrl = url + '/' + n.note_id; - - return cache.tryGet(noteUrl, async () => { - const notePage = await browser.newPage(); - await setPageFilter(notePage); - - logger.http(`Requesting ${noteUrl}`); - await notePage.goto(noteUrl); - - let feed = {}; - try { - feed = await notePage.evaluate(() => window.__INITIAL_STATE__); - - // Sometimes the page is not server-side rendered - if (feed?.note?.note === undefined || JSON.stringify(feed?.note?.note) === '{}') { - const res = await notePage.waitForResponse((res) => { - const req = res.request(); - return req.url().includes('/api/sns/web/v1/feed') && req.method() === 'POST'; - }); - - const json = await res.json(); - const note_card = json.data.items[0].note_card; - feed.note.note = { - title: note_card.title, - noteId: note_card.id, - desc: note_card.desc, - tagList: note_card.tag_list, - imageList: note_card.image_list, - user: note_card.user, - time: note_card.time, - lastUpdateTime: note_card.last_update_time, - }; - } - } catch (error) { - throw new Error(`Could not get note ${n.note_id}\n${error}`); - } - - await notePage.close(); - - if (feed?.note?.note !== undefined && JSON.stringify(feed?.note?.note) !== '{}') { - return feed.note.note; - } else { - throw new Error(`Could not get note ${n.note_id}`); - } - }); - }); - - user = otherInfo.data.basic_info; - notes = await Promise.all(notesPromise); } finally { await browser.close(); } - - return { user, notes }; }, config.cache.routeExpire, false @@ -194,4 +118,149 @@ const formatNote = (url, note) => ({ updated: parseDate(note.lastUpdateTime, 'x'), }); -export { getUser, getBoard, getNotes, formatText, formatNote }; +async function renderNotesFulltext(notes, urlPrex, displayLivePhoto) { + const data: Array<{ + title: string; + link: string; + description: string; + author: string; + guid: string; + pubDate: Date; + }> = []; + const promises = notes.flatMap((note) => + note.map(async ({ noteCard, id }) => { + const link = `${urlPrex}/${id}`; + const { title, description, pubDate } = await getFullNote(link, displayLivePhoto); + return { + title, + link, + description, + author: noteCard.user.nickName, + guid: noteCard.noteId, + pubDate, + }; + }) + ); + data.push(...(await Promise.all(promises))); + return data; +} + +async function getFullNote(link, displayLivePhoto) { + const data = (await cache.tryGet(link, async () => { + const res = await ofetch(link, { + headers: getHeaders(config.xiaohongshu.cookie), + }); + const $ = load(res); + const script = extractInitialState($); + const state = JSON.parse(script); + const note = state.note.noteDetailMap[state.note.firstNoteId].note; + const title = note.title; + let desc = note.desc; + desc = desc.replaceAll(/\[.*?\]/g, ''); + desc = desc.replaceAll(/#(.*?)#/g, '#$1'); + desc = desc.replaceAll('\n', '<br>'); + const pubDate = new Date(note.time); + + let mediaContent = ''; + if (note.type === 'video') { + const originVideoKey = note.video?.consumer?.originVideoKey; + const videoUrls: string[] = []; + + if (originVideoKey) { + videoUrls.push(`http://sns-video-al.xhscdn.com/${originVideoKey}`); + } + + const streamTypes = ['av1', 'h264', 'h265', 'h266']; + for (const type of streamTypes) { + const streams = note.video?.media?.stream?.[type]; + if (streams?.length > 0) { + const stream = streams[0]; + if (stream.masterUrl) { + videoUrls.push(stream.masterUrl); + } + if (stream.backupUrls?.length) { + videoUrls.push(...stream.backupUrls); + } + } + } + + const posterUrl = note.imageList?.[0]?.urlDefault; + + if (videoUrls.length > 0) { + mediaContent = `<video controls ${posterUrl ? `poster="${posterUrl}"` : ''}> + ${videoUrls.map((url) => `<source src="${url}" type="video/mp4">`).join('\n')} + </video><br>`; + } + } else { + mediaContent = note.imageList + .map((image) => { + if (image.livePhoto && displayLivePhoto) { + const videoUrls: string[] = []; + + const streamTypes = ['av1', 'h264', 'h265', 'h266']; + for (const type of streamTypes) { + const streams = image.stream?.[type]; + if (streams?.length > 0) { + if (streams[0].masterUrl) { + videoUrls.push(streams[0].masterUrl); + } + if (streams[0].backupUrls?.length) { + videoUrls.push(...streams[0].backupUrls); + } + } + } + + if (videoUrls.length > 0) { + return `<video controls poster="${image.urlDefault}"> + ${videoUrls.map((url) => `<source src="${url}" type="video/mp4">`).join('\n')} + </video>`; + } + } + return `<img src="${image.urlDefault}">`; + }) + .join('<br>'); + } + + const description = `${mediaContent}<br>${title}<br>${desc}`; + return { + title, + description, + pubDate, + }; + })) as Promise<{ title: string; description: string; pubDate: Date }>; + return data; +} + +async function getUserWithCookie(url: string, cookie: string) { + const res = await ofetch(url, { + headers: getHeaders(cookie), + }); + const $ = load(res); + const paths = $('#userPostedFeeds > section > div > a.cover.ld.mask').map((i, item) => item.attributes[3].value); + const script = extractInitialState($); + const state = JSON.parse(script); + let index = 0; + for (const item of state.user.notes.flat()) { + const path = paths[index]; + if (path && path.includes('?')) { + item.id = item.id + path?.substring(path.indexOf('?')); + } + index = index + 1; + } + return state.user; +} + +// Add helper function to extract initial state +function extractInitialState($) { + let script = $('script') + .filter((i, script) => { + const text = script.children[0]?.data; + return text?.startsWith('window.__INITIAL_STATE__='); + }) + .text(); + script = script.slice('window.__INITIAL_STATE__='.length); + script = script.replaceAll('undefined', 'null'); + return script; +} + +export { getUser, getBoard, formatText, formatNote, renderNotesFulltext, getFullNote, getUserWithCookie }; diff --git a/lib/routes/xiaomiyoupin/namespace.ts b/lib/routes/xiaomiyoupin/namespace.ts index affd534069e001..d454bb657750a6 100644 --- a/lib/routes/xiaomiyoupin/namespace.ts +++ b/lib/routes/xiaomiyoupin/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '小米有品', url: 'xiaomiyoupin.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xiaote/namespace.ts b/lib/routes/xiaote/namespace.ts index 014ac0445e8524..e8c71544662d24 100644 --- a/lib/routes/xiaote/namespace.ts +++ b/lib/routes/xiaote/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '小特社区', url: 'xiaote.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xiaoyuzhou/namespace.ts b/lib/routes/xiaoyuzhou/namespace.ts index 9e852ff18d1e86..4432d735111b52 100644 --- a/lib/routes/xiaoyuzhou/namespace.ts +++ b/lib/routes/xiaoyuzhou/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '小宇宙', url: 'xiaoyuzhoufm.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xiaoyuzhou/pickup.ts b/lib/routes/xiaoyuzhou/pickup.ts index 2c0aef6df5813b..bce0d11650395d 100644 --- a/lib/routes/xiaoyuzhou/pickup.ts +++ b/lib/routes/xiaoyuzhou/pickup.ts @@ -57,7 +57,7 @@ const ProcessFeed = async () => { return playList.map((item) => { const title = item.episode.title + ' - ' + item.episode.podcast.title; const eid = item.episode.eid; - const itunes_item_image = item.episode.image ? item.episode.image.picUrl : item.episode.podcast.image ? item.episode.podcast.image.picUrl : ''; + const itunes_item_image = item.episode.image ? item.episode.image.picUrl : (item.episode.podcast.image ? item.episode.podcast.image.picUrl : ''); const link = `https://www.xiaoyuzhoufm.com/episode/${eid}`; const pubDate = item.pubDate; const itunes_duration = item.episode.duration; diff --git a/lib/routes/xiaoyuzhou/podcast.ts b/lib/routes/xiaoyuzhou/podcast.ts index c208e89d9c951a..b87b210a16b147 100644 --- a/lib/routes/xiaoyuzhou/podcast.ts +++ b/lib/routes/xiaoyuzhou/podcast.ts @@ -1,13 +1,14 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; +import { Route, ViewType } from '@/types'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/podcast/:id', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Audios, example: '/xiaoyuzhou/podcast/6021f949a789fca4eff4492c', - parameters: { id: '播客id,可以在小宇宙播客的 URL 中找到' }, + parameters: { id: '播客 id 或单集 id,可以在小宇宙播客的 URL 中找到' }, features: { requireConfig: false, requirePuppeteer: false, @@ -18,25 +19,53 @@ export const route: Route = { }, radar: [ { - source: ['xiaoyuzhoufm.com/podcast/:id'], + source: ['xiaoyuzhoufm.com/podcast/:id', 'xiaoyuzhoufm.com/episode/:id'], }, ], name: '播客', - maintainers: ['hondajojo', 'jtsang4'], + maintainers: ['hondajojo', 'jtsang4', 'pseudoyu'], handler, url: 'xiaoyuzhoufm.com/', }; async function handler(ctx) { - const link = `https://www.xiaoyuzhoufm.com/podcast/${ctx.req.param('id')}`; - const response = await got({ - method: 'get', - url: link, - }); + const id = ctx.req.param('id'); + let link; + let response; + let $; + let page_data; - const $ = load(response.data); + // First try podcast URL, if that fails try episode URL + try { + link = `https://www.xiaoyuzhoufm.com/podcast/${id}`; + response = await ofetch(link); - const page_data = JSON.parse($('#__NEXT_DATA__')[0].children[0].data); + $ = load(response); + const nextDataElement = $('#__NEXT_DATA__').get(0); + page_data = JSON.parse(nextDataElement.children[0].data); + + // If no episodes found, we should try episode URL + if (!page_data.props.pageProps.podcast?.episodes) { + throw new Error('No episodes found in podcast data'); + } + } catch { + // Try as episode instead + link = `https://www.xiaoyuzhoufm.com/episode/${id}`; + response = await ofetch(link); + + $ = load(response); + const podcastLink = $('a[href^="/podcast/"].name').attr('href'); + + if (podcastLink) { + const podcastId = podcastLink.split('/').pop(); + link = `https://www.xiaoyuzhoufm.com/podcast/${podcastId}`; + response = await ofetch(link); + + $ = load(response); + const nextDataElement = $('#__NEXT_DATA__').get(0); + page_data = JSON.parse(nextDataElement.children[0].data); + } + } const episodes = page_data.props.pageProps.podcast.episodes.map((item) => ({ title: item.title, diff --git a/lib/routes/xiaozhuanlan/namespace.ts b/lib/routes/xiaozhuanlan/namespace.ts index e493b1c8507bbb..20d8b2ed4dd9ce 100644 --- a/lib/routes/xiaozhuanlan/namespace.ts +++ b/lib/routes/xiaozhuanlan/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '小专栏', url: 'xiaozhuanlan.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xidian/cs.ts b/lib/routes/xidian/cs.ts new file mode 100644 index 00000000000000..2760f263785ac8 --- /dev/null +++ b/lib/routes/xidian/cs.ts @@ -0,0 +1,143 @@ +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 baseUrl = 'https://cs.xidian.edu.cn'; + +const struct = { + xyxw: { + selector: { + list: '.n_wenzhang ul li', + }, + name: '主页-学院新闻', + path: '/xyxw', + }, + tzgg: { + selector: { + list: '.n_wenzhang ul li', + }, + name: '主页-通知公告', + path: '/tzgg', + }, + jlhz1: { + selector: { + list: '.n_wenzhang ul li', + }, + name: '主页-交流合作', + path: '/jlhz1', + }, + rsrc: { + selector: { + list: '.n_wenzhang ul li', + }, + name: '主页-人事人才', + path: 'rsrc', + }, + bkjy_jxxw: { + selector: { + list: '.n_wenzhang ul li', + }, + name: '主页-本科生教育 / 本科教育-教学新闻', + path: 'bkjy/jxxw', + }, + yjsjy_yjstz: { + selector: { + list: '.n_wenzhang ul li', + }, + name: '主页-研究生教育 / 研究生教育-研究生通知', + path: 'yjsjy/yjstz', + }, + jyzhaop: { + selector: { + list: '.n_wenzhang ul li', + }, + name: '主页-就业招聘', + path: 'jyzhaop', + }, +}; + +export const route: Route = { + path: '/cs/:category?', + categories: ['university'], + example: '/xidian/cs/xyxw', + parameters: { category: '通知类别,默认为主页-学院新闻' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '计算机科学与技术学院', + url: 'cs.xidian.edu.cn', + maintainers: ['ZiHao256'], + handler, + description: `| 文章来源 | 参数 | +| ---------------------- | ----------- | +| ✅主页-学院新闻 | xyxw | +| ✅主页-通知公告 | tzgg | +| ✅主页-交流合作 | jlhz1 | +| ✅主页-人事人才 | rsrc | +| ✅主页-本科生教育 / 本科教育-教学新闻 | bkjy_jxxw | +| ✅主页-研究生教育 / 研究生教育-研究生通知 | yjsjy_yjstz | +| ✅主页-就业招聘 | jyzhaop |`, + radar: [ + { + source: ['cs.xidian.edu.cn/'], + }, + ], +}; + +async function handler(ctx) { + const { category = 'xyxw' } = ctx.req.param(); + const url = `${baseUrl}/${struct[category].path}.htm`; + const response = await got(url, { + headers: { + referer: baseUrl, + }, + https: { + rejectUnauthorized: false, + }, + }); + + const $ = load(response.data); + + let items = $(struct[category].selector.list) + .toArray() + .map((item) => { + item = $(item); + return { + title: item.find('a').text(), + link: new URL(item.find('a').attr('href'), baseUrl).href, + pubDate: parseDate(item.find('span').text()), + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got(item.link, { + headers: { + referer: url, + }, + https: { + rejectUnauthorized: false, + }, + }); + const content = load(detailResponse.data); + content('.content-sxt').remove(); + item.description = content('[name="_newscontent_fromname"]').html(); + return item; + }) + ) + ); + + return { + title: $('title').text(), + link: url, + item: items, + }; +} diff --git a/lib/routes/xidian/gr.ts b/lib/routes/xidian/gr.ts new file mode 100644 index 00000000000000..3ece8bee034741 --- /dev/null +++ b/lib/routes/xidian/gr.ts @@ -0,0 +1,289 @@ +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 baseUrl = 'https://gr.xidian.edu.cn'; + +const struct = { + home_zxdt: { + selector: { + list: '.main-right-list ul li', + }, + name: '主页-最新动态', + path: '/zxdt', + }, + home_tzgg1: { + selector: { + list: '.main-right-list ul li', + }, + name: '主页-通知公告', + path: '/tzgg1', + }, + home_jzbg: { + selector: { + list: '.jzbg-list ul li', + }, + name: '主页-讲座报告', + path: '/jzbg', + }, + yyjs_jbqk: { + name: '研院介绍-基本情况', + path: '/yyjs/jbqk', + }, + yyjs_jbqk1: { + name: '研院介绍-机构设置', + path: '/yyjs/jbqk1', + }, + yyjs_jbqk2: { + name: '研院介绍-部门领导', + path: '/yyjs/jbqk2', + }, + yyjs_jbqk3: { + selector: { + list: '.main-right-list ul li', + }, + name: '研院介绍-服务指南', + path: '/yyjs/jbqk3', + }, + yyjs_jbqk4: { + name: '研院介绍-学院联系方式', + path: '/yyjs/jbqk4', + }, + yjsy: { + selector: { + list: '.main-right-list ul li', + }, + name: '招生信息', + path: '/yjsy', + }, + yjsy_yjszs: { + selector: { + list: '.main-right-list ul li', + }, + name: '招生信息-硕士研究生招生', + path: '/yjsy/yjszs', + }, + yjsy_bsyjszs: { + selector: { + list: '.main-right-list ul li', + }, + name: '招生信息-博士研究生招生', + path: '/yjsy/bsyjszs', + }, + yjsy_qtlxzs: { + selector: { + list: '.main-right-list ul li', + }, + name: '招生信息-其他类型招生', + path: '/yjsy/qtlxzs', + }, + pygl: { + selector: { + list: '.main-right-list ul li', + }, + name: '培养管理', + path: '/pygl', + }, + pygl_xsxw: { + selector: { + list: '.main-right-list ul li', + }, + name: '培养管理-学术学位', + path: 'pygl/pyfa1/xsxw1', + }, + pygl_zyxw: { + selector: { + list: '.main-right-list ul li', + }, + name: '培养管理-专业学位', + path: '/pygl/pyfa1/zyxw1', + }, + pygl_jxgl: { + selector: { + list: '.main-right-list ul li', + }, + name: '培养管理-教学管理', + path: '/pygl/jxgl', + }, + pygl_jxgl1: { + selector: { + list: '.main-right-list ul li', + }, + name: '培养管理-课程建设', + path: '/pygl/jxgl1', + }, + pygl_jxgl2: { + selector: { + list: '.main-right-list ul li', + }, + name: '培养管理-管理规定', + path: '/pygl/jxgl2', + }, + pygl_jxgl3: { + selector: { + list: '.main-right-list ul li', + }, + name: '培养管理-国际交流', + path: '/pygl/jxgl3', + }, + pygl_bslc: { + selector: { + list: '.main-right-list ul li', + }, + name: '培养管理-办事流程', + path: '/pygl/bslc', + }, + xwsy: { + selector: { + list: '.main-right-list ul li', + }, + name: '学位授予', + path: '/xwsy', + }, + xwsy_tzgg: { + selector: { + list: '.main-right-list ul li', + }, + name: '学位授予-通知公告', + path: '/xwsy/tzgg', + }, + xwsy_gzzd: { + selector: { + list: '.main-right-list ul li', + }, + name: '学位授予-规章制度', + path: '/xwsy/gzzd', + }, + xwsy_swmd: { + selector: { + list: '.main-right-list ul li', + }, + name: '学位授予-授位名单', + path: '/xwsy/swmd', + }, + xwsy_zlxz: { + selector: { + list: '.main-right-list ul li', + }, + name: '学位授予-资料下载', + path: '/xwsy/zlxz', + }, +}; + +export const route: Route = { + path: '/gr/:category?', + categories: ['university'], + example: '/xidian/gr/home_tzgg1', + parameters: { category: '通知类别,默认为主页-通知公告' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '研究生院/卓越工程师学院', + url: 'gr.xidian.edu.cn', + maintainers: ['ZiHao256'], + handler, + description: `| 文章来源 | 参数 | +| ------------- | ------------ | +| ✅主页-最新动态 | home_zxdt | +| ✅主页-通知公告 | home_tzgg1 | +| ✅主页-讲座报告 | home_jzbg | +| ✅研院介绍-基本情况 | yyjs_jbqk | +| ✅研院介绍-机构设置 | yyjs_jbqk1 | +| ✅研院介绍-部门领导 | yyjs_jbqk2 | +| ✅研院介绍-服务指南 | yyjs_jbqk3 | +| ✅研院介绍-学院联系方式 | yyjs_jbqk4 | +| ✅招生信息 | yjsy | +| ✅招生信息-硕士研究生招生 | yjsy_yjszs | +| ✅招生信息-博士研究生招生 | yjsy_bsyjszs | +| ✅招生信息-其他类型招生 | yjsy_qtlxzs | +| ✅培养管理 | pygl | +| ✅培养管理-学术学位 | pygl_xsxw | +| ✅培养管理-专业学位 | pygl_zyxw | +| ✅培养管理-教学管理 | pygl_jxgl | +| ✅培养管理-课程建设 | pygl_jxgl1 | +| ✅培养管理-管理规定 | pygl_jxgl2 | +| ✅培养管理-国际交流 | pygl_jxgl3 | +| ✅培养管理-办事流程 | pygl_bslc | +| ✅学位授予 | xwsy | +| ✅学位授予-通知公告 | xwsy_tzgg | +| ✅学位授予-规章制度 | xwsy_gzzd | +| ✅学位授予-授位名单 | xwsy_swmd | +| ✅学位授予-资料下载 | xwsy_zlxz |`, + radar: [ + { + source: ['gr.xidian.edu.cn/'], + }, + ], +}; + +async function handler(ctx) { + const { category = 'home_tzgg1' } = ctx.req.param(); + const url = `${baseUrl}/${struct[category].path}.htm`; + const response = await got(url, { + headers: { + referer: baseUrl, + }, + https: { + rejectUnauthorized: false, + }, + }); + + const $ = load(response.data); + + if (category === 'yyjs_jbqk' || category === 'yyjs_jbqk1' || category === 'yyjs_jbqk2' || category === 'yyjs_jbqk4') { + return { + title: $('.right-bt-left').text(), + link: url, + item: [ + { + title: $('.right-bt-left').text(), + link: url, + description: $('.main-right').html(), + }, + ], + }; + } else { + let items = $(struct[category].selector.list) + .toArray() + .map((item) => { + item = $(item); + return { + title: item.find('a').text(), + link: new URL(item.find('a').attr('href'), baseUrl).href, + pubDate: parseDate(item.find('span').text()), + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got(item.link, { + headers: { + referer: url, + }, + https: { + rejectUnauthorized: false, + }, + }); + const content = load(detailResponse.data); + content('.content-sxt').remove(); + item.description = content('[name="_newscontent_fromname"]').html(); + return item; + }) + ) + ); + + return { + title: $('title').text(), + link: url, + item: items, + }; + } +} diff --git a/lib/routes/xidian/jwc.ts b/lib/routes/xidian/jwc.ts index f985d9e5acd777..60b41068495b0e 100644 --- a/lib/routes/xidian/jwc.ts +++ b/lib/routes/xidian/jwc.ts @@ -19,11 +19,12 @@ export const route: Route = { supportScihub: false, }, name: '教务处', + url: 'jwc.xidian.edu.cn', maintainers: ['ShadowySpirits'], handler, description: `| 教学信息 | 教学研究 | 实践教学 | 质量监控 | 通知公告 | - | :------: | :------: | :------: | :------: | :------: | - | jxxx | jxyj | sjjx | zljk | tzgg |`, +| :------: | :------: | :------: | :------: | :------: | +| jxxx | jxyj | sjjx | zljk | tzgg |`, }; async function handler(ctx) { diff --git a/lib/routes/xidian/namespace.ts b/lib/routes/xidian/namespace.ts index 159fe8bce37e2b..3151cd849e35a7 100644 --- a/lib/routes/xidian/namespace.ts +++ b/lib/routes/xidian/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '西安电子科技大学', - url: 'jwc.xidian.edu.cn', + url: 'www.xidian.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ximalaya/album.ts b/lib/routes/ximalaya/album.ts index 519d5a7e3cef5b..4784d1db3fe2ed 100644 --- a/lib/routes/ximalaya/album.ts +++ b/lib/routes/ximalaya/album.ts @@ -1,11 +1,12 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; -import { getUrl, getRandom16 } from './utils'; +import { getRandom16, decryptUrl } from './utils'; const baseUrl = 'https://www.ximalaya.com'; import { config } from '@/config'; import { parseDate } from '@/utils/parse-date'; +import { RichIntro, TrackInfoResponse } from './types'; // Find category from: https://help.apple.com/itc/podcasts_connect/?lang=en#/itc9267a2f12 const categoryDict = { @@ -50,9 +51,8 @@ async function parseAlbumData(category, album_id) { // 这里的 {category} 并不重要,系统会准确的返回该 id 对应专辑的信息 // 现在 {category} 必须要精确到大的类别 // https://www.ximalaya.com/{category}/{album_id} - const response = await got(`${baseUrl}/${category}/${album_id}`); - const data = response.body; - const $ = load(data); + const response = await ofetch(`${baseUrl}/${category}/${album_id}`); + const $ = load(response); const stateElement = $('script') .toArray() .filter((element) => getTextFromElement(element).startsWith('window.__INITIAL_STATE__')); @@ -82,7 +82,7 @@ function judgeTrue(str, ...validStrings) { } export const route: Route = { - path: ['/:type/:id/:all?', '/:type/:id/:all/:shownote?'], + path: ['/:type/:id/:all/:shownote?'], categories: ['multimedia'], example: '/ximalaya/album/299146', parameters: { type: '专辑类型, 通常可以使用 `album`,可在对应专辑页面的 URL 中找到', id: '专辑 id, 可在对应专辑页面的 URL 中找到', all: '是否需要获取全部节目,填入 `1`、`true`、`all` 视为获取所有节目,填入其他则不获取。' }, @@ -99,16 +99,16 @@ export const route: Route = { supportPodcast: true, supportScihub: false, }, - name: '专辑(不输出 ShowNote)', + name: '专辑', maintainers: ['lengthmin', 'jjeejj', 'prnake'], handler, - description: `目前喜马拉雅的 API 只能一集一集的获取各节目上的 ShowNote,会极大的占用系统资源,所以默认为不获取节目的 ShowNote。下方有一个新的路径可选获取 ShowNote。 + description: `目前喜马拉雅的 API 只能一集一集的获取各节目上的 ShowNote,会极大的占用系统资源,所以默认为不获取节目的 ShowNote。 - :::warning +::: warning 专辑类型即 url 中的分类拼音,使用通用分类 \`album\` 通常是可行的,专辑 id 是跟在**分类拼音**后的那个 id, 不要输成某集的 id 了 **付费内容需要配置好已购买账户的 token 才能收听,详情见部署页面的配置模块** - :::`, +:::`, }; async function handler(ctx) { @@ -140,20 +140,26 @@ async function handler(ctx) { // const isAsc = albumData.store.AlbumDetailTrackList.sort === 0; // 喜马拉雅的 API 的 query 参数 isAsc=0 时才是升序,不写就是降序。 const trackInfoApi = `http://mobile.ximalaya.com/mobile/v1/album/track/?albumId=${id}&pageSize=${pageSize}&pageId=`; - const trackInfoResponse = await got(trackInfoApi + '1'); - const maxPageId = trackInfoResponse.data.data.maxPageId; // 最大页数 + const trackInfoResponse = await ofetch<TrackInfoResponse>(trackInfoApi + '1', { + parseResponse: JSON.parse, + }); + const maxPageId = trackInfoResponse.data.maxPageId; // 最大页数 - let playList = trackInfoResponse.data.data.list; + let playList = trackInfoResponse.data.list; if (shouldAll) { const promises = []; for (let i = 2; i <= maxPageId; i++) { // string + number -> string - promises.push(got(trackInfoApi + i)); + promises.push( + ofetch<TrackInfoResponse>(trackInfoApi + i, { + parseResponse: JSON.parse, + }) + ); } const responses = await Promise.all(promises); for (const j of responses) { - playList = [...playList, ...j.data.data.list]; + playList = [...playList, ...j.data.list]; } } @@ -164,8 +170,8 @@ async function handler(ctx) { let _desc; if (shouldShowNote) { const trackRichInfoApi = `https://mobile.ximalaya.com/mobile-track/richIntro?trackId=${item.trackId}`; - const trackRichInfoResponse = await got(trackRichInfoApi); - _desc = trackRichInfoResponse.data.richIntro; + const trackRichInfoResponse = await ofetch<RichIntro>(trackRichInfoApi); + _desc = trackRichInfoResponse.richIntro; } if (!_desc) { _desc = `<a href="${link}">在网页中查看</a>`; @@ -180,22 +186,24 @@ async function handler(ctx) { if (isPaid && token) { await Promise.all( playList.map(async (item) => { - const trackPayInfoApi = `https://mpay.ximalaya.com/mobile/track/pay/${item.trackId}/?device=pc`; + const timestamp = Math.floor(Date.now()); + const trackPayInfoApi = `https://www.ximalaya.com/mobile-playpage/track/v3/baseInfo/${timestamp}?device=www2&trackQualityLevel=2&trackId=${item.trackId}`; const data = await cache.tryGet('ximalaya:trackPayInfo' + trackPayInfoApi, async () => { - const trackPayInfoResponse = await got(trackPayInfoApi, { + const trackPayInfoResponse = await ofetch(trackPayInfoApi, { headers: { 'user-agent': 'ting_6.7.9(GM1900,Android29)', cookie: `1&_device=android&${randomToken}&6.7.9;1&_token=${token}`, }, }); + const trackInfo = trackPayInfoResponse.trackInfo; const _item = {}; - if (trackPayInfoResponse.data.ep) { - _item.playPathAacv224 = getUrl(trackPayInfoResponse.data); - } else if (trackPayInfoResponse.data.msg) { - _item.desc = item.desc + '<br/>' + trackPayInfoResponse.data.msg; + if (!trackInfo.isAuthorized) { + return _item; } + _item.playPathAacv224 = decryptUrl(trackInfo.playUrlList[0].url); return _item; }); + if (data.playPathAacv224) { item.playPathAacv224 = data.playPathAacv224; } diff --git a/lib/routes/ximalaya/namespace.ts b/lib/routes/ximalaya/namespace.ts index 46367a459dd1bd..e5e0c20550728a 100644 --- a/lib/routes/ximalaya/namespace.ts +++ b/lib/routes/ximalaya/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '喜马拉雅', url: 'ximalaya.com', + lang: 'zh-CN', }; diff --git a/lib/routes/ximalaya/types.ts b/lib/routes/ximalaya/types.ts new file mode 100644 index 00000000000000..c6606d7b79349d --- /dev/null +++ b/lib/routes/ximalaya/types.ts @@ -0,0 +1,68 @@ +interface Track { + trackId: number; + trackRecordId: number; + uid: number; + playUrl64: string; + playUrl32: string; + playPathAacv164: string; + playPathAacv224: string; + playPathHq: string; + title: string; + duration: number; + albumId: number; + albumTitle: string; + isPaid: boolean; + isFree: boolean; + isVideo: boolean; + isDraft: boolean; + isAuthorized: boolean; + priceTypeId: number; + sampleDuration: number; + priceTypeEnum: number; + type: number; + relatedId: number; + orderNo: number; + isHoldCopyright: boolean; + vipFirstStatus: number; + paidType: number; + isSample: boolean; + ximiFirstStatus: number; + coverLarge: string; + permissionSource: string; + playtimes: number; + labelType: number; + processState: number; + createdAt: number; + coverSmall: string; + coverMiddle: string; + nickname: string; + smallLogo: string; + userSource: number; + opType: number; + isPublic: boolean; + likes: number; + comments: number; + shares: number; + status: number; + videoCover: string; + intro: string; + labelList: string[]; + isTrailer: number; +} + +export interface TrackInfoResponse { + msg: string; + ret: number; + data: { + list: Track[]; + pageId: number; + pageSize: number; + maxPageId: number; + totalCount: number; + }; +} + +export interface RichIntro { + ret: number; + richIntro: string; +} diff --git a/lib/routes/ximalaya/utils.ts b/lib/routes/ximalaya/utils.ts index c5b01864f5c1e5..142e694dba455f 100644 --- a/lib/routes/ximalaya/utils.ts +++ b/lib/routes/ximalaya/utils.ts @@ -126,4 +126,41 @@ const getRandom16 = (len) => .toString('hex') .slice(0, len); -export { getUrl, getRandom16 }; +const decryptUrl = (encryptedUrl) => { + const o = [ + 183, 174, 108, 16, 131, 159, 250, 5, 239, 110, 193, 202, 153, 137, 251, 176, 119, 150, 47, 204, 97, 237, 1, 71, 177, 42, 88, 218, 166, 82, 87, 94, 14, 195, 69, 127, 215, 240, 225, 197, 238, 142, 123, 44, 219, 50, 190, 29, + 181, 186, 169, 98, 139, 185, 152, 13, 141, 76, 6, 157, 200, 132, 182, 49, 20, 116, 136, 43, 155, 194, 101, 231, 162, 242, 151, 213, 53, 60, 26, 134, 211, 56, 28, 223, 107, 161, 199, 15, 229, 61, 96, 41, 66, 158, 254, 21, 165, + 253, 103, 89, 3, 168, 40, 246, 81, 95, 58, 31, 172, 78, 99, 45, 148, 187, 222, 124, 55, 203, 235, 64, 68, 149, 180, 35, 113, 207, 118, 111, 91, 38, 247, 214, 7, 212, 209, 189, 241, 18, 115, 173, 25, 236, 121, 249, 75, 57, + 216, 10, 175, 112, 234, 164, 70, 206, 198, 255, 140, 230, 12, 32, 83, 46, 245, 0, 62, 227, 72, 191, 156, 138, 248, 114, 220, 90, 84, 170, 128, 19, 24, 122, 146, 80, 39, 37, 8, 34, 22, 11, 93, 130, 63, 154, 244, 160, 144, 79, + 23, 133, 92, 54, 102, 210, 65, 67, 27, 196, 201, 106, 143, 52, 74, 100, 217, 179, 48, 233, 126, 117, 184, 226, 85, 171, 167, 86, 2, 147, 17, 135, 228, 252, 105, 30, 192, 129, 178, 120, 36, 145, 51, 163, 77, 205, 73, 4, 188, + 125, 232, 33, 243, 109, 224, 104, 208, 221, 59, 9, + ]; + + const a = [204, 53, 135, 197, 39, 73, 58, 160, 79, 24, 12, 83, 180, 250, 101, 60, 206, 30, 10, 227, 36, 95, 161, 16, 135, 150, 235, 116, 242, 116, 165, 171]; + + const padding = '='.repeat((4 - (encryptedUrl.length % 4)) % 4); + const encrypted_data = Buffer.from(encryptedUrl.replace('_', '/').replace('-', '+') + padding, 'base64'); + if (encrypted_data.length < 16) { + return encryptedUrl; + } + const data = encrypted_data.slice(0, -16); + const iv = encrypted_data.slice(-16); + const decryptedData = new Uint8Array(data); + for (let i = 0; i < decryptedData.length; i++) { + decryptedData[i] = o[decryptedData[i]]; + } + for (let i = 0; i < decryptedData.length; i += 16) { + const block = decryptedData.slice(i, i + 16); + for (const [j, element] of block.entries()) { + decryptedData[i + j] = element ^ iv[j]; + } + } + for (let i = 0; i < decryptedData.length; i += 32) { + const block = decryptedData.slice(i, i + 32); + for (const [j, element] of block.entries()) { + decryptedData[i + j] = element ^ a[j]; + } + } + return Buffer.from(decryptedData).toString('utf8'); +}; +export { getUrl, getRandom16, decryptUrl }; diff --git a/lib/routes/xinhuanet/app.ts b/lib/routes/xinhuanet/app.ts new file mode 100644 index 00000000000000..12d9530414f23f --- /dev/null +++ b/lib/routes/xinhuanet/app.ts @@ -0,0 +1,109 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; + +export const handler = async (ctx) => { + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 7; + + const rootUrl = 'https://app.xinhuanet.com'; + const currentUrl = new URL('news/index.html', rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('a.article-item') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + const title = item.find('div.article-title').text(); + const guid = `xinhuanet-${item.prop('data-uuid')}`; + + return { + title, + link: item.prop('href'), + guid, + id: guid, + 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.article_title').text(); + const description = $$('#detail-content').html(); + + item.title = title; + item.description = description; + + const authorEl = $$('div.article_auth div').first(); + item.author = authorEl.text(); + + authorEl.remove(); + + item.pubDate = timezone(parseDate($$('div.article_auth div').first().text()), +8); + item.content = { + html: description, + text: $$('#detail-content').text(), + }; + item.language = language; + + return item; + }) + ) + ); + + const image = $('meta[itemprop="image"]').prop('content'); + + return { + title: $('title').text(), + description: $('meta[name="description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $('meta[itemprop="name"]').prop('content'), + language, + }; +}; + +export const route: Route = { + path: '/app', + name: '客户端', + url: 'app.xinhuanet.com', + maintainers: ['nczitzk'], + handler, + example: '/xinhuanet/app', + parameters: undefined, + description: '', + categories: ['traditional-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['app.xinhuanet.com'], + target: '/app', + }, + ], +}; diff --git a/lib/routes/xinhuanet/namespace.ts b/lib/routes/xinhuanet/namespace.ts new file mode 100644 index 00000000000000..ca6bda0947add9 --- /dev/null +++ b/lib/routes/xinhuanet/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '新华网', + url: 'xinhuanet.com', + categories: ['traditional-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/xinpianchang/index.ts b/lib/routes/xinpianchang/index.ts index 1a5568dcbef69e..261d1bdcf17fc0 100644 --- a/lib/routes/xinpianchang/index.ts +++ b/lib/routes/xinpianchang/index.ts @@ -18,11 +18,11 @@ export const route: Route = { name: '发现', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 跳转到欲订阅的分类页,将 URL 的 \`/discover\` 到末尾的部分填入 \`params\` 参数。 如 [全部原创视频作品](https://www.xinpianchang.com/discover/article-0-0-all-all-0-0-score) 的 URL 为 \`https://www.xinpianchang.com/discover/article-0-0-all-all-0-0-score\`,其 \`/discover\` 到末尾的部分为 \`article-0-0-all-all-0-0-score\`,所以对应的路由为 [/xinpianchang/discover/article-0-0-all-all-0-0-score](https://rsshub.app/xinpianchang/discover/article-0-0-all-all-0-0-score)。 - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/xinpianchang/namespace.ts b/lib/routes/xinpianchang/namespace.ts index d88eb922521a39..1bd6eafc61f7a7 100644 --- a/lib/routes/xinpianchang/namespace.ts +++ b/lib/routes/xinpianchang/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '新片场', url: 'xinpianchang.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xinpianchang/rank.ts b/lib/routes/xinpianchang/rank.ts index 4025e06b961bbe..58ab8ad7d93a82 100644 --- a/lib/routes/xinpianchang/rank.ts +++ b/lib/routes/xinpianchang/rank.ts @@ -21,13 +21,13 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 分类 | id | - | -------- | ---------- | - | 总榜 | all | - | 精选榜 | staffPicks | - | 广告 | ad | - | 宣传片 | publicity | - | 创意 | creative | - | 干货教程 | backstage |`, +| -------- | ---------- | +| 总榜 | all | +| 精选榜 | staffPicks | +| 广告 | ad | +| 宣传片 | publicity | +| 创意 | creative | +| 干货教程 | backstage |`, }; async function handler(ctx) { diff --git a/lib/routes/xjtu/2yuan/news.ts b/lib/routes/xjtu/2yuan/news.ts index 8305594049d475..f97e56dd8b72b7 100644 --- a/lib/routes/xjtu/2yuan/news.ts +++ b/lib/routes/xjtu/2yuan/news.ts @@ -22,17 +22,17 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 分类 | 编号 | - | -------- | ---- | - | 通知公告 | 110 | - | 综合新闻 | 6 | - | 科室动态 | 8 | - | 教学动态 | 45 | - | 科研动态 | 51 | - | 护理动态 | 57 | - | 党群活动 | 63 | - | 外事活动 | 13 | - | 媒体二院 | 14 | - | 理论政策 | 16 |`, +| -------- | ---- | +| 通知公告 | 110 | +| 综合新闻 | 6 | +| 科室动态 | 8 | +| 教学动态 | 45 | +| 科研动态 | 51 | +| 护理动态 | 57 | +| 党群活动 | 63 | +| 外事活动 | 13 | +| 媒体二院 | 14 | +| 理论政策 | 16 |`, }; async function handler(ctx) { diff --git a/lib/routes/xjtu/job.ts b/lib/routes/xjtu/job.ts index 2feec0c217e542..db1b3461e6e2bc 100644 --- a/lib/routes/xjtu/job.ts +++ b/lib/routes/xjtu/job.ts @@ -37,9 +37,9 @@ export const route: Route = { handler, description: `栏目类型 - | 中心公告 | 选调生 | 重点单位 | 国际组织 | 创新创业 | 就业实习 | - | -------- | ------ | -------- | -------- | -------- | -------- | - | zxgg | xds | zddw | gjzz | cxcy | jysx |`, +| 中心公告 | 选调生 | 重点单位 | 国际组织 | 创新创业 | 就业实习 | +| -------- | ------ | -------- | -------- | -------- | -------- | +| zxgg | xds | zddw | gjzz | cxcy | jysx |`, }; async function handler(ctx) { diff --git a/lib/routes/xjtu/namespace.ts b/lib/routes/xjtu/namespace.ts index 680fda43ba3716..42a32dd9549920 100644 --- a/lib/routes/xjtu/namespace.ts +++ b/lib/routes/xjtu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '西安交通大学', url: '2yuan.xjtu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/xjtu/std.ts b/lib/routes/xjtu/std.ts index bf9e5909c88e79..780277af3be622 100644 --- a/lib/routes/xjtu/std.ts +++ b/lib/routes/xjtu/std.ts @@ -27,8 +27,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 通知公告 | 重要通知 | 项目申报 | 成果申报 | 信息快讯 | - | -------- | -------- | -------- | -------- | -------- | - | | zytz | xmsb | cgsb | xxkx |`, +| -------- | -------- | -------- | -------- | -------- | +| | zytz | xmsb | cgsb | xxkx |`, }; async function handler(ctx) { diff --git a/lib/routes/xjtu/yz.ts b/lib/routes/xjtu/yz.ts new file mode 100644 index 00000000000000..2082d2628c6996 --- /dev/null +++ b/lib/routes/xjtu/yz.ts @@ -0,0 +1,74 @@ +import type { Data, DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import logger from '@/utils/logger'; +import timezone from '@/utils/timezone'; + +export const route: Route = { + path: '/yz/:category?', + categories: ['university'], + example: '/xjtu/yz/zsdt', + parameters: { category: '栏目类型,默认请求`zsdt`,详见下方表格' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['yz.xjtu.edu.cn/index/:category.htm'], + target: '/yz/:category', + }, + ], + name: '研究生招生信息网', + maintainers: ['YoghurtGuy'], + handler, + description: `栏目类型 + +| 招生动态 | 通知公告 | 政策法规 | 招生统计 | 历年复试线 | 博士招生 | 硕士招生 | 推免生 | 其他招生 | +| -------- | -------- | -------- | -------- | ---------- | -------- | -------- | ------ | -------- | +| zsdt | tzgg | zcfg | zstj | lnfsx | bszs | sszs | tms | qtzs |`, +}; +async function handler(ctx) { + const { category = 'zsdt' } = ctx.req.param(); + const baseUrl = 'https://yz.xjtu.edu.cn'; + + const response = await ofetch(`${baseUrl}/index/${category}.htm`); + const $ = load(response); + const list = $('div.list-con ul li') + .toArray() + .map((item_) => { + const item = $(item_); + const a = item.find('a'); + return { + title: a.attr('title'), + link: new URL(a.attr('href')!, baseUrl).href, + pubDate: timezone(parseDate(item.find('span.date.fr').text()), +8), + } as DataItem; + }); + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link!, async () => { + try { + const res = await ofetch(item.link!); + const content = load(res); + item.description = content('#vsb_content').html()! + (content('form ul').length > 0 ? content('form ul').html() : ''); + return item; + } catch (error) { + logger.error(`Fetch failed for ${item.link}:`, error); + return item; + } + }) + ) + ); + return { + title: '西安交通大学研究生招生信息网', + link: 'https://yz.xjtu.edu.cn', + item: items, + } as Data; +} diff --git a/lib/routes/xkb/index.ts b/lib/routes/xkb/index.ts index e5cd7a32ea5aa1..67ac526a3d1fec 100644 --- a/lib/routes/xkb/index.ts +++ b/lib/routes/xkb/index.ts @@ -27,13 +27,13 @@ export const route: Route = { handler, description: `常用栏目 ID: - | 栏目名 | ID | - | ------ | --- | - | 首页 | 350 | - | 重点 | 359 | - | 广州 | 353 | - | 湾区 | 360 | - | 天下 | 355 |`, +| 栏目名 | ID | +| ------ | --- | +| 首页 | 350 | +| 重点 | 359 | +| 广州 | 353 | +| 湾区 | 360 | +| 天下 | 355 |`, }; async function handler(ctx) { @@ -44,7 +44,7 @@ async function handler(ctx) { method: 'get', url: currentUrl, headers: { - siteId: '35', + siteId: 35, }, }); diff --git a/lib/routes/xkb/namespace.ts b/lib/routes/xkb/namespace.ts index 185044dfcebec7..25fe66a0259823 100644 --- a/lib/routes/xkb/namespace.ts +++ b/lib/routes/xkb/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '新快报', url: 'xkb.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/xmanhua/namespace.ts b/lib/routes/xmanhua/namespace.ts index 476dfd8936c336..60b6bdf30106f6 100644 --- a/lib/routes/xmanhua/namespace.ts +++ b/lib/routes/xmanhua/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'X 漫画', url: 'xmanhua.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xmnn/epaper.ts b/lib/routes/xmnn/epaper.ts index 62af3aebe29ec6..47125f0db7ab36 100644 --- a/lib/routes/xmnn/epaper.ts +++ b/lib/routes/xmnn/epaper.ts @@ -28,8 +28,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 厦门日报 | 厦门晚报 | 海西晨报 | 城市捷报 | - | -------- | -------- | -------- | -------- | - | xmrb | xmwb | hxcb | csjb |`, +| -------- | -------- | -------- | -------- | +| xmrb | xmwb | hxcb | csjb |`, }; async function handler(ctx) { diff --git a/lib/routes/xmnn/namespace.ts b/lib/routes/xmnn/namespace.ts index 83bed84171d9fc..6b7a08ba35c5bf 100644 --- a/lib/routes/xmnn/namespace.ts +++ b/lib/routes/xmnn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '厦门网', url: 'epaper.xmnn.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/xmu/kydt.ts b/lib/routes/xmu/kydt.ts new file mode 100644 index 00000000000000..ac6c0be51cedf8 --- /dev/null +++ b/lib/routes/xmu/kydt.ts @@ -0,0 +1,68 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/kydt', + categories: ['university'], + example: '/xmu/kydt', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['soe.xmu.edu.cn/kxyj/kydt.htm'], + }, + ], + name: '科研动态', + maintainers: ['linsenwang'], + handler, +}; + +async function handler() { + const host = 'https://soe.xmu.edu.cn/kxyj/kydt.htm'; + const response = await ofetch(host); + const $ = load(response); + + const list = $('div.news li') + .toArray() + .map((item) => { + item = $(item); + const title = item.find('h4').first().text(); + const time = item.find('h6').first().text(); + const a = item.find('a').first().attr('href'); + const fullUrl = new URL(a, host).href; + + return { + title, + link: fullUrl, + pubDate: time, + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + + item.description = $('.v_news_content').first().html(); + + return item; + }) + ) + ); + + return { + allowEmpty: true, + title: '厦门大学经济学院科研动态', + link: host, + item: items, + }; +} diff --git a/lib/routes/xmu/namespace.ts b/lib/routes/xmu/namespace.ts new file mode 100644 index 00000000000000..c5725efa5d0d99 --- /dev/null +++ b/lib/routes/xmu/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Xiamen University', + url: 'soe.xmu.edu.cn', + zh: { + name: '厦门大学经济学院', + }, +}; diff --git a/lib/routes/xmut/namespace.ts b/lib/routes/xmut/namespace.ts index 53ad878f1aa93c..70b636f10b0032 100644 --- a/lib/routes/xmut/namespace.ts +++ b/lib/routes/xmut/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '厦门理工大学', url: 'jwc.xmut.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/xsijishe/forum.ts b/lib/routes/xsijishe/forum.ts index fe332093ca7c13..9cfd086ecec08d 100644 --- a/lib/routes/xsijishe/forum.ts +++ b/lib/routes/xsijishe/forum.ts @@ -3,6 +3,7 @@ import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; +import { config } from '@/config'; const baseUrl = 'https://xsijishe.com'; export const route: Route = { @@ -11,7 +12,16 @@ export const route: Route = { example: '/xsijishe/forum/51', parameters: { fid: '子论坛 id' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'XSIJISHE_COOKIE', + description: '', + }, + { + name: 'XSIJISHE_USER_AGENT', + description: '', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -21,15 +31,23 @@ export const route: Route = { name: '论坛', maintainers: ['akynazh'], handler, - description: `:::tip 关于子论坛 id 的获取方法 + description: `::: tip 关于子论坛 id 的获取方法 \`/xsijishe/forum/51\` 对应于论坛 \`https://xsijishe.com/forum-51-1.html\`,这个论坛的 fid 为 51,也就是 \`forum-{fid}-1\` 中的 fid。 - :::`, +:::`, }; async function handler(ctx) { const fid = ctx.req.param('fid'); const url = `${baseUrl}/forum-${fid}-1.html`; - const resp = await got(url); + const headers = { + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + Cookie: config.xsijishe.cookie, + 'User-Agent': config.xsijishe.userAgent, + }; + const resp = await got(url, { + headers, + }); const $ = load(resp.data); const forumCategory = $('.nex_bkinterls_top .nex_bkinterls_ls a').text(); let items = $('[id^="normalthread"]') @@ -51,7 +69,9 @@ async function handler(ctx) { items = await Promise.all( items.map((item) => cache.tryGet(item.link, async () => { - const resp = await got(item.link); + const resp = await got(item.link, { + headers, + }); const $ = load(resp.data); const firstViewBox = $('.t_f').first(); diff --git a/lib/routes/xsijishe/namespace.ts b/lib/routes/xsijishe/namespace.ts index 96d4232a74a239..222007b13ab2c7 100644 --- a/lib/routes/xsijishe/namespace.ts +++ b/lib/routes/xsijishe/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '司机社', url: 'xsijishe.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xsijishe/rank.ts b/lib/routes/xsijishe/rank.ts index fd95af2be76980..7ab8471d8d9533 100644 --- a/lib/routes/xsijishe/rank.ts +++ b/lib/routes/xsijishe/rank.ts @@ -3,75 +3,153 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; +import { config } from '@/config'; +import { puppeteerGet } from './utils'; +import puppeteer from '@/utils/puppeteer'; + const baseUrl = 'https://xsijishe.com'; export const route: Route = { path: '/rank/:type', - categories: ['bbs'], + categories: ['bbs', 'popular'], example: '/xsijishe/rank/weekly', - parameters: { type: '排行榜类型: weekly | monthly' }, + parameters: { + type: { + description: '排行榜类型', + options: [ + { value: 'weekly', label: '周榜' }, + { value: 'monthly', label: '月榜' }, + ], + }, + }, features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, + requireConfig: [ + { + name: 'XSIJISHE_COOKIE', + description: '', + }, + { + name: 'XSIJISHE_USER_AGENT', + description: '', + }, + ], + requirePuppeteer: true, + antiCrawler: true, supportBT: false, supportPodcast: false, supportScihub: false, }, name: '排行榜', - maintainers: ['akynazh'], + maintainers: ['akynazh', 'AiraNadih'], handler, }; async function handler(ctx) { const rankType = ctx.req.param('type'); let title; - let rankId; + let index; // 用于选择第几个 li + if (rankType === 'weekly') { title = '司机社综合周排行榜'; - rankId = 'nex_recons_demens'; + index = 0; // 第一个 li 是周榜 } else if (rankType === 'monthly') { title = '司机社综合月排行榜'; - rankId = 'nex_recons_demens1'; + index = 1; // 第二个 li 是月榜 } else { throw new InvalidParameterError('Invalid rank type'); } + + const browser = await puppeteer(); + let usePuppeteer = false; + const url = `${baseUrl}/portal.php`; - const resp = await got(url); + const headers = { + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + Cookie: config.xsijishe.cookie, + 'User-Agent': config.xsijishe.user_agent, + }; + + const resp = await got(url, { + headers, + }); + + const redirectMatch = resp.data.match(/window\.location\.href\s*=\s*"([^"]+)"/); + if (redirectMatch) { + const redirectUrl = `${baseUrl}${redirectMatch[1]}`; + // 使用提取到的地址重新请求 + const realResp = await got(redirectUrl, { + headers, + }); + resp.data = realResp.data; + } + const $ = load(resp.data); - let items = $(`#${rankId} dd`) + // 根据 index 选择对应的 li,然后获取其中的 dd 元素 + let items = $('.nex_recon_lists ul li') + .eq(index) + .find('.nex_recons_demens dl dd') .toArray() .map((item) => { item = $(item); const title = item.find('h5').text().trim(); const link = item.find('a').attr('href'); + const description = item.find('img').prop('outerHTML'); return { title, link: `${baseUrl}/${link}`, + description, }; }); + + if (items.length > 0) { + const firstItem = items[0]; + const resp = await got(firstItem.link, { + headers, + }); + const $ = load(resp.data); + const firstViewBox = $('.t_f').first(); + if (firstViewBox.length === 0) { + usePuppeteer = true; + } + } + items = await Promise.all( items.map((item) => cache.tryGet(item.link, async () => { - const resp = await got(item.link); - const $ = load(resp.data); + let data; + if (usePuppeteer) { + data = await puppeteerGet(item.link, browser); + } else { + const resp = await got(item.link, { + headers, + }); + data = resp.data; + } + const $ = load(data); const firstViewBox = $('.t_f').first(); - firstViewBox.find('img').each((_, img) => { - img = $(img); - if (img.attr('zoomfile')) { - img.attr('src', img.attr('zoomfile')); - img.removeAttr('zoomfile'); - img.removeAttr('file'); - } - img.removeAttr('onmouseover'); - }); - - item.description = firstViewBox.html(); + if (firstViewBox.length === 1) { + firstViewBox.find('img').each((_, img) => { + img = $(img); + if (img.attr('zoomfile')) { + img.attr('src', img.attr('zoomfile')); + img.removeAttr('zoomfile'); + img.removeAttr('file'); + } + img.removeAttr('onmouseover'); + }); + + item.description = firstViewBox.html(); + } + return item; }) ) ); + + await browser.close(); + return { title, link: url, diff --git a/lib/routes/xsijishe/utils.ts b/lib/routes/xsijishe/utils.ts new file mode 100644 index 00000000000000..70cdb678239185 --- /dev/null +++ b/lib/routes/xsijishe/utils.ts @@ -0,0 +1,25 @@ +const puppeteerGet = async (url, browser) => { + const page = await browser.newPage(); + const expectResourceTypes = new Set(['document', 'script']); + await page.setRequestInterception(true); + page.on('request', (request) => { + expectResourceTypes.has(request.resourceType()) ? request.continue() : request.abort(); + }); + await page.goto(url, { + waitUntil: 'domcontentloaded', + }); + + let html; + html = await page.evaluate(() => document.documentElement.innerHTML); + if (html.includes('抱歉,您尚未登录,没有权限访问该版块')) { + await page.close(); + return html; + } + + await page.waitForSelector('.t_f'); + html = await page.evaluate(() => document.documentElement.innerHTML); + await page.close(); + return html; +}; + +export { puppeteerGet }; diff --git a/lib/routes/xueqiu/column.ts b/lib/routes/xueqiu/column.ts index b2053248cd949f..b7bd7c00fcca42 100644 --- a/lib/routes/xueqiu/column.ts +++ b/lib/routes/xueqiu/column.ts @@ -14,7 +14,7 @@ export const route: Route = { features: { requireConfig: false, requirePuppeteer: false, - antiCrawler: false, + antiCrawler: true, supportBT: false, supportPodcast: false, supportScihub: false, @@ -25,7 +25,7 @@ export const route: Route = { }, ], name: '用户专栏', - maintainers: ['TonyRL'], + maintainers: ['TonyRL', 'pseudoyu'], handler, }; @@ -33,6 +33,11 @@ async function handler(ctx) { const id = ctx.req.param('id'); const pageUrl = `${baseUrl}/${id}/column`; + // Get cookie first + await got(baseUrl, { + cookieJar, + }); + const pageData = await got(pageUrl, { cookieJar, }); @@ -49,6 +54,10 @@ async function handler(ctx) { }, }); + if (!data.list) { + throw new Error('Error occurred, please refresh the page or try again after logging back into your account'); + } + const items = data.list.map((item) => ({ title: item.title, description: item.description, diff --git a/lib/routes/xueqiu/cookies.ts b/lib/routes/xueqiu/cookies.ts index 175296ee8bed87..7e11d5218a3fa9 100644 --- a/lib/routes/xueqiu/cookies.ts +++ b/lib/routes/xueqiu/cookies.ts @@ -1,14 +1,24 @@ -import ofetch from '@/utils/ofetch'; import cache from '@/utils/cache'; import { config } from '@/config'; +import puppeteer from '@/utils/puppeteer'; +import { getCookies } from '@/utils/puppeteer-utils'; -export const parseToken = () => +export const parseToken = (link: string) => cache.tryGet( 'xueqiu:token', async () => { - const res = await ofetch.raw(`https://xueqiu.com`); - const cookieArray = res.headers.getSetCookie(); - return cookieArray.find((c) => c.startsWith('xq_a_token=')); + const browser = await puppeteer({ stealth: true }); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (request) => { + request.resourceType() === 'document' ? request.continue() : request.abort(); + }); + await page.goto(link, { + waitUntil: 'domcontentloaded', + }); + await page.evaluate(() => document.documentElement.innerHTML); + const cookies = await getCookies(page); + return cookies; }, config.cache.routeExpire, false diff --git a/lib/routes/xueqiu/favorite.ts b/lib/routes/xueqiu/favorite.ts index 9bb49dfc80204e..cb1e6197138a42 100644 --- a/lib/routes/xueqiu/favorite.ts +++ b/lib/routes/xueqiu/favorite.ts @@ -3,6 +3,7 @@ import got from '@/utils/got'; import queryString from 'query-string'; import { parseDate } from '@/utils/parse-date'; import { parseToken } from '@/routes/xueqiu/cookies'; +import ofetch from '@/utils/ofetch'; export const route: Route = { path: '/favorite/:id', @@ -29,7 +30,9 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const token = await parseToken(); + + const link = `https://xueqiu.com/u/${id}`; + const token = await parseToken(link); const res2 = await got({ method: 'get', url: 'https://xueqiu.com/favorites.json', @@ -38,20 +41,33 @@ async function handler(ctx) { }), headers: { Cookie: token, - Referer: `https://xueqiu.com/u/${id}`, + Referer: link, }, }); const data = res2.data.list; + const { + user: { screen_name }, + } = await ofetch('https://xueqiu.com/statuses/original/show.json', { + query: { + user_id: id, + }, + headers: { + Cookie: token, + Referer: link, + }, + }); + return { - title: `ID: ${id} 的雪球收藏动态`, - link: `https://xueqiu.com/u/${id}`, - description: `ID: ${id} 的雪球收藏动态`, + title: `${screen_name} 的雪球收藏动态`, + link, + description: `${screen_name} 的雪球收藏动态`, item: data.map((item) => ({ title: item.title, description: item.description, pubDate: parseDate(item.created_at), link: `https://xueqiu.com${item.target}`, })), + allowEmpty: true, }; } diff --git a/lib/routes/xueqiu/hots.ts b/lib/routes/xueqiu/hots.ts index dc20dd40f6be15..ffaf9d52057b29 100644 --- a/lib/routes/xueqiu/hots.ts +++ b/lib/routes/xueqiu/hots.ts @@ -30,7 +30,7 @@ export const route: Route = { }; async function handler() { - const token = await parseToken(); + const token = await parseToken('https://xueqiu.com'); const res2 = await got({ method: 'get', url: 'https://xueqiu.com/statuses/hots.json', @@ -44,20 +44,20 @@ async function handler() { }), headers: { Cookie: token, - Referer: `https://xueqiu.com/`, + Referer: 'https://xueqiu.com/', }, }); const data = res2.data; return { - title: `热帖 - 雪球`, - link: `https://xueqiu.com/`, - description: `雪球热门帖子`, + title: '热帖 - 雪球', + link: 'https://xueqiu.com/', + description: '雪球热门帖子', item: data.map((item) => { const description = item.text; return { title: item.title ?? sanitizeHtml(description, { allowedTags: [], allowedAttributes: {} }), - description: item.text, + description, pubDate: parseDate(item.created_at), link: `https://xueqiu.com${item.target}`, author: item.user.screen_name, diff --git a/lib/routes/xueqiu/namespace.ts b/lib/routes/xueqiu/namespace.ts index ede7826599ebea..dfba531b76ee3d 100644 --- a/lib/routes/xueqiu/namespace.ts +++ b/lib/routes/xueqiu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '雪球', url: 'danjuanapp.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xueqiu/stock-info.ts b/lib/routes/xueqiu/stock-info.ts index 67b232eb536c2e..cc3ec8032c0351 100644 --- a/lib/routes/xueqiu/stock-info.ts +++ b/lib/routes/xueqiu/stock-info.ts @@ -29,8 +29,8 @@ export const route: Route = { maintainers: ['YuYang'], handler, description: `| 公告 | 新闻 | 研报 | - | ------------ | ---- | -------- | - | announcement | news | research |`, +| ------------ | ---- | -------- | +| announcement | news | research |`, }; async function handler(ctx) { @@ -46,12 +46,13 @@ async function handler(ctx) { }; const source = typename[type]; + const link = `https://xueqiu.com/S/${id}`; const res1 = await got({ method: 'get', - url: `https://xueqiu.com/S/${id}`, + url: link, }); - const token = await parseToken(); + const token = await parseToken(link); const $ = load(res1.data); // 使用 cheerio 加载返回的 HTML const stock_name = $('.stock-name').text().split('(')[0]; @@ -73,26 +74,20 @@ async function handler(ctx) { }), headers: { Cookie: token, - Referer: `https://xueqiu.com/u/${id}`, + Referer: link, }, }); const data = res2.data.list; return { title: `${id} ${stock_name} - ${source}`, - link: `https://xueqiu.com/S/${id}`, + link, description: `${stock_name} - ${source}`, - item: data.map((item) => { - let link = `https://xueqiu.com${item.target}`; - if (item.quote_cards) { - link = item.quote_cards[0].target_url; - } - return { - title: item.title || sanitizeHtml(item.description, { allowedTags: [], allowedAttributes: {} }), - description: item.description, - pubDate: parseDate(item.created_at), - link, - }; - }), + item: data.map((item) => ({ + title: item.title || sanitizeHtml(item.description, { allowedTags: [], allowedAttributes: {} }), + description: item.description, + pubDate: parseDate(item.created_at), + link: `https://xueqiu.com${item.target}`, + })), }; } diff --git a/lib/routes/xueqiu/timeline.ts b/lib/routes/xueqiu/timeline.ts index 1b47e38dfa1261..37c97b08d3e1b6 100644 --- a/lib/routes/xueqiu/timeline.ts +++ b/lib/routes/xueqiu/timeline.ts @@ -26,13 +26,13 @@ export const route: Route = { name: '用户关注时间线', maintainers: ['ErnestDong'], handler, - description: `:::warning + description: `::: warning 用户关注动态需要登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 - ::: +::: - | -1 | -2 | 1 | - | ---- | -------- | ------------- | - | 全部 | 关注精选 | 自定义第 1 组 |`, +| -1 | -2 | 1 | +| ---- | -------- | ------------- | +| 全部 | 关注精选 | 自定义第 1 组 |`, }; async function handler(ctx) { diff --git a/lib/routes/xueqiu/today.ts b/lib/routes/xueqiu/today.ts index d262a341d53908..2235d017eec324 100644 --- a/lib/routes/xueqiu/today.ts +++ b/lib/routes/xueqiu/today.ts @@ -36,7 +36,7 @@ async function handler(ctx) { const currentUrl = `${rootUrl}/today`; const apiUrl = `${rootUrl}/statuses/hot/listV2.json?since_id=-1&size=${size}`; - const token = await parseToken(); + const token = await parseToken(currentUrl); const response = await got({ method: 'get', url: apiUrl, diff --git a/lib/routes/xueqiu/user-stock.ts b/lib/routes/xueqiu/user-stock.ts index 02e6490b350508..97c9c0ca834106 100644 --- a/lib/routes/xueqiu/user-stock.ts +++ b/lib/routes/xueqiu/user-stock.ts @@ -1,5 +1,7 @@ import { Route } from '@/types'; import ofetch from '@/utils/ofetch'; +import { config } from '@/config'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { @@ -8,7 +10,12 @@ export const route: Route = { example: '/xueqiu/user_stock/1247347556', parameters: { id: '用户 id, 可在用户主页 URL 中找到' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'XUEQIU_COOKIES', + description: '', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -23,22 +30,24 @@ export const route: Route = { name: '用户自选动态', maintainers: ['hillerliao'], handler, + description: `::: warning + 用户自选动态需要登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 +:::`, }; async function handler(ctx) { - const id = ctx.req.param('id'); + const cookie = config.xueqiu.cookies; + if (cookie === undefined) { + throw new ConfigNotFoundError('缺少雪球用户登录后的 Cookie 值'); + } - const { headers } = await ofetch.raw('https://xueqiu.com/'); - const token = headers - ?.getSetCookie() - .find((s) => s.startsWith('xq_a_token=')) - ?.split(';')[0] as string; + const id = ctx.req.param('id'); const { data: { stocks: data }, } = await ofetch(`https://stock.xueqiu.com/v5/stock/portfolio/stock/list.json?category=1&size=1000&uid=${id}`, { headers: { - Cookie: token, + Cookie: cookie, Referer: `https://xueqiu.com/u/${id}`, }, }); @@ -50,7 +59,7 @@ async function handler(ctx) { user_id: id, }, headers: { - Cookie: token, + Cookie: cookie, Referer: `https://xueqiu.com/u/${id}`, }, }); @@ -61,7 +70,7 @@ async function handler(ctx) { description: `@${screen_name} 的雪球自选动态`, item: data.map((item) => ({ title: `@${screen_name} 关注了股票 ${item.name}`, - description: `@${screen_name} 在${parseDate(item.created).toLocaleString()} 关注了 ${item.name}(${item.exchange}:${item.symbol})。`, + description: `@${screen_name} 在${parseDate(item.created).toLocaleString()} 关注了 ${item.marketplace} ${item.name}(${item.exchange}:${item.symbol})。`, pubDate: parseDate(item.created), link: `https://xueqiu.com/s/${item.symbol}`, })), diff --git a/lib/routes/xueqiu/user.ts b/lib/routes/xueqiu/user.ts index 0314185e4a86b4..c696060fa13dc8 100644 --- a/lib/routes/xueqiu/user.ts +++ b/lib/routes/xueqiu/user.ts @@ -31,8 +31,8 @@ export const route: Route = { maintainers: ['imlonghao'], handler, description: `| 原发布 | 长文 | 问答 | 热门 | 交易 | - | ------ | ---- | ---- | ---- | ---- | - | 0 | 2 | 4 | 9 | 11 |`, +| ------ | ---- | ---- | ---- | ---- | +| 0 | 2 | 4 | 9 | 11 |`, }; async function handler(ctx) { @@ -48,7 +48,8 @@ async function handler(ctx) { 11: '交易', }; - const token = await parseToken(); + const link = `${rootUrl}/u/${id}`; + const token = await parseToken(link); const res2 = await got({ method: 'get', url: `${rootUrl}/v4/statuses/user_timeline.json`, @@ -59,7 +60,7 @@ async function handler(ctx) { }), headers: { Cookie: token, - Referer: `${rootUrl}/u/${id}`, + Referer: link, }, }); const data = res2.data.statuses.filter((s) => s.mark !== 1); // 去除置顶动态 @@ -71,7 +72,7 @@ async function handler(ctx) { method: 'get', url: rootUrl + item.target, headers: { - Referer: `${rootUrl}/u/${id}`, + Referer: link, Cookie: token, }, }); @@ -83,7 +84,7 @@ async function handler(ctx) { const description = item.description + retweetedStatus; return { - title: item.title ?? sanitizeHtml(description, { allowedTags: [], allowedAttributes: {} }), + title: item.title || sanitizeHtml(description, { allowedTags: [], allowedAttributes: {} }), description: item.text ? item.text + retweetedStatus : description, pubDate: parseDate(item.created_at), link: rootUrl + item.target, @@ -94,7 +95,7 @@ async function handler(ctx) { return { title: `${data[0].user.screen_name} 的雪球${typename[type]}动态`, - link: `${rootUrl}/u/${id}`, + link, description: `${data[0].user.screen_name} 的雪球${typename[type]}动态`, item: items, }; diff --git a/lib/routes/xunhupay/namespace.ts b/lib/routes/xunhupay/namespace.ts index ff61ed9707e278..5e7bd53be7227a 100644 --- a/lib/routes/xunhupay/namespace.ts +++ b/lib/routes/xunhupay/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '虎皮椒', url: 'www.xunhupay.com', + lang: 'zh-CN', }; diff --git a/lib/routes/xys/namespace.ts b/lib/routes/xys/namespace.ts index a4ea4ec30df0e0..842c2edc9310cd 100644 --- a/lib/routes/xys/namespace.ts +++ b/lib/routes/xys/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '新语丝', url: 'xys.org', + lang: 'zh-CN', }; diff --git a/lib/routes/xyzrank/namespace.ts b/lib/routes/xyzrank/namespace.ts index 33524af70b47e7..d54160fc6be1c6 100644 --- a/lib/routes/xyzrank/namespace.ts +++ b/lib/routes/xyzrank/namespace.ts @@ -3,11 +3,12 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中文播客榜', url: 'xyzrank.com', - description: `:::tip + description: `::: tip 可以通过指定 \`limit\` 参数确定榜单排名下限,默认为 250。 若只查看榜单前 50,可在订阅 URL 后加入 \`?limit=50\`。 即,以 [热门节目](https://xyzrank.com/#/) 为例,路由为[\`/xyzrank?limit=50\`](https://rsshub.app/xyzrank?limit=50)。 :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/yahoo/namespace.ts b/lib/routes/yahoo/namespace.ts index 80a75c2c77a944..fc2da8e81d7512 100644 --- a/lib/routes/yahoo/namespace.ts +++ b/lib/routes/yahoo/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Yahoo', url: 'hk.news.yahoo.com', + lang: 'zh-HK', }; diff --git a/lib/routes/yahoo/news/index.ts b/lib/routes/yahoo/news/index.ts new file mode 100644 index 00000000000000..b04bc31545d648 --- /dev/null +++ b/lib/routes/yahoo/news/index.ts @@ -0,0 +1,149 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import parser from '@/utils/rss-parser'; +import { getArchive, getCategories, parseList, parseItem } from './utils'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +export const route: Route = { + path: '/news/:region/:category?', + categories: ['new-media', 'popular'], + example: '/yahoo/news/hk/world', + parameters: { + region: 'Region, `hk/tw/au/ca/fr/malaysia/nz/sg/uk/en(us)`, the part represented by the asterisk (*) in *.news.yahoo.com', + category: 'Category, The part represented by the asterisk (*) in .news.yahoo.com/rss/*, region "hk/tw" differs, see the description below', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['news.yahoo.com/'], + }, + ], + name: 'News', + maintainers: ['KeiLongW', 'williamgateszhao'], + handler, + url: 'news.yahoo.com/', + description: ` +\`Region\` + +Support all regions represented by the asterisk (*) in *.news.yahoo.com, such as hk/tw/au/ca/fr/malaysia/nz/sg/uk/en(us). For www.yahoo.com, use en or us. Sites with news domains other than *.news.yahoo.com, such as de.nachrichten.yahoo.com or news.yahoo.co.jp, are not supported. + +\`Category\` + +The parsing method for Yahoo Hong Kong and Taiwan is quite unique. All supported categories are as follows + +Category for hk.news.yahoo.com (hongkong) + +| 全部 | 港聞 | 兩岸國際 | 財經 | 娛樂 | 體育 | 健康 | 親子 | 副刊 | +| ------- | --------- | -------- | -------- | ------------- | ------ | ------ | --------- | ---------- | +| (empty) | hong-kong | world | business | entertainment | sports | health | parenting | supplement | + +Category for tw.news.yahoo.com (taiwan) + +| 全部 | 政治 | 財經 | 娛樂 | 運動 | 社會地方 | 國際 | 生活 | 健康 | 科技 | 品味 | +| ------- | -------- | ------- | ------------- | ------ | -------- | ----- | --------- | ------ | ---------- | ----- | +| (empty) | politics | finance | entertainment | sports | society | world | lifestyle | health | technology | style | + +Other Yahoo news is fetched from the RSS provided by Yahoo. Please refer to the categories displayed on the pages of *.news.yahoo.com (for example, "world"), and try to access *.news.yahoo.com/rss/world to see if it is accessible and contains recent news (some categories exist but are not updated). If it is accessible and has recent news, then that category can be used on the corresponding site. For example, the available categories for news.yahoo.com are as follows + +Category for news.yahoo.com (US) + +| All | US | Politics | World | Science | Tech | +| ------- | -- | -------- | ----- | ------- | ---- | +| (empty) | us | politics | world | science | tech | + +To give another example, since uk.news.yahoo.com/rss/ukoriginal is accessible and has recent news, /yahoo/news/uk/ukoriginal is a valid RSSHub route. + +\`author\` + +For Yahoo Hong Kong and Yahoo Taiwan, please use another "news source" route. + +For other Yahoo News, this route's RSS provides the author field. You can use RSSHub's built-in "content filtering" feature. For example, /yahoo-wg/news/tw/technology?filter_author=Yahoo%20Tech|Engadget can filter out news with authors containing Yahoo Tech or Engadget from Yahoo Taiwan's technology news, which is the Chinese version of Engadget. +`, + + zh: { + name: '新闻', + description: ` +\`区域 Region\` + +支持所有 *.news.yahoo.com 中*号所代表的区域, 例如\`hk/tw/au/ca/fr/malaysia/nz/sg/uk/en(us)\`, 其中 www.yahoo.com 用 en 或 us 来表示。不支持新闻域名不为 *.news.yahoo.com 的站点如 de.nachrichten.yahoo.com 或 news.yahoo.co.jp。 + +\`分类 Category\` + +香港和台湾雅虎的读取方式比较特别, 所有支持的 category 如下 + +hk.news.yahoo.com (香港) 所支持的分类 + +| 全部 | 港聞 | 兩岸國際 | 財經 | 娛樂 | 體育 | 健康 | 親子 | 副刊 | +| ------- | --------- | -------- | -------- | ------------- | ------ | ------ | --------- | ---------- | +| (留空) | hong-kong | world | business | entertainment | sports | health | parenting | supplement | + +tw.news.yahoo.com (台湾) 所支持的分类 + +| 全部 | 政治 | 財經 | 娛樂 | 運動 | 社會地方 | 國際 | 生活 | 健康 | 科技 | 品味 | +| ------- | -------- | ------- | ------------- | ------ | -------- | ----- | --------- | ------ | ---------- | ----- | +| (留空) | politics | finance | entertainment | sports | society | world | lifestyle | health | technology | style | + +其他雅虎新闻读取自 yahoo 提供的 RSS, 请根据 *.news.yahoo.com 的页面上展示的分类(例如 world ), 尝试 *.news.yahoo.com/rss/world 能否访问并且有近期的新闻(有些分类存在但未更新), 如果可以的话则该分类可以用在相应站点, 例如 news.yahoo.com 可用的分类如下 + +news.yahoo.com (美国) 所支持的分类 + +| All | US | Politics | World | Science | Tech | +| ------- | -- | -------- | ----- | ------- | ---- | +| (留空) | us | politics | world | science | tech | + +再举例, 由于 uk.news.yahoo.com/rss/ukoriginal 可以访问并且有较新的新闻, 所以 /yahoo/news/uk/ukoriginal 是一个有效的RSSHub路由。 + +\`作者 author\` + +对于香港和台湾雅虎, 请使用另一个"新聞來源"路由。 + +对于其他雅虎新闻, 本路由的 RSS 中提供了 author 字段, 可使用 RSSHub 的内置"内容过滤"功能, 例如 /yahoo-wg/news/tw/technology?filter_author=Yahoo%20Tech|Engadget 可从台湾雅虎的科技新闻中过滤出作者名称中包含 Yahoo Tech 或者 Engadget 的新闻, 即瘾科技中文版。 +`, + }, +}; + +async function handler(ctx) { + const region = ['en', 'EN', 'us', 'US', 'www', 'WWW', ''].includes(ctx.req.param('region')) ? '' : ctx.req.param('region').toLowerCase(); + const category = ctx.req.param('category'); + if (!['hk', 'tw', 'au', 'ca', 'fr', 'malaysia', 'nz', 'sg', 'uk', ''].includes(region)) { + throw new InvalidParameterError(`Unknown region: ${region}`); + } + + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + + if (['hk', 'tw'].includes(region)) { + const categoryMap = await getCategories(region, cache.tryGet); + const tag = category ? categoryMap[category].yctMap : null; + + const response = await getArchive(region, limit, tag); + const list = parseList(region, response); + + const items = await Promise.all(list.map((item) => parseItem(item, cache.tryGet))); + + return { + title: `Yahoo 新聞 ${region.toUpperCase()} - ${category ? categoryMap[category].name : '所有類別'}`, + link: `https://${region}.news.yahoo.com/${category ? `${category}/` : ''}archive`, + image: 'https://s.yimg.com/cv/apiv2/social/images/yahoo_default_logo-1200x1200.png', + item: items, + }; + } else { + const rssUrl = `https://${region ? `${region}.` : ''}news.yahoo.com/rss/${category ? `${category}/` : ''}`; + const feed = await parser.parseURL(rssUrl); + const filteredItems = feed.items.filter((item) => item?.link && !item.link.includes('promotions') && new URL(item.link).hostname.match(/.*\.yahoo\.com$/)); + const items = await Promise.all(filteredItems.map((item) => parseItem(item, cache.tryGet))); + + return { + title: `Yahoo News ${region.toUpperCase()} - ${category ? category.toUpperCase() : 'All'}`, + link: feed.link, + description: feed.description, + item: items, + }; + } +} diff --git a/lib/routes/yahoo/news/listid.ts b/lib/routes/yahoo/news/listid.ts new file mode 100644 index 00000000000000..76b44923807902 --- /dev/null +++ b/lib/routes/yahoo/news/listid.ts @@ -0,0 +1,70 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import { getList, parseList, parseItem } from './utils'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +export const route: Route = { + path: '/news/list/:region/:listId', + categories: ['new-media', 'popular'], + example: '/yahoo/news/list/hk/09fcf7b0-0ab2-11e8-bf1f-4d52d4f79454', + parameters: { region: '`hk`, `tw`', listId: '見下表' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['hk.news.yahoo.com/'], + }, + { + source: ['tw.news.yahoo.com/'], + }, + ], + name: '合作媒體', + maintainers: ['TonyRL', 'williamgateszhao', 'tpnonthealps'], + handler, + description: ` +| 合作媒體 (\`HK\`) | \`:listId\` | +| ----------------- | ---------------------------------------- | +| 東方日報 | \`33ddd580-0ab3-11e8-bfe1-4b555fb1e429\` | +| now.com | \`01b4d760-0ab4-11e8-af3a-54037d3dced3\` | +| am730 | \`c4842090-0ab2-11e8-af7f-041a72ce7398\` | +| BBC | \`4d3fc9a0-fac8-11e9-87f2-564ca250983e\` | +| 信報財經新聞 | \`5a8a0aa0-0ab3-11e8-b3dc-d990c79d6cb1\` | +| 香港電台 | \`b4bfc2d0-0ab3-11e8-bf9f-c888fc09923f\` | +| 法新社 | \`1cc44280-facb-11e9-ad7c-f3ba971275c8\` | +| Bloomberg | \`40023670-facc-11e9-9dde-9175ff306602\` | +| 香港動物報 | \`6058fa9c-d74d-487a-8b49-aa99a2a2978e\` |`, +}; + +async function handler(ctx) { + const { region, listId } = ctx.req.param(); + if (!['hk', 'tw'].includes(region)) { + throw new InvalidParameterError(`Unsupported region: ${region}`); + } + + const response = await getList(region, listId); + + // console.log('Response:', response.stream_items); + // console.log('Type of response:', typeof response.stream_items); + // console.log('Is response an array?', Array.isArray(response.stream_items)); + const list = parseList(region, response.stream_items); + + const items = await Promise.all(list.map((item) => parseItem(item, cache.tryGet))); + + const author = items[0].author; + const atIndex = author.indexOf('@'); // fing '@' + const source = atIndex === -1 ? author : author.substring(atIndex + 1).trim(); + // console.log(source); + + return { + title: `Yahoo 新聞 - ${source ?? ''}`, + link: `https://${region}.news.yahoo.com`, + image: 'https://s.yimg.com/cv/apiv2/social/images/yahoo_default_logo-1200x1200.png', + item: items, + }; +} diff --git a/lib/routes/yahoo/news/tw/provider-helper.ts b/lib/routes/yahoo/news/provider-helper.ts similarity index 71% rename from lib/routes/yahoo/news/tw/provider-helper.ts rename to lib/routes/yahoo/news/provider-helper.ts index e7c9345f342b20..467a35d72bebb3 100644 --- a/lib/routes/yahoo/news/tw/provider-helper.ts +++ b/lib/routes/yahoo/news/provider-helper.ts @@ -4,10 +4,10 @@ import { getProviderList } from './utils'; import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { - path: '/news/providers/:region', - categories: ['new-media'], - example: '/yahoo/news/providers/tw', - parameters: { region: '地區,見上表' }, + path: '/news/providers/:region/list', + categories: ['new-media', 'popular'], + example: '/yahoo/news/providers/tw/list', + parameters: { region: '地区, 同路由"新闻来源"中的支持地区, 即 hk 或 tw' }, features: { requireConfig: false, requirePuppeteer: false, @@ -16,8 +16,16 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, + radar: [ + { + source: ['hk.news.yahoo.com/'], + }, + { + source: ['tw.news.yahoo.com/'], + }, + ], name: '新聞來源列表', - maintainers: ['TonyRL'], + maintainers: ['TonyRL', 'williamgateszhao'], handler, }; diff --git a/lib/routes/yahoo/news/tw/provider.ts b/lib/routes/yahoo/news/provider.ts similarity index 62% rename from lib/routes/yahoo/news/tw/provider.ts rename to lib/routes/yahoo/news/provider.ts index 934ef11501dc9a..cb0a4aa3e68844 100644 --- a/lib/routes/yahoo/news/tw/provider.ts +++ b/lib/routes/yahoo/news/provider.ts @@ -5,9 +5,9 @@ import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { path: '/news/provider/:region/:providerId', - categories: ['new-media'], - example: '/yahoo/news/provider/tw/udn.com', - parameters: { region: '地區,見下表', providerId: '新聞來源 ID,可透過下方新聞來源列表獲得' }, + categories: ['new-media', 'popular'], + example: '/yahoo/news/provider/tw/yahoo_tech_tw_942', + parameters: { region: '地區, hk 或 tw, 分别表示香港雅虎和台湾雅虎', providerId: '新聞來源 ID, 可透過路由"新聞來源列表"獲得' }, features: { requireConfig: false, requirePuppeteer: false, @@ -16,12 +16,30 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, + radar: [ + { + source: ['hk.news.yahoo.com/'], + }, + { + source: ['tw.news.yahoo.com/'], + }, + ], name: '新聞來源', - maintainers: ['TonyRL'], + maintainers: ['TonyRL', 'williamgateszhao'], handler, - description: `| 香港 | 台灣 | - | ---- | ---- | - | hk | tw |`, + description: ` +\`Region\` + +| 香港 | 台灣 | +| ---- | ---- | +| hk | tw | + +\`ProviderId\` + +除了可以通过路由"新聞來源列表"获得外, 也可通过 hk.news.yahoo.com/archive 和 tw.news.yahoo.com/archive 选择"新闻来源"后通过页面 Url 来获得。 + +例如 hk.news.yahoo.com/yahoo_movies_hk_660--所有分類/archive, \`yahoo_movies_hk_660\` 就是 ProviderId 。 +`, }; async function handler(ctx) { diff --git a/lib/routes/yahoo/news/tw/index.ts b/lib/routes/yahoo/news/tw/index.ts deleted file mode 100644 index 30fb4a70f4f9c4..00000000000000 --- a/lib/routes/yahoo/news/tw/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Route } from '@/types'; -import cache from '@/utils/cache'; -import { getArchive, getCategories, parseList, parseItem } from './utils'; -import InvalidParameterError from '@/errors/types/invalid-parameter'; - -export const route: Route = { - path: '/news/:region/:category?', - categories: ['new-media'], - example: '/yahoo/news/hk/world', - parameters: { region: 'Region, see the table below', category: 'Category, see the table below' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['yahoo.com/'], - }, - ], - name: 'News', - maintainers: ['KeiLongW'], - handler, - url: 'yahoo.com/', - description: `\`Region\` - - | Hong Kong | Taiwan | US | - | --------- | ------ | -- | - | hk | tw | en | - - <details> - <summary>\`Category\` (Hong Kong)</summary> - - | 全部 | 港聞 | 兩岸國際 | 財經 | 娛樂 | 體育 | 健康 | 親子 | 副刊 | - | -------- | --------- | -------- | -------- | ------------- | ------ | ------ | --------- | ---------- | - | (留空) | hong-kong | world | business | entertainment | sports | health | parenting | supplement | - </details> - - <details> - <summary>\`Category\` (Taiwan)</summary> - - | 全部 | 政治 | 財經 | 娛樂 | 運動 | 社會地方 | 國際 | 生活 | 健康 | 科技 | 品味 | - | -------- | -------- | ------- | ------------- | ------ | -------- | ----- | --------- | ------ | ---------- | ----- | - | (留空) | politics | finance | entertainment | sports | society | world | lifestyle | health | technology | style | - </details> - - <details> - <summary>\`Category\` (US)</summary> - - | All | World | Business | Entertainment | Sports | Health | - | ------- | ----- | -------- | ------------- | ------ | ------ | - | (Empty) | world | business | entertainment | sports | health | - </details>`, -}; - -async function handler(ctx) { - const { region, category } = ctx.req.param(); - if (!['hk', 'tw'].includes(region)) { - throw new InvalidParameterError(`Unknown region: ${region}`); - } - - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; - const categoryMap = await getCategories(region, cache.tryGet); - const tag = category ? categoryMap[category].yctMap : null; - - const response = await getArchive(region, limit, tag); - const list = parseList(region, response); - - const items = await Promise.all(list.map((item) => parseItem(item, cache.tryGet))); - - return { - title: `Yahoo 新聞 - ${category ? categoryMap[category].name : '所有類別'}`, - link: `https://${region}.news.yahoo.com/${category ? `${category}/` : ''}archive`, - image: 'https://s.yimg.com/cv/apiv2/social/images/yahoo_default_logo-1200x1200.png', - item: items, - }; -} diff --git a/lib/routes/yahoo/news/us/index.ts b/lib/routes/yahoo/news/us/index.ts deleted file mode 100644 index 3daf5d415cf662..00000000000000 --- a/lib/routes/yahoo/news/us/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import parser from '@/utils/rss-parser'; -import { load } from 'cheerio'; -import { isValidHost } from '@/utils/valid-host'; -import InvalidParameterError from '@/errors/types/invalid-parameter'; - -export const route: Route = { - path: '/news/en/:category?', - name: 'Unknown', - maintainers: [], - handler, -}; - -async function handler(ctx) { - const region = ctx.req.param('region') === 'en' ? '' : ctx.req.param('region').toLowerCase(); - if (!isValidHost(region) && region !== '') { - throw new InvalidParameterError('Invalid region'); - } - const category = ctx.req.param('category') ? ctx.req.param('category').toLowerCase() : ''; - const rssUrl = `https://${region ? `${region}.` : ''}news.yahoo.com/rss/${category}`; - const feed = await parser.parseURL(rssUrl); - const filteredItems = feed.items.filter((item) => !item.link.includes('promotions') && new URL(item.link).hostname.match(/.*\.yahoo\.com$/)); - const items = await Promise.all( - filteredItems.map((item) => - cache.tryGet(item.link, async () => { - const response = await got({ - method: 'get', - url: item.link, - }); - const $ = load(response.data); - const author = `${$('span.caas-author-byline-collapse').text()} @${$('span.caas-attr-provider').text()}`; - $('.caas-content-byline-wrapper, .caas-xray-wrapper, .caas-header, .caas-readmore').remove(); - const description = $('.caas-content-wrapper').html(); - - const single = { - title: item.title, - description, - author, - pubDate: item.pubDate, - link: item.link, - }; - return single; - }) - ) - ); - - return { - title: feed.title, - link: feed.link, - description: feed.description, - item: items, - }; -} diff --git a/lib/routes/yahoo/news/tw/utils.ts b/lib/routes/yahoo/news/utils.ts similarity index 71% rename from lib/routes/yahoo/news/tw/utils.ts rename to lib/routes/yahoo/news/utils.ts index 021335b7a9cabb..760f8ee3d7cf73 100644 --- a/lib/routes/yahoo/news/tw/utils.ts +++ b/lib/routes/yahoo/news/utils.ts @@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date'; import path from 'node:path'; import { art } from '@/utils/render'; -const getArchive = async (region, limit, tag, providerId) => { +const getArchive = async (region, limit, tag, providerId?) => { const { data: response } = await got( `https://${region}.news.yahoo.com/_td-news/api/resource/NCPListService;api=archive;ncpParams=${encodeURIComponent( JSON.stringify({ @@ -24,6 +24,11 @@ const getArchive = async (region, limit, tag, providerId) => { return response; }; +const getList = async (region, listId) => { + const { data: response } = await got(`https://${region}.news.yahoo.com/_td-news/api/resource/StreamService;category=LISTID%3A${listId};useNCP=true`); + return response; +}; + const getCategories = (region, tryGet) => tryGet(`yahoo:${region}:categoryMap`, async () => { const { PageStore } = await getStores(region, tryGet); @@ -61,7 +66,7 @@ const getStores = (region, tryGet) => const appData = JSON.parse( $('script:contains("root.App.main")') .text() - .match(/root.App.main\s+=\s+({.+});/)[1] + .match(/root.App.main\s+=\s+({.+});/)?.[1] as string ); return appData.context.dispatcher.stores; @@ -70,7 +75,7 @@ const getStores = (region, tryGet) => const parseList = (region, response) => response.map((item) => ({ title: item.title, - link: new URL(item.url, `https://${region}.news.yahoo.com`).href, + link: item.url.startsWith('http') ? item.url : new URL(item.url, `https://${region}.news.yahoo.com`).href, description: item.summary, pubDate: parseDate(item.published_at, 'X'), })); @@ -81,45 +86,49 @@ const parseItem = (item, tryGet) => const $ = load(response); const ldJson = JSON.parse($('script[type="application/ld+json"]').first().text()); + const author = `${$('span.caas-author-byline-collapse').text()} @${$('span.caas-attr-provider').text()}`; const body = $('.caas-body'); body.find('noscript').remove(); // remove padding body.find('.caas-figure-with-pb, .caas-img-container').each((_, ele) => { - ele = $(ele); - ele.removeAttr('style'); + const $ele = $(ele); + $ele.removeAttr('style'); }); body.find('img').each((_, ele) => { - ele = $(ele); - let dataSrc = ele.data('src'); + const $ele = $(ele); + let dataSrc = $ele.data('src') as string; if (dataSrc) { const match = dataSrc.match(/.*--\/.*--\/(.*)/); if (match?.[1]) { dataSrc = match?.[1]; } - ele.attr('src', dataSrc); - ele.removeAttr('data-src'); + $ele.attr('src', dataSrc); + $ele.removeAttr('data-src'); } }); // fix blockquote iframe body.find('.caas-iframe').each((_, ele) => { - ele = $(ele); - if (ele.data('type') === 'youtube') { - ele.replaceWith( - art(path.join(__dirname, '../../templates/youtube.art'), { - id: ele.find('blockquote').data('src').split('/').pop()?.split('?')?.[0], + const $ele = $(ele); + if ($ele.data('type') === 'youtube') { + const blockquoteSrc = $ele.find('blockquote').data('src') as string; + $ele.replaceWith( + art(path.join(__dirname, '../templates/youtube.art'), { + id: blockquoteSrc.split('/').pop()?.split('?')?.[0], }) ); } }); item.description = body.html(); + item.author = author; item.category = ldJson.keywords; + item.pubDate = parseDate(ldJson.datePublished); item.updated = parseDate(ldJson.dateModified); return item; }); -export { getArchive, getCategories, getProviderList, getStores, parseList, parseItem }; +export { getArchive, getList, getCategories, getProviderList, getStores, parseList, parseItem }; diff --git a/lib/routes/yamap/articles.ts b/lib/routes/yamap/articles.ts new file mode 100644 index 00000000000000..ec36a4457cbb53 --- /dev/null +++ b/lib/routes/yamap/articles.ts @@ -0,0 +1,62 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const baseUrl = 'https://yamap.com/activities/'; +const host = 'https://api.yamap.com/v3/activities?page=1&per=24'; + +export const route: Route = { + path: '/', + categories: ['travel'], + example: '/yamap', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '文章', + maintainers: ['valuex'], + handler, + description: '', +}; + +async function handler() { + const link = host; + const response = await got(link); + const metadata = response.data; + // const recordNum = metadata.activities.length - 1 ; + + const lists = metadata.activities.map((item) => ({ + title: item.title, + link: baseUrl + item.id.toString(), + pubDate: parseDate(item.created_at), + location: item.map.name || 'Japan', + })); + + const items = await Promise.all( + lists.map((item) => + cache.tryGet(item.link, async () => { + const response = await got(item.link); + const $ = load(response.data); + item.title = item.title + '-' + item.location; + item.description = $('div.ActivitiesId__Body main').html(); + item.pubDate = timezone(parseDate($('span[class=ActivityDetailTabLayout__Middle__Date]').text()), 8); + return item; + }) + ) + ); + + return { + title: 'Yamap', + link: baseUrl, + description: 'Yamap', + item: items, + }; +} diff --git a/lib/routes/yamap/namespace.ts b/lib/routes/yamap/namespace.ts new file mode 100644 index 00000000000000..21ef8397ce3d23 --- /dev/null +++ b/lib/routes/yamap/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'YAMAP', + url: 'yamap.com', + lang: 'ja', +}; diff --git a/lib/routes/yamibo/bbs/forum.ts b/lib/routes/yamibo/bbs/forum.ts new file mode 100644 index 00000000000000..21820def90d304 --- /dev/null +++ b/lib/routes/yamibo/bbs/forum.ts @@ -0,0 +1,116 @@ +import type { Data, DataItem, Route } from '@/types'; +import type { Context } from 'hono'; +import { config } from '@/config'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { asyncPoolAll, fetchThread, generateDescription, getDate, bbsOrigin } from '../utils'; +import cache from '@/utils/cache'; + +export const route: Route = { + name: 'BBS - 板块', + categories: ['bbs'], + path: '/bbs/forum/:fid/:type?', + example: '/yamibo/bbs/forum/5/404', + parameters: { + fid: '板块 id,可从URL中提取。https://bbs.yamibo.com/forum-aa-b.html中的aa部分即为fid值', + type: '板块子分类,网页中选中板块分类后URL中的typeid值', + }, + maintainers: ['KarasuShin'], + handler, + features: { + antiCrawler: true, + requireConfig: [ + { + optional: true, + name: 'YAMIBO_SALT', + description: + '百合会BBS登录后的认证信息,获取方式:1. 登录百合会BBS网页版 2. 打开浏览器开发者工具,切换到 Application 面板\n3. 点击侧边栏中的Storage -> Cookies -> https://bbs.yamibo.com 4. 复制 Cookie 中的 EeqY_2132_saltkey 值', + }, + { + optional: true, + name: 'YAMIBO_AUTH', + description: + '百合会BBS登录后的认证信息,获取方式:1. 登录百合会BBS网页版 2. 打开浏览器开发者工具,切换到 Application 面板\n3. 点击侧边栏中的Storage -> Cookies -> https://bbs.yamibo.com 4. 复制 Cookie 中的 EeqY_2132_auth 值', + }, + ], + }, + description: `::: warning +百合会BBS访问部分板块需要用户登录认证,请参考配置说明 +:::`, +}; + +async function handler(ctx: Context): Promise<Data> { + const fid = ctx.req.param('fid'); + const type = ctx.req.param('type'); + const { auth, salt } = config.yamibo; + + const params = new URLSearchParams(); + params.set('mod', 'forumdisplay'); + params.set('fid', fid); + params.set('orderby', 'dateline'); + if (type) { + params.set('filter', 'typeid'); + params.set('typeid', type); + } + const headers: HeadersInit = {}; + + if (auth && salt) { + headers.cookie = `EeqY_2132_saltkey=${salt}; EeqY_2132_auth=${auth}`; + } + + const link = `${bbsOrigin}/forum.php?${params.toString()}`; + + const $ = load(await ofetch<string>(link, { headers })); + + const title = $('title').text().replace(' - 百合会 - Powered by Discuz!', ''); + + let items: DataItem[] = $('tbody[id^="normalthread_"]') + .toArray() + .map((item) => { + const $item = $(item); + const id = $item.attr('id')!.match(/\d+/)![0]; + const title = $item.find('th em').text() + $item.find('th .s.xst').text(); + const link = `${bbsOrigin}/thread-${id}-1-1.html`; + const pubDate = getDate($item.find('td.by').first().find('em').text()); + + return { + id, + title, + link, + pubDate, + }; + }); + + items = await asyncPoolAll( + 5, + items, + async (item) => + (await cache.tryGet(item.link!, async () => { + let description: string | undefined; + const { data } = await fetchThread(item.id!); + if (data && !data.startsWith('<script type="text/javascript">')) { + const $ = load(data); + if ($('#postlist>div[id^="post_"]').length) { + const op = $('#postlist>div[id^="post_"]').first(); + const postId = op.attr('id')?.match(/\d+/)?.[0]; + if (postId) { + description = generateDescription(op, postId); + } + } + } + + return { + title: item.title, + link: item.link, + description, + pubDate: item.pubDate, + }; + })) as DataItem + ); + + return { + title, + link, + item: items, + }; +} diff --git a/lib/routes/yamibo/bbs/thread.ts b/lib/routes/yamibo/bbs/thread.ts new file mode 100644 index 00000000000000..0b2060ec5f8b9d --- /dev/null +++ b/lib/routes/yamibo/bbs/thread.ts @@ -0,0 +1,84 @@ +import type { Data, DataItem, Route } from '@/types'; +import type { Context } from 'hono'; +import { load } from 'cheerio'; +import { fetchThread, generateDescription, getDate, bbsOrigin } from '../utils'; + +export const route: Route = { + name: 'BBS - 讨论串', + categories: ['bbs'], + path: '/bbs/thread/:tid', + example: '/yamibo/bbs/thread/541914', + parameters: { + tid: '讨论串 id,可从URL中提取。https://bbs.yamibo.com/forum.php?mod=viewthread&tid=xxxx中的xxx或https://bbs.yamibo.com/thread-aaa-b-c.html中的aaa部分即为tid值', + }, + maintainers: ['KarasuShin'], + handler, + features: { + antiCrawler: true, + requireConfig: [ + { + optional: true, + name: 'YAMIBO_SALT', + description: + '百合会BBS登录后的认证信息,获取方式:1. 登录百合会BBS网页版 2. 打开浏览器开发者工具,切换到 Application 面板\n3. 点击侧边栏中的Storage -> Cookies -> https://bbs.yamibo.com 4. 复制 Cookie 中的 EeqY_2132_saltkey 值', + }, + { + optional: true, + name: 'YAMIBO_AUTH', + description: + '百合会BBS登录后的认证信息,获取方式:1. 登录百合会BBS网页版 2. 打开浏览器开发者工具,切换到 Application 面板\n3. 点击侧边栏中的Storage -> Cookies -> https://bbs.yamibo.com 4. 复制 Cookie 中的 EeqY_2132_auth 值', + }, + ], + }, + description: `::: warning +百合会BBS访问部分讨论串需要用户登录认证,请参考配置说明 +:::`, +}; + +async function handler(ctx: Context): Promise<Data> { + const tid = ctx.req.param('tid'); + + const { data, link } = await fetchThread(tid, { ordertype: '1' }); + + if (!data) { + return { + title: '讨论串不存在', + link, + item: [], + }; + } + + const $ = load(data); + const title = $('title').text().replace(' - 百合会 - Powered by Discuz!', ''); + const items: DataItem[] = $('#postlist>div[id^="post_"]') + .toArray() + .map((item) => { + const $item = $(item); + const isOP = !!$item.has('#fj').length; + const postId = $item.attr('id')!.match(/\d+/)![0]; + const $tr = $item.find('table').find('tr').first(); + const profileBlock = $tr.find(`#favatar${postId}`); + const nickName = profileBlock.find('.authi').text(); + const floor = isOP ? '主楼' : $tr.find(`#postnum${postId} em`).text(); + const link = isOP ? `${bbsOrigin}/forum.php?mod=viewthread&tid=${tid}` : `${bbsOrigin}/forum.php?mod=redirect&goto=findpost&ptid=${tid}&pid=${postId}`; + const description = generateDescription($item, postId); + + const createTime = $tr + .find(`#authorposton${postId}`) + .text() + .match(/\d{4}(?:-\d{1,2}){2} \d{2}:\d{2}/)![0]; + + return { + title: `${floor} - ${nickName}`, + link, + description, + pubDate: getDate(createTime), + }; + }); + + return { + title, + link, + item: items, + }; +} diff --git a/lib/routes/yamibo/namespace.ts b/lib/routes/yamibo/namespace.ts new file mode 100644 index 00000000000000..1704a40fb98bba --- /dev/null +++ b/lib/routes/yamibo/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '百合会', + url: 'yamibo.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/yamibo/utils.ts b/lib/routes/yamibo/utils.ts new file mode 100644 index 00000000000000..3e6a7ee0e57e44 --- /dev/null +++ b/lib/routes/yamibo/utils.ts @@ -0,0 +1,114 @@ +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import ofetch from '@/utils/ofetch'; +import { config } from '@/config'; +import asyncPool from 'tiny-async-pool'; +import { JSDOM } from 'jsdom'; +import type { Cheerio, Element } from 'cheerio'; + +export const bbsOrigin = 'https://bbs.yamibo.com'; + +export function getDate(date: string): Date { + return timezone(parseDate(date), 8); +} + +export async function fetchThread( + tid: string, + options?: { + ordertype?: string; + _dsign?: string; + }, + retry = 0 +): Promise<{ + link: string; + data?: string; +}> { + const { auth, salt } = config.yamibo; + const params = new URLSearchParams(); + params.set('mod', 'viewthread'); + params.set('tid', tid); + if (options?.ordertype) { + params.set('ordertype', options.ordertype); + } + if (options?._dsign) { + params.set('_dsign', options._dsign); + } + const link = `https://bbs.yamibo.com/forum.php?${params.toString()}`; + + const headers: HeadersInit = {}; + + if (auth && salt) { + headers.cookie = `EeqY_2132_saltkey=${salt}; EeqY_2132_auth=${auth}`; + } + + const data = await ofetch<string>(link, { headers }); + + // sometimes may trigger anti-crawling measures + if (data.startsWith('<script type="text/javascript">') && retry <= 3) { + let script = data.match(/<script type="text\/javascript">([\S\s]*?)<\/script>/)![1]; + script = script.replace(/= location;|=location;/, '=fakeLocation;'); + script = script.replace('location.replace', 'foo'); + script = script.replace('location.assign', 'foo'); + script = script.replace(/location\[[^\]]*]\(/, 'foo('); + script = script.replace(/location\[[^\]]*]=/, 'window.locationValue='); + script = script.replace('location.href=', 'window.locationValue='); + script = script.replace('location=', 'window.locationValue='); + const dom = new JSDOM( + `<script> + function foo(value) { window.locationValue = value; }; + fakeLocation = { href: '', replace: foo, assign: foo }; + Object.defineProperty(fakeLocation, 'href', { + set: function (value) { + window.locationValue = value; + } + }); + ${script} + </script>`, + { + runScripts: 'dangerously', + } + ); + const locationValue = dom.window.locationValue; + if (locationValue) { + const searchParams = new URLSearchParams(locationValue); + const _dsign = searchParams.get('_dsign'); + if (_dsign) { + options = { + ...options, + _dsign, + }; + } + } + return await fetchThread(tid, options, ++retry); + } + + return { + link, + data, + }; +} + +export function generateDescription($item: Cheerio<Element>, postId: string) { + const content = $item.find(`#postmessage_${postId}`).parent(); + content.find('img').each((_, img) => { + const src = img.attribs.zoomfile ?? img.attribs.src; + img.attribs.src = `${bbsOrigin}/${src}`; + }); + let description = content.html() ?? ''; + + const images = $item.find('.pattl img').toArray(); + for (const img of images) { + const src = img.attribs.zoomfile ?? img.attribs.src; + description += `<img src="${bbsOrigin}/${src}" />`; + } + + return description; +} + +export async function asyncPoolAll<IN, OUT>(poolLimit: number, array: readonly IN[], iteratorFn: (generator: IN) => Promise<OUT>) { + const results: Awaited<OUT[]> = []; + for await (const result of asyncPool(poolLimit, array, iteratorFn)) { + results.push(result); + } + return results; +} diff --git a/lib/routes/yande/namespace.ts b/lib/routes/yande/namespace.ts index aadd5323d86972..83371c10c93bd5 100644 --- a/lib/routes/yande/namespace.ts +++ b/lib/routes/yande/namespace.ts @@ -1,7 +1,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'yande', + name: 'yande.re', url: 'yande.re', description: `yande post`, + lang: 'en', }; diff --git a/lib/routes/yande/post.ts b/lib/routes/yande/post.ts index fc1ba6db97ac0b..e3bf59c3f6082f 100644 --- a/lib/routes/yande/post.ts +++ b/lib/routes/yande/post.ts @@ -1,24 +1,34 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import queryString from 'query-string'; export const route: Route = { path: '/post/popular_recent/:period?', - categories: ['anime'], + categories: ['picture', 'popular'], + view: ViewType.Pictures, example: '/yande/post/popular_recent/1d', parameters: { - period: '展示时间', + period: { + description: '展示时间', + options: [ + { value: '1d', label: '最近 24 小时' }, + { value: '1w', label: '最近一周' }, + { value: '1m', label: '最近一月' }, + { value: '1y', label: '最近一年' }, + ], + default: '1d', + }, }, radar: [ { - source: ['yande.re/post/'], + source: ['yande.re/post'], }, ], - name: 'posts', - maintainers: ['fashioncj'], - description: `| 最近 24 小时 | 最近一周 | 最近一月 | 最近一年 | - | ------- | -------- | ------- | -------- | - | 1d | 1w | 1m |1y|`, + name: 'Popular Recent Posts', + maintainers: ['magic-akari', 'SettingDust', 'fashioncj', 'NekoAria'], + description: `| 最近 24 小时 | 最近一周 | 最近一月 | 最近一年 | +| ------- | -------- | ------- | -------- | +| 1d | 1w | 1m | 1y |`, handler, }; diff --git a/lib/routes/yangtzeu/namespace.ts b/lib/routes/yangtzeu/namespace.ts index 33da8158a20f71..221f5cc97fe185 100644 --- a/lib/routes/yangtzeu/namespace.ts +++ b/lib/routes/yangtzeu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '长江大学', url: 'yangtzeu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/ycwb/index.ts b/lib/routes/ycwb/index.ts index 37b3987fb842cf..d0d639fa2d55f7 100644 --- a/lib/routes/ycwb/index.ts +++ b/lib/routes/ycwb/index.ts @@ -30,13 +30,13 @@ export const route: Route = { 常用栏目节点: - | 首页 | 中国 | 国际 | 体育 | 要闻 | 珠江评论 | 民生观察 | 房产 | 金羊教育 | 金羊财富 | 金羊文化 | 金羊健康 | 金羊汽车 | - | ---- | ---- | ---- | ---- | ---- | -------- | -------- | ---- | -------- | -------- | -------- | -------- | -------- | - | 1 | 14 | 15 | 16 | 22 | 1875 | 21773 | 222 | 5725 | 633 | 5281 | 21692 | 223 | +| 首页 | 中国 | 国际 | 体育 | 要闻 | 珠江评论 | 民生观察 | 房产 | 金羊教育 | 金羊财富 | 金羊文化 | 金羊健康 | 金羊汽车 | +| ---- | ---- | ---- | ---- | ---- | -------- | -------- | ---- | -------- | -------- | -------- | -------- | -------- | +| 1 | 14 | 15 | 16 | 22 | 1875 | 21773 | 222 | 5725 | 633 | 5281 | 21692 | 223 | - | 广州 | 广州 - 广州要闻 | 广州 - 社会百态 | 广州 - 深读广州 | 广州 - 生活服务 | 今日大湾区 | 广东 - 政经热闻 | 广东 - 民生视点 | 广东 - 滚动新闻 | - | ---- | --------------- | --------------- | --------------- | --------------- | ---------- | --------------- | --------------- | --------------- | - | 18 | 5261 | 6030 | 13352 | 83422 | 100418 | 13074 | 12252 | 12212 |`, +| 广州 | 广州 - 广州要闻 | 广州 - 社会百态 | 广州 - 深读广州 | 广州 - 生活服务 | 今日大湾区 | 广东 - 政经热闻 | 广东 - 民生视点 | 广东 - 滚动新闻 | +| ---- | --------------- | --------------- | --------------- | --------------- | ---------- | --------------- | --------------- | --------------- | +| 18 | 5261 | 6030 | 13352 | 83422 | 100418 | 13074 | 12252 | 12212 |`, }; async function handler(ctx) { diff --git a/lib/routes/ycwb/namespace.ts b/lib/routes/ycwb/namespace.ts index 71be8e8bbf99ee..bf70b3da786b18 100644 --- a/lib/routes/ycwb/namespace.ts +++ b/lib/routes/ycwb/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '羊城晚报金羊网', url: 'xwlb.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/yenpress/namespace.ts b/lib/routes/yenpress/namespace.ts index 2749ef6d48edf1..daedbb488989d6 100644 --- a/lib/routes/yenpress/namespace.ts +++ b/lib/routes/yenpress/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: 'Yen Press', url: 'yenpress.com', categories: ['reading'], + lang: 'en', }; diff --git a/lib/routes/ygkkk/namespace.ts b/lib/routes/ygkkk/namespace.ts index cd116d5ed4b926..7a6c1a4d6dd050 100644 --- a/lib/routes/ygkkk/namespace.ts +++ b/lib/routes/ygkkk/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '甬哥侃侃侃YouTube教程摘要随笔', url: 'ygkkk.blogspot.com', + lang: 'zh-CN', }; diff --git a/lib/routes/yicai/carousel.ts b/lib/routes/yicai/carousel.ts new file mode 100644 index 00000000000000..ef8f9b99a6bfa9 --- /dev/null +++ b/lib/routes/yicai/carousel.ts @@ -0,0 +1,48 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import { rootUrl, fetchFullArticles } from './utils'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/carousel', + categories: ['traditional-media'], + example: '/yicai/carousel', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['yicai.com/'], + }, + ], + name: '轮播', + maintainers: ['nczitzk'], + handler, + url: 'yicai.com/', +}; + +async function handler() { + const res = await ofetch(rootUrl); + const $ = load(res); + const items = await Promise.all( + fetchFullArticles( + $('#breaknews a') + .toArray() + .map((e) => ({ link: new URL($(e).attr('href'), rootUrl).href, title: $(e).text() })), + cache.tryGet + ) + ); + + return { + title: '第一财经 - 轮播', + link: rootUrl, + item: items, + }; +} diff --git a/lib/routes/yicai/dt.ts b/lib/routes/yicai/dt.ts index c978059cb3ba89..a43e189df84d3d 100644 --- a/lib/routes/yicai/dt.ts +++ b/lib/routes/yicai/dt.ts @@ -33,45 +33,45 @@ export const route: Route = { handler, description: `#### [文章](https://dt.yicai.com/article) - | 分类 | ID | - | -------- | ---------- | - | 全部 | article/0 | - | 新流行 | article/31 | - | 新趋势 | article/32 | - | 商业黑马 | article/33 | - | 新品 | article/34 | - | 营销 | article/35 | - | 大公司 | article/36 | - | 城市生活 | article/38 | - - #### [报告](https://dt.yicai.com/report) - - | 分类 | ID | - | ---------- | --------- | - | 全部 | report/0 | - | 人群观念 | report/9 | - | 人群行为 | report/22 | - | 美妆个护 | report/23 | - | 3C 数码 | report/24 | - | 营销趋势 | report/25 | - | 服饰鞋包 | report/27 | - | 互联网 | report/28 | - | 城市与居住 | report/29 | - | 消费趋势 | report/30 | - | 生活趋势 | report/37 | - - #### [可视化](https://dt.yicai.com/visualization) - - | 分类 | ID | - | -------- | ---------------- | - | 全部 | visualization/0 | - | 新流行 | visualization/39 | - | 新趋势 | visualization/40 | - | 商业黑马 | visualization/41 | - | 新品 | visualization/42 | - | 营销 | visualization/43 | - | 大公司 | visualization/44 | - | 城市生活 | visualization/45 |`, +| 分类 | ID | +| -------- | ---------- | +| 全部 | article/0 | +| 新流行 | article/31 | +| 新趋势 | article/32 | +| 商业黑马 | article/33 | +| 新品 | article/34 | +| 营销 | article/35 | +| 大公司 | article/36 | +| 城市生活 | article/38 | + +#### [报告](https://dt.yicai.com/report) + +| 分类 | ID | +| ---------- | --------- | +| 全部 | report/0 | +| 人群观念 | report/9 | +| 人群行为 | report/22 | +| 美妆个护 | report/23 | +| 3C 数码 | report/24 | +| 营销趋势 | report/25 | +| 服饰鞋包 | report/27 | +| 互联网 | report/28 | +| 城市与居住 | report/29 | +| 消费趋势 | report/30 | +| 生活趋势 | report/37 | + +#### [可视化](https://dt.yicai.com/visualization) + +| 分类 | ID | +| -------- | ---------------- | +| 全部 | visualization/0 | +| 新流行 | visualization/39 | +| 新趋势 | visualization/40 | +| 商业黑马 | visualization/41 | +| 新品 | visualization/42 | +| 营销 | visualization/43 | +| 大公司 | visualization/44 | +| 城市生活 | visualization/45 |`, }; async function handler(ctx) { diff --git a/lib/routes/yicai/feed.ts b/lib/routes/yicai/feed.ts index a73de58bf89ffe..e2912fc2cee752 100644 --- a/lib/routes/yicai/feed.ts +++ b/lib/routes/yicai/feed.ts @@ -27,9 +27,9 @@ export const route: Route = { name: '关注', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 全部主题词见 [此处](https://www.yicai.com/feed/alltheme) - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/yicai/namespace.ts b/lib/routes/yicai/namespace.ts index 8a4a78e1d4bc86..f6da0a5574cf38 100644 --- a/lib/routes/yicai/namespace.ts +++ b/lib/routes/yicai/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '第一财经', url: 'yicai.com', + lang: 'zh-CN', }; diff --git a/lib/routes/yicai/news.ts b/lib/routes/yicai/news.ts index d116ad451dde41..037a507f12828f 100644 --- a/lib/routes/yicai/news.ts +++ b/lib/routes/yicai/news.ts @@ -27,32 +27,32 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| Id | 名称 | - | ------------------------ | ---------- | - | gushi | A 股 | - | kechuangban | 科创板 | - | hongguan | 大政 | - | jinrong | 金融 | - | quanqiushichang | 海外市场 | - | gongsi | 产经 | - | shijie | 全球 | - | kechuang | 科技 | - | quyu | 区域 | - | comment | 评论 | - | dafengwenhua | 商业人文 | - | books | 阅读周刊 | - | loushi | 地产 | - | automobile | 汽车 | - | china\_financial\_herald | 对话陆家嘴 | - | fashion | 时尚 | - | ad | 商业资讯 | - | info | 资讯 | - | jzfxb | 价值风向标 | - | shuducaijing | 数读财经 | - | shujujiepan | 数据解盘 | - | shudushenghuo | 数读生活 | - | cbndata | CBNData | - | dtcj | DT 财经 | - | xfsz | 消费数知 |`, +| ------------------------ | ---------- | +| gushi | A 股 | +| kechuangban | 科创板 | +| hongguan | 大政 | +| jinrong | 金融 | +| quanqiushichang | 海外市场 | +| gongsi | 产经 | +| shijie | 全球 | +| kechuang | 科技 | +| quyu | 区域 | +| comment | 评论 | +| dafengwenhua | 商业人文 | +| books | 阅读周刊 | +| loushi | 地产 | +| automobile | 汽车 | +| china\_financial\_herald | 对话陆家嘴 | +| fashion | 时尚 | +| ad | 商业资讯 | +| info | 资讯 | +| jzfxb | 价值风向标 | +| shuducaijing | 数读财经 | +| shujujiepan | 数据解盘 | +| shudushenghuo | 数读生活 | +| cbndata | CBNData | +| dtcj | DT 财经 | +| xfsz | 消费数知 |`, }; async function handler(ctx) { diff --git a/lib/routes/yicai/utils.ts b/lib/routes/yicai/utils.ts index 779c7790459fc5..d1f58a7be4b2bf 100644 --- a/lib/routes/yicai/utils.ts +++ b/lib/routes/yicai/utils.ts @@ -35,25 +35,31 @@ const ProcessItems = async (apiUrl, tryGet) => { }), })); - return Promise.all( - items.map((item) => - tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); + return Promise.all(fetchFullArticles(items, tryGet)); +}; +function fetchFullArticles(items, tryGet) { + return items.map((item) => + tryGet(item.link, async () => { + const detailResponse = await got({ + method: 'get', + url: item.link, + }); - const content = load(detailResponse.data); + const content = load(detailResponse.data); - content('h1').remove(); - content('.u-btn6, .m-smallshare, .topic-hot').remove(); + if (!item.pubDate) { + const dataScript = content("script[src='/js/alert.min.js']").next().text() || content('title').next().text(); + const pb = new Map(JSON.parse(dataScript.match(/_pb = (\[.*?]);/)[1].replaceAll("'", '"'))); + item.pubDate = parseDate(`${pb.get('actime')}:00`); + } - item.description += content('.multiText, #multi-text, .txt').html() ?? ''; + content('h1').remove(); + content('.u-btn6, .m-smallshare, .topic-hot').remove(); - return item; - }) - ) - ); -}; + item.description = (item.description ?? '') + (content('.multiText, #multi-text, .txt').html() ?? ''); -export { rootUrl, ProcessItems }; + return item; + }) + ); +} +export { rootUrl, ProcessItems, fetchFullArticles }; diff --git a/lib/routes/yicai/video.ts b/lib/routes/yicai/video.ts index 8984651e6a7904..1fb9749f1eb2f4 100644 --- a/lib/routes/yicai/video.ts +++ b/lib/routes/yicai/video.ts @@ -27,42 +27,42 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| Id | 名称 | - | -------------------- | ---------------------------- | - | youliao | 有料 | - | appshipin | 此刻 | - | yicaisudi | 速递 | - | caishang | 财商 | - | shiji | 史记 | - | jinrigushi | 今日股市 | - | tangulunjin | 谈股论金 | - | gongsiyuhangye | 公司与行业 | - | cjyxx | 财经夜行线 | - | 6thtradingday | 第六交易日 | - | cjfw | 财经风味 | - | chuangshidai | 创时代 | - | weilaiyaoqinghan | 未来邀请函 | - | tounaofengbao | 头脑风暴 | - | zhongguojingyingzhe | 中国经营者 | - | shichanglingjuli | 市场零距离 | - | huanqiucaijing | 环球财经视界 | - | zgjcqyjglsxftl | 中国杰出企业家管理思想访谈录 | - | jiemacaishang | 解码财商 | - | sxpl | 首席评论 | - | zhongguojingjiluntan | 中国经济论坛 | - | opinionleader | 意见领袖 | - | xinjinrong | 解码新金融 | - | diyidichan | 第一地产 | - | zhichedaren | 智车达人 | - | chuangtoufengyun | 创投风云 | - | chunxiangrensheng | 醇享人生 | - | diyishengyin | 第一声音 | - | sanliangboqianjin | 财智双全 | - | weilaiyaoqinghan | 未来邀请函 | - | zjdy | 主角 ▪ 大医 | - | leye | 乐业之城 | - | sanrenxing | 价值三人行 | - | yuandongli | 中国源动力 | - | pioneerzone | 直击引领区 |`, +| -------------------- | ---------------------------- | +| youliao | 有料 | +| appshipin | 此刻 | +| yicaisudi | 速递 | +| caishang | 财商 | +| shiji | 史记 | +| jinrigushi | 今日股市 | +| tangulunjin | 谈股论金 | +| gongsiyuhangye | 公司与行业 | +| cjyxx | 财经夜行线 | +| 6thtradingday | 第六交易日 | +| cjfw | 财经风味 | +| chuangshidai | 创时代 | +| weilaiyaoqinghan | 未来邀请函 | +| tounaofengbao | 头脑风暴 | +| zhongguojingyingzhe | 中国经营者 | +| shichanglingjuli | 市场零距离 | +| huanqiucaijing | 环球财经视界 | +| zgjcqyjglsxftl | 中国杰出企业家管理思想访谈录 | +| jiemacaishang | 解码财商 | +| sxpl | 首席评论 | +| zhongguojingjiluntan | 中国经济论坛 | +| opinionleader | 意见领袖 | +| xinjinrong | 解码新金融 | +| diyidichan | 第一地产 | +| zhichedaren | 智车达人 | +| chuangtoufengyun | 创投风云 | +| chunxiangrensheng | 醇享人生 | +| diyishengyin | 第一声音 | +| sanliangboqianjin | 财智双全 | +| weilaiyaoqinghan | 未来邀请函 | +| zjdy | 主角 ▪ 大医 | +| leye | 乐业之城 | +| sanrenxing | 价值三人行 | +| yuandongli | 中国源动力 | +| pioneerzone | 直击引领区 |`, }; async function handler(ctx) { diff --git a/lib/routes/yilinzazhi/index.ts b/lib/routes/yilinzazhi/index.ts new file mode 100644 index 00000000000000..2eea833e9d889e --- /dev/null +++ b/lib/routes/yilinzazhi/index.ts @@ -0,0 +1,59 @@ +import { Data, DataItem, Route, ViewType } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/', + categories: ['reading', 'popular'], + view: ViewType.Articles, + example: '/yilinzazhi', + radar: [ + { + source: ['www.yilinzazhi.com'], + target: '/', + }, + ], + name: '文章列表', + maintainers: ['g0ngjie'], + handler, + url: 'www.yilinzazhi.com', +}; + +async function handler(): Promise<Data> { + const baseUrl = 'https://www.yilinzazhi.com/'; + const response = await got(baseUrl); + const $ = load(response.data); + const contents: DataItem[] = $('section.content') + .find('li') + .toArray() + .map((target) => { + const li = $(target); + + const aTag = li.find('a'); + const title = aTag.text(); + const link = baseUrl + aTag.attr('href'); + + return { + title, + link, + description: '', + }; + }); + + const items = (await Promise.all( + contents.map((content) => + cache.tryGet(content.link!, async () => { + const childRes = await got(content.link); + const $$ = load(childRes.data); + content.description = $$('.maglistbox').html()!; + return content; + }) + ) + )) as DataItem[]; + return { + title: '意林杂志网', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/yilinzazhi/latest.ts b/lib/routes/yilinzazhi/latest.ts new file mode 100644 index 00000000000000..828caf48918f4c --- /dev/null +++ b/lib/routes/yilinzazhi/latest.ts @@ -0,0 +1,104 @@ +import { Data, DataItem, Route, ViewType } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import dayjs from 'dayjs'; + +export const route: Route = { + path: '/latest', + categories: ['reading', 'popular'], + view: ViewType.Articles, + example: '/yilinzazhi/latest', + radar: [ + { + source: ['www.yilinzazhi.com'], + target: '/', + }, + ], + name: '近期文章汇总', + maintainers: ['g0ngjie'], + handler, + url: 'www.yilinzazhi.com', + description: '最近一期的文章汇总', +}; + +type Stage = { + link: string; + title: string; +}; + +type Catalog = { + title: string; + tables: Data[]; +}; + +async function handler(): Promise<Data> { + const baseUrl = 'https://www.yilinzazhi.com/'; + const response = await got(baseUrl); + const $ = load(response.data); + + const currentYear = dayjs().year(); + const yearSection = $('.year-section') + .toArray() + .find((el) => + $(el) + .find('.year-title') + .text() + .includes(currentYear + '') + ); + + const stage = $(yearSection!) + .find('a') + .toArray() + .map<Stage>((elem) => { + const aTag = $(elem); + const link = baseUrl + aTag.attr('href'); + const title = aTag.text(); + return { link, title }; + })[0]; + + const catalogs = (await cache.tryGet(stage.link, async () => { + const stageRes = await got(stage.link); + const $$ = load(stageRes.data); + const catalogsEl = $$('.maglistbox dl').toArray(); + const children = catalogsEl.map<Catalog>((catalog) => { + const title = $$(catalog).find('dt span').text(); + const tables = $$(catalog) + .find('a') + .toArray() + .map<Data>((aTag) => { + const href = $$(aTag).attr('href')!; + const yearType = currentYear + href.substring(4, 5); + return { + title: $$(aTag).text(), + link: `${baseUrl}${currentYear}/yl${yearType}/${href}`, + }; + }); + return { title, tables }; + }); + return children; + })) as Catalog[]; + + const contents: Data[] = catalogs.flatMap((catalog) => catalog.tables); + + const items = (await Promise.all( + contents.map( + async (target) => + await cache.tryGet(target.link!, async () => { + const detailRes = await got(target.link); + const $$ = load(detailRes.data); + const detailContainer = $$('.blkContainerSblk.collectionContainer'); + + target.description = detailContainer.html()!; + + return target; + }) + ) + )) as DataItem[]; + + return { + title: '意林 - 近期文章汇总', + link: stage.link, + item: items, + }; +} diff --git a/lib/routes/yilinzazhi/namespace.ts b/lib/routes/yilinzazhi/namespace.ts new file mode 100644 index 00000000000000..9bc96d4a44a339 --- /dev/null +++ b/lib/routes/yilinzazhi/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '意林杂志', + url: 'www.yilinzazhi.com', + categories: ['reading'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/ymgal/article.ts b/lib/routes/ymgal/article.ts index 0a8435d7088bff..d8a21ab37d9f9f 100644 --- a/lib/routes/ymgal/article.ts +++ b/lib/routes/ymgal/article.ts @@ -29,8 +29,8 @@ export const route: Route = { maintainers: ['SunBK201'], handler, description: `| 全部文章 | 资讯 | 专栏 | - | -------- | ---- | ------ | - | all | news | column |`, +| -------- | ---- | ------ | +| all | news | column |`, }; async function handler(ctx) { @@ -42,7 +42,7 @@ async function handler(ctx) { await Promise.all( Object.values(types).map(async (type) => { const response = await got(`${host}/co/topic/list${type}`); - data.push(response.data.data); + data.push(...response.data.data); }) ); data = data.sort((a, b) => b.publishTime - a.publishTime).slice(0, 10); diff --git a/lib/routes/ymgal/namespace.ts b/lib/routes/ymgal/namespace.ts index f595187df05f36..df169dcd4d0530 100644 --- a/lib/routes/ymgal/namespace.ts +++ b/lib/routes/ymgal/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '月幕 Galgame', url: 'ymgal.games', + lang: 'zh-CN', }; diff --git a/lib/routes/yna/index.ts b/lib/routes/yna/index.ts new file mode 100644 index 00000000000000..62d53c59db6373 --- /dev/null +++ b/lib/routes/yna/index.ts @@ -0,0 +1,92 @@ +import { Route } from '@/types'; +import parser from '@/utils/rss-parser'; +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: '/:lang?/:channel?', + categories: ['traditional-media'], + example: '/yna/en/national', + parameters: { + lang: 'Language, see below, `ko` by default', + channel: 'RSS Feed Channel, see below, `news` by default', + }, + features: { + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + requireConfig: false, + }, + name: 'News', + maintainers: ['quiniapiezoelectricity'], + handler, + description: ` +| Language | 한국어 | English | 简体中文 | 日本語 | عربي | Español | Français | +| --------- | ------ | ------- | -------- | ------ | ------ | ------- | -------- | +| \`:lang\` | \`ko\` | \`en\` | \`cn\` | \`jp\` | \`ar\` | \`es\` | \`fr\` | + +For a full list of RSS Feed Channels, please refer to the RSS feed page of the corresponding language +| RSS Feed Page | +| --------------------------------------------------------- | +| [한국어](https://www.yna.co.kr/rss/index?site=footer_rss) | +| [English](https://en.yna.co.kr/channel/index) | +| [简体中文](https://cn.yna.co.kr/channel/index) | +| [日本語](https://jp.yna.co.kr/channel/index) | +| [عربي](https://ar.yna.co.kr/channel/index) | +| [Español](https://sp.yna.co.kr/channel/index) | +| [Français](https://fr.yna.co.kr/channel/index) | + +::: tip +For example, the path for the RSS feed url https://www.yna.co.kr/rss/economy.xml and https://cn.yna.co.kr/RSS/news.xml would be \`/ko/economy\` and \`/cn/news\` respectively. +::: +`, +}; + +async function handler(ctx) { + const lang = ctx.req.param('lang') ?? 'ko'; + const channel = ctx.req.param('channel') ?? 'news'; + let url; + switch (lang) { + case 'ko': + url = `https://www.yna.co.kr/rss/${channel}.xml`; + break; + default: + url = `https://${lang}.yna.co.kr/RSS/${channel}.xml`; + break; + } + + const feed = await parser.parseURL(url); + const items = await Promise.all( + feed.items.map((item) => + cache.tryGet(item.link, async () => { + item.pubDate = lang === 'ko' ? parseDate(item.pubDate) : timezone(parseDate(item.pubDate), +9); // Timezone is only included in the pubDate of the Korean language RSS + const response = await got(item.link); + const $ = load(response.data); + item.author = + item.creator ?? + $('.tit-name') + .toArray() + .map((c) => $(c).text()) + .join(', '); + const article = $('article.story-news'); + article.find('.related-group').remove(); + article.find('.writer-zone01').remove(); + item.description = article.html(); + return item; + }) + ) + ); + + return { + title: feed.title, + link: feed.link, + description: feed.description, + language: feed.language ?? lang, + item: items, + }; +} diff --git a/lib/routes/yna/namespace.ts b/lib/routes/yna/namespace.ts new file mode 100644 index 00000000000000..361481423442a1 --- /dev/null +++ b/lib/routes/yna/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Yonhap News Agency', + url: 'yna.co.kr', + lang: 'ko', + zh: { + name: '韩联社', + }, +}; diff --git a/lib/routes/yoasobi-music/jsonp-helper.ts b/lib/routes/yoasobi-music/jsonp-helper.ts index d2e8ddfa55c8ef..81d3e1eddc522b 100644 --- a/lib/routes/yoasobi-music/jsonp-helper.ts +++ b/lib/routes/yoasobi-music/jsonp-helper.ts @@ -5,7 +5,7 @@ function parseJSONP(jsonpData) { let jsonString = jsonpData.substring(startPos + 1, endPos + 1); // remove escaped single quotes since they are not valid json - jsonString = jsonString.replaceAll("\\'", "'"); + jsonString = jsonString.replaceAll(String.raw`\'`, "'"); return JSON.parse(jsonString); } catch (error_) { diff --git a/lib/routes/yoasobi-music/namespace.ts b/lib/routes/yoasobi-music/namespace.ts index 0bb58dbbb74349..3f9e72503ef4bd 100644 --- a/lib/routes/yoasobi-music/namespace.ts +++ b/lib/routes/yoasobi-music/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Yoasobi Official', url: 'www.yoasobi-music.jp', + lang: 'ja', }; diff --git a/lib/routes/yomiuri/namespace.ts b/lib/routes/yomiuri/namespace.ts index ab8d90bfe05d4c..8d252fa3a6e149 100644 --- a/lib/routes/yomiuri/namespace.ts +++ b/lib/routes/yomiuri/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Yomiuri Shimbun 読売新聞', url: 'www.yomiuri.co.jp', + lang: 'ja', }; diff --git a/lib/routes/yomiuri/news.ts b/lib/routes/yomiuri/news.ts index 678789f1c69246..57d3253b5a60d0 100644 --- a/lib/routes/yomiuri/news.ts +++ b/lib/routes/yomiuri/news.ts @@ -28,24 +28,24 @@ export const route: Route = { handler, description: `Free articles only. - | Category | Parameter | - | -------------- | --------- | - | 新着・速報 | news | - | 社会 | national | - | 政治 | politics | - | 経済 | economy | - | スポーツ | sports | - | 国際 | world | - | 地域 | local | - | 科学・IT | science | - | エンタメ・文化 | culture | - | ライフ | life | - | 医療・健康 | medical | - | 教育・就活 | kyoiku | - | 選挙・世論調査 | election | - | 囲碁・将棋 | igoshougi | - | 社説 | editorial | - | 皇室 | koushitsu |`, +| Category | Parameter | +| -------------- | --------- | +| 新着・速報 | news | +| 社会 | national | +| 政治 | politics | +| 経済 | economy | +| スポーツ | sports | +| 国際 | world | +| 地域 | local | +| 科学・IT | science | +| エンタメ・文化 | culture | +| ライフ | life | +| 医療・健康 | medical | +| 教育・就活 | kyoiku | +| 選挙・世論調査 | election | +| 囲碁・将棋 | igoshougi | +| 社説 | editorial | +| 皇室 | koushitsu |`, }; async function handler(ctx) { diff --git a/lib/routes/yomujp/namespace.ts b/lib/routes/yomujp/namespace.ts index b173c7d6c94e9b..e4577af7fb5b18 100644 --- a/lib/routes/yomujp/namespace.ts +++ b/lib/routes/yomujp/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '日本語多読道場', url: 'yomujp.com', + lang: 'ja', }; diff --git a/lib/routes/youku/namespace.ts b/lib/routes/youku/namespace.ts index 92ff5cdaa721ba..8e6635c9f895c5 100644 --- a/lib/routes/youku/namespace.ts +++ b/lib/routes/youku/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '优酷', url: 'i.youku.com', + lang: 'zh-CN', }; diff --git a/lib/routes/youmemark/index.ts b/lib/routes/youmemark/index.ts new file mode 100644 index 00000000000000..feacaed4b204b0 --- /dev/null +++ b/lib/routes/youmemark/index.ts @@ -0,0 +1,88 @@ +import { Route, Data } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/:userid', + categories: ['blog'], + example: '/youmemark/pseudoyu', + parameters: { userid: '`userid` is the user id of youmemark' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Bookmarks', + maintainers: ['pseudoyu'], + handler, + radar: [ + { + source: ['youmemark.com/user/:userid'], + target: '/:userid', + }, + ], + description: `Get user's public bookmarks from YouMeMark +::: tip + Add \`?limit=<number>\` to the end of the route to limit the number of items. Default is 10. +:::`, +}; + +async function handler(ctx): Promise<Data> { + const userid = ctx.req.param('userid'); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10; + + const response = await ofetch(`https://youmemark.com/user/${userid}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }, + }); + + const $ = load(response); + + const name = $('h2.font-bold').text().trim(); + const avatar = $('span.relative.flex img, span.relative.flex.shrink-0 img').attr('src'); + const intro = $('.text-center .prose p, .text-sm.text-gray-500.text-center .prose p').first().text().trim(); + + const items: Data['item'] = []; + $('h2:contains("收集箱")') + .parent() + .find('.rounded-lg.space-y-2') + .each((_, element) => { + const $item = $(element); + + const $linkDiv = $item.find('> div').first(); + const $link = $linkDiv.find('a'); + const title = $link.find('span').first().text().trim(); + const domain = $link.find('span').last().text().trim().replaceAll(/[()]/g, '').trim(); + const link = $link.attr('href'); + + const $contentDiv = $linkDiv.find('> div.text-sm.text-gray-500'); + const content = $contentDiv.find('p').text().trim(); + + const dateStr = $item.find('.text-xs.text-gray-500 span').text().trim(); + + if (link && title && dateStr) { + items.push({ + title, + link, + description: content, + pubDate: parseDate(dateStr, 'YYYY-MM-DD'), + author: domain, + guid: link, + }); + } + }); + + return { + title: `${name}'s Bookmarks - YouMeMark`, + link: `https://youmemark.com/user/${userid}`, + description: intro, + image: avatar, + item: items.slice(0, limit), + language: 'en', + } as Data; +} diff --git a/lib/routes/youmemark/namespace.ts b/lib/routes/youmemark/namespace.ts new file mode 100644 index 00000000000000..d80283f62d2ee9 --- /dev/null +++ b/lib/routes/youmemark/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'YouMeMark', + url: 'youmemark.com', + lang: 'zh-CN', +}; diff --git a/lib/routes/youtube/channel.ts b/lib/routes/youtube/channel.ts index abd64ec0433435..3a0690a50226e7 100644 --- a/lib/routes/youtube/channel.ts +++ b/lib/routes/youtube/channel.ts @@ -30,10 +30,10 @@ export const route: Route = { target: '/channel/:id', }, ], - name: 'Channel', + name: 'Channel with id', maintainers: ['DIYgod'], handler, - description: `:::tip + description: `::: tip YouTube provides official RSS feeds for channels, for instance [https://www.youtube.com/feeds/videos.xml?channel\_id=UCDwDMPOZfxVV0x\_dz0eQ8KQ](https://www.youtube.com/feeds/videos.xml?channel_id=UCDwDMPOZfxVV0x_dz0eQ8KQ). :::`, }; @@ -69,6 +69,7 @@ async function handler(ctx) { pubDate: parseDate(snippet.publishedAt), link: `https://www.youtube.com/watch?v=${videoId}`, author: snippet.videoOwnerChannelTitle, + image: img.url, }; }), }; diff --git a/lib/routes/youtube/charts.ts b/lib/routes/youtube/charts.ts index 0bfa8d4f9fedba..aac74d1c1fb352 100644 --- a/lib/routes/youtube/charts.ts +++ b/lib/routes/youtube/charts.ts @@ -14,47 +14,47 @@ export const route: Route = { handler, description: `Chart - | Top artists | Top songs | Top music videos | Trending | - | ----------- | --------- | ---------------- | -------------- | - | TopArtists | TopSongs | TopVideos | TrendingVideos | +| Top artists | Top songs | Top music videos | Trending | +| ----------- | --------- | ---------------- | -------------- | +| TopArtists | TopSongs | TopVideos | TrendingVideos | Country Code - | Argentina | Australia | Austria | Belgium | Bolivia | Brazil | Canada | - | --------- | --------- | ------- | ------- | ------- | ------ | ------ | - | ar | au | at | be | bo | br | ca | +| Argentina | Australia | Austria | Belgium | Bolivia | Brazil | Canada | +| --------- | --------- | ------- | ------- | ------- | ------ | ------ | +| ar | au | at | be | bo | br | ca | - | Chile | Colombia | Costa Rica | Czechia | Denmark | Dominican Republic | Ecuador | - | ----- | -------- | ---------- | ------- | ------- | ------------------ | ------- | - | cl | co | cr | cz | dk | do | ec | +| Chile | Colombia | Costa Rica | Czechia | Denmark | Dominican Republic | Ecuador | +| ----- | -------- | ---------- | ------- | ------- | ------------------ | ------- | +| cl | co | cr | cz | dk | do | ec | - | Egypt | El Salvador | Estonia | Finland | France | Germany | Guatemala | - | ----- | ----------- | ------- | ------- | ------ | ------- | --------- | - | eg | sv | ee | fi | fr | de | gt | +| Egypt | El Salvador | Estonia | Finland | France | Germany | Guatemala | +| ----- | ----------- | ------- | ------- | ------ | ------- | --------- | +| eg | sv | ee | fi | fr | de | gt | - | Honduras | Hungary | Iceland | India | Indonesia | Ireland | Israel | Italy | - | -------- | ------- | ------- | ----- | --------- | ------- | ------ | ----- | - | hn | hu | is | in | id | ie | il | it | +| Honduras | Hungary | Iceland | India | Indonesia | Ireland | Israel | Italy | +| -------- | ------- | ------- | ----- | --------- | ------- | ------ | ----- | +| hn | hu | is | in | id | ie | il | it | - | Japan | Kenya | Luxembourg | Mexico | Netherlands | New Zealand | Nicaragua | - | ----- | ----- | ---------- | ------ | ----------- | ----------- | --------- | - | jp | ke | lu | mx | nl | nz | ni | +| Japan | Kenya | Luxembourg | Mexico | Netherlands | New Zealand | Nicaragua | +| ----- | ----- | ---------- | ------ | ----------- | ----------- | --------- | +| jp | ke | lu | mx | nl | nz | ni | - | Nigeria | Norway | Panama | Paraguay | Peru | Poland | Portugal | Romania | - | ------- | ------ | ------ | -------- | ---- | ------ | -------- | ------- | - | ng | no | pa | py | pe | pl | pt | ro | +| Nigeria | Norway | Panama | Paraguay | Peru | Poland | Portugal | Romania | +| ------- | ------ | ------ | -------- | ---- | ------ | -------- | ------- | +| ng | no | pa | py | pe | pl | pt | ro | - | Russia | Saudi Arabia | Serbia | South Africa | South Korea | Spain | Sweden | Switzerland | - | ------ | ------------ | ------ | ------------ | ----------- | ----- | ------ | ----------- | - | ru | sa | rs | za | kr | es | se | ch | +| Russia | Saudi Arabia | Serbia | South Africa | South Korea | Spain | Sweden | Switzerland | +| ------ | ------------ | ------ | ------------ | ----------- | ----- | ------ | ----------- | +| ru | sa | rs | za | kr | es | se | ch | - | Tanzania | Turkey | Uganda | Ukraine | United Arab Emirates | United Kingdom | United States | - | -------- | ------ | ------ | ------- | -------------------- | -------------- | ------------- | - | tz | tr | ug | ua | ae | gb | us | +| Tanzania | Turkey | Uganda | Ukraine | United Arab Emirates | United Kingdom | United States | +| -------- | ------ | ------ | ------- | -------------------- | -------------- | ------------- | +| tz | tr | ug | ua | ae | gb | us | - | Uruguay | Zimbabwe | - | ------- | -------- | - | uy | zw |`, +| Uruguay | Zimbabwe | +| ------- | -------- | +| uy | zw |`, }; async function handler(ctx) { diff --git a/lib/routes/youtube/community.ts b/lib/routes/youtube/community.ts index 344b472a19b325..78e37294835e87 100644 --- a/lib/routes/youtube/community.ts +++ b/lib/routes/youtube/community.ts @@ -47,7 +47,7 @@ async function handler(ctx) { const items = list .filter((i) => i.backstagePostThreadRenderer) .map((item) => { - const post = item.backstagePostThreadRenderer.post.backstagePostRenderer; + const post = item.backstagePostThreadRenderer.post.backstagePostRenderer || item.backstagePostThreadRenderer.post.sharedPostRenderer.originalPost.backstagePostRenderer; const media = post.backstageAttachment?.postMultiImageRenderer?.images.map((i) => i.backstageImageRenderer.image.thumbnails.pop()) ?? [post.backstageAttachment?.backstageImageRenderer?.image.thumbnails.pop()]; return { title: post.contentText.runs[0].text, diff --git a/lib/routes/youtube/custom.ts b/lib/routes/youtube/custom.ts index 2f7f0453d2e403..bdcb33e45a1f43 100644 --- a/lib/routes/youtube/custom.ts +++ b/lib/routes/youtube/custom.ts @@ -12,6 +12,19 @@ export const route: Route = { categories: ['social-media'], example: '/youtube/c/YouTubeCreators', parameters: { username: 'YouTube custom URL', embed: 'Default to embed the video, set to any value to disable embedding' }, + features: { + requireConfig: [ + { + name: 'YOUTUBE_KEY', + description: ' YouTube API Key, support multiple keys, split them with `,`, [API Key application](https://console.developers.google.com/)', + }, + ], + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, radar: [ { source: ['www.youtube.com/c/:id'], @@ -46,6 +59,7 @@ async function handler(ctx) { title: `${username} - YouTube`, link: `https://www.youtube.com/c/${username}`, description: ytInitialData.metadata.channelMetadataRenderer.description, + image: ytInitialData.metadata.channelMetadataRenderer.avatar?.thumbnails?.[0]?.url, item: data .filter((d) => d.snippet.title !== 'Private video' && d.snippet.title !== 'Deleted video') .map((item) => { @@ -58,6 +72,7 @@ async function handler(ctx) { pubDate: parseDate(snippet.publishedAt), link: `https://www.youtube.com/watch?v=${videoId}`, author: snippet.videoOwnerChannelTitle, + image: img.url, }; }), }; diff --git a/lib/routes/youtube/live.ts b/lib/routes/youtube/live.ts index 6e2f10cced8a68..59365f84ae0b99 100644 --- a/lib/routes/youtube/live.ts +++ b/lib/routes/youtube/live.ts @@ -13,7 +13,12 @@ export const route: Route = { example: '/youtube/live/@GawrGura', parameters: { username: 'YouTuber id', embed: 'Default to embed the video, set to any value to disable embedding' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'YOUTUBE_KEY', + description: ' YouTube API Key, support multiple keys, split them with `,`, [API Key application](https://console.developers.google.com/)', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -63,6 +68,7 @@ async function handler(ctx) { pubDate: parseDate(snippet.publishedAt), guid: liveVideoId, link: `https://www.youtube.com/watch?v=${liveVideoId}`, + image: img.url, }; }), allowEmpty: true, diff --git a/lib/routes/youtube/namespace.ts b/lib/routes/youtube/namespace.ts index 83dd26a0bd8665..1ee16b1dd8f48b 100644 --- a/lib/routes/youtube/namespace.ts +++ b/lib/routes/youtube/namespace.ts @@ -1,6 +1,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'YouTube Live', - url: 'charts.youtube.com', + name: 'YouTube', + url: 'youtube.com', + lang: 'en', }; diff --git a/lib/routes/youtube/playlist.ts b/lib/routes/youtube/playlist.ts index e62717e991d7bd..de9ee2a0a3d2f4 100644 --- a/lib/routes/youtube/playlist.ts +++ b/lib/routes/youtube/playlist.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import utils from './utils'; import { config } from '@/config'; @@ -7,11 +7,17 @@ import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { path: '/playlist/:id/:embed?', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.Videos, example: '/youtube/playlist/PLqQ1RwlxOgeLTJ1f3fNMSwhjVgaWKo_9Z', parameters: { id: 'YouTube playlist id', embed: 'Default to embed the video, set to any value to disable embedding' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'YOUTUBE_KEY', + description: ' YouTube API Key, support multiple keys, split them with `,`, [API Key application](https://console.developers.google.com/)', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -48,6 +54,7 @@ async function handler(ctx) { pubDate: parseDate(snippet.publishedAt), link: `https://www.youtube.com/watch?v=${videoId}`, author: snippet.videoOwnerChannelTitle, + image: img.url, }; }), }; diff --git a/lib/routes/youtube/subscriptions.ts b/lib/routes/youtube/subscriptions.ts index 8932efe2077a96..7455f30be20c8b 100644 --- a/lib/routes/youtube/subscriptions.ts +++ b/lib/routes/youtube/subscriptions.ts @@ -77,12 +77,14 @@ async function handler(ctx) { pubDate: parseDate(snippet.publishedAt), link: `https://www.youtube.com/watch?v=${videoId}`, author: snippet.videoOwnerChannelTitle, + image: img.url, }; }); const ret = { title: 'Subscriptions - YouTube', description: 'YouTube Subscriptions', + link: 'www.youtube.com/feed/subscriptions', item: items, }; diff --git a/lib/routes/youtube/user.ts b/lib/routes/youtube/user.ts index bd1d1e6ecd52ae..5d0fe5a111b781 100644 --- a/lib/routes/youtube/user.ts +++ b/lib/routes/youtube/user.ts @@ -1,19 +1,26 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; -import utils from './utils'; +import utils, { getVideoUrl } from './utils'; import { config } from '@/config'; import { parseDate } from '@/utils/parse-date'; -import got from '@/utils/got'; -import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; import ConfigNotFoundError from '@/errors/types/config-not-found'; +import NotFoundError from '@/errors/types/not-found'; export const route: Route = { path: '/user/:username/:embed?', - categories: ['social-media'], - example: '/youtube/user/JFlaMusic', - parameters: { username: 'YouTuber id', embed: 'Default to embed the video, set to any value to disable embedding' }, + categories: ['social-media', 'popular'], + view: ViewType.Videos, + example: '/youtube/user/@JFlaMusic', + parameters: { username: 'YouTuber handle with @', embed: 'Default to embed the video, set to any value to disable embedding' }, features: { - requireConfig: false, + requireConfig: [ + { + name: 'YOUTUBE_KEY', + description: ' YouTube API Key, support multiple keys, split them with `,`, [API Key application](https://console.developers.google.com/)', + }, + ], requirePuppeteer: false, antiCrawler: false, supportBT: false, @@ -22,11 +29,11 @@ export const route: Route = { }, radar: [ { - source: ['www.youtube.com/user/:username'], + source: ['www.youtube.com/user/:username', 'www.youtube.com/:username'], target: '/user/:username', }, ], - name: 'User', + name: 'Channel with user handle', maintainers: ['DIYgod'], handler, }; @@ -38,25 +45,55 @@ async function handler(ctx) { const username = ctx.req.param('username'); const embed = !ctx.req.param('embed'); - let playlistId; - let channelName; + let userHandleData; if (username.startsWith('@')) { - const link = `https://www.youtube.com/${username}`; - const response = await got(link); - const $ = load(response.data); - const channelId = $('meta[itemprop="identifier"]').attr('content'); - channelName = $('meta[itemprop="name"]').attr('content'); - playlistId = (await utils.getChannelWithId(channelId, 'contentDetails', cache)).data.items[0].contentDetails.relatedPlaylists.uploads; + userHandleData = await cache.tryGet(`youtube:handle:${username}`, async () => { + const link = `https://www.youtube.com/${username}`; + const response = await ofetch(link); + const $ = cheerio.load(response); + const ytInitialData = JSON.parse( + $('script') + .text() + .match(/ytInitialData = ({.*?});/)?.[1] || '{}' + ); + const metadataRenderer = ytInitialData.metadata.channelMetadataRenderer; + + const channelId = metadataRenderer.externalId; + const channelName = metadataRenderer.title; + const image = metadataRenderer.avatar?.thumbnails?.[0]?.url; + const description = metadataRenderer.description; + const playlistId = (await utils.getChannelWithId(channelId, 'contentDetails', cache)).data.items[0].contentDetails.relatedPlaylists.uploads; + + return { + channelName, + image, + description, + playlistId, + }; + }); } - playlistId = playlistId || (await utils.getChannelWithUsername(username, 'contentDetails', cache)).data.items[0].contentDetails.relatedPlaylists.uploads; + const playlistId = + userHandleData?.playlistId || + (await (async () => { + const channelData = await utils.getChannelWithUsername(username, 'contentDetails', cache); + const items = channelData.data.items; + if (!items) { + throw new NotFoundError(`The channel https://www.youtube.com/user/${username} does not exist.`); + } + return items[0].contentDetails.relatedPlaylists.uploads; + })()); - const data = (await utils.getPlaylistItems(playlistId, 'snippet', cache)).data.items; + const playlistItems = await utils.getPlaylistItems(playlistId, 'snippet', cache); + if (!playlistItems) { + throw new NotFoundError("This channel doesn't have any content."); + } return { - title: `${channelName || username} - YouTube`, + title: `${userHandleData?.channelName || username} - YouTube`, link: username.startsWith('@') ? `https://www.youtube.com/${username}` : `https://www.youtube.com/user/${username}`, - description: `YouTube user ${username}`, - item: data + description: userHandleData?.description || `YouTube user ${username}`, + image: userHandleData?.image, + item: playlistItems.data.items .filter((d) => d.snippet.title !== 'Private video' && d.snippet.title !== 'Deleted video') .map((item) => { const snippet = item.snippet; @@ -68,6 +105,13 @@ async function handler(ctx) { pubDate: parseDate(snippet.publishedAt), link: `https://www.youtube.com/watch?v=${videoId}`, author: snippet.videoOwnerChannelTitle, + image: img.url, + attachments: [ + { + url: getVideoUrl(videoId), + mime_type: 'text/html', + }, + ], }; }), }; diff --git a/lib/routes/youtube/utils.ts b/lib/routes/youtube/utils.ts index dcf297c3504e64..0b468409a687f9 100644 --- a/lib/routes/youtube/utils.ts +++ b/lib/routes/youtube/utils.ts @@ -152,6 +152,8 @@ export const getLive = (id, cache) => ); return res; }); +export const getVideoUrl = (id: string) => `https://www.youtube-nocookie.com/embed/${id}?controls=1&autoplay=1&mute=0`; + const youtubeUtils = { getPlaylistItems, getPlaylist, @@ -164,5 +166,6 @@ const youtubeUtils = { getSubscriptionsRecusive, isYouTubeChannelId, getLive, + getVideoUrl, }; export default youtubeUtils; diff --git a/lib/routes/youzhiyouxing/materials.ts b/lib/routes/youzhiyouxing/materials.ts index e71c9abacf59d8..12428a7c8fc4b3 100644 --- a/lib/routes/youzhiyouxing/materials.ts +++ b/lib/routes/youzhiyouxing/materials.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,24 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/materials/:id?', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/youzhiyouxing/materials', - parameters: { id: '分类,见下表,默认为全部' }, + parameters: { + id: { + description: '分类', + options: [ + { value: '0', label: '全部' }, + { value: '4', label: '知行小酒馆' }, + { value: '2', label: '知行黑板报' }, + { value: '10', label: '无人知晓' }, + { value: '1', label: '孟岩专栏' }, + { value: '3', label: '知行读书会' }, + { value: '11', label: '你好,同路人' }, + ], + default: '0', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -28,8 +43,8 @@ export const route: Route = { handler, url: 'youzhiyouxing.cn/materials', description: `| 全部 | 知行小酒馆 | 知行黑板报 | 无人知晓 | 孟岩专栏 | 知行读书会 | 你好,同路人 | - | :--: | :--------: | :--------: | :------: | :------: | :--------: | :----------: | - | 0 | 4 | 2 | 10 | 1 | 3 | 11 |`, +| :--: | :--------: | :--------: | :------: | :------: | :--------: | :----------: | +| 0 | 4 | 2 | 10 | 1 | 3 | 11 |`, }; async function handler(ctx) { diff --git a/lib/routes/youzhiyouxing/namespace.ts b/lib/routes/youzhiyouxing/namespace.ts index 2f957a47e63f1e..3cd9909935779d 100644 --- a/lib/routes/youzhiyouxing/namespace.ts +++ b/lib/routes/youzhiyouxing/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '有知有行', url: 'youzhiyouxing.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/yuque/book.ts b/lib/routes/yuque/book.ts index 883a54db8c55b7..408b8e12834fc4 100644 --- a/lib/routes/yuque/book.ts +++ b/lib/routes/yuque/book.ts @@ -31,8 +31,8 @@ export const route: Route = { maintainers: ['aha2mao', 'ltaoo'], handler, description: `| Node.js 专栏 | 阮一峰每周分享 | 语雀使用手册 | - | -------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------- | - | [/yuque/egg/nodejs](https://rsshub.app/yuque/egg/nodejs) | [/yuque/ruanyf/weekly](https://rsshub.app/yuque/ruanyf/weekly) | [/yuque/yuque/help](https://rsshub.app/yuque/yuque/help) |`, +| -------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------- | +| [/yuque/egg/nodejs](https://rsshub.app/yuque/egg/nodejs) | [/yuque/ruanyf/weekly](https://rsshub.app/yuque/ruanyf/weekly) | [/yuque/yuque/help](https://rsshub.app/yuque/yuque/help) |`, }; async function handler(ctx) { diff --git a/lib/routes/yuque/namespace.ts b/lib/routes/yuque/namespace.ts index ae395404b9b6c7..460ebbda06ad3e 100644 --- a/lib/routes/yuque/namespace.ts +++ b/lib/routes/yuque/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '语雀', url: 'yuque.com', + lang: 'zh-CN', }; diff --git a/lib/routes/yuque/utils.ts b/lib/routes/yuque/utils.ts index ccf5fbc7182ba2..58d719964b4533 100644 --- a/lib/routes/yuque/utils.ts +++ b/lib/routes/yuque/utils.ts @@ -8,6 +8,7 @@ const card2Html = (elem, link) => { case 'emoji': case 'flowchart2': case 'image': + case 'math': case 'mindmap': case 'puml': html = `<img src='${value.src}'>`; diff --git a/lib/routes/yxdown/namespace.ts b/lib/routes/yxdown/namespace.ts index 4dec1c81b3f3f3..969092288d5e09 100644 --- a/lib/routes/yxdown/namespace.ts +++ b/lib/routes/yxdown/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '游讯网', url: 'yxdown.com', + lang: 'zh-CN', }; diff --git a/lib/routes/yxdown/news.ts b/lib/routes/yxdown/news.ts index 20f34bee00dbd5..b88acf0785a11e 100644 --- a/lib/routes/yxdown/news.ts +++ b/lib/routes/yxdown/news.ts @@ -23,8 +23,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 资讯首页 | 业界动态 | 视频预告 | 新作发布 | 游戏资讯 | 游戏评测 | 网络游戏 | 手机游戏 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | | dongtai | yugao | xinzuo | zixun | pingce | wangluo | shouyou |`, +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| | dongtai | yugao | xinzuo | zixun | pingce | wangluo | shouyou |`, }; async function handler(ctx) { diff --git a/lib/routes/yxdzqb/index.ts b/lib/routes/yxdzqb/index.ts index f7ddd01463f45b..42bdbacfde38bc 100644 --- a/lib/routes/yxdzqb/index.ts +++ b/lib/routes/yxdzqb/index.ts @@ -40,8 +40,8 @@ export const route: Route = { handler, url: 'yxdzqb.com/', description: `| Steam 最新折扣 | Steam 热门游戏折扣 | Steam 热门中文游戏折扣 | Steam 历史低价 | Steam 中文游戏历史低价 | - | -------------- | ------------------ | ---------------------- | -------------- | ---------------------- | - | discount | popular | popular\_cn | low | low\_cn |`, +| -------------- | ------------------ | ---------------------- | -------------- | ---------------------- | +| discount | popular | popular\_cn | low | low\_cn |`, }; async function handler(ctx) { diff --git a/lib/routes/yxdzqb/namespace.ts b/lib/routes/yxdzqb/namespace.ts index ec539b1f2d95dd..7519a9c7a1996b 100644 --- a/lib/routes/yxdzqb/namespace.ts +++ b/lib/routes/yxdzqb/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '游戏打折情报', url: 'yxdzqb.com', + lang: 'zh-CN', }; diff --git a/lib/routes/yxrb/home.ts b/lib/routes/yxrb/home.ts index d8e59cea085107..c66c05fd27082a 100644 --- a/lib/routes/yxrb/home.ts +++ b/lib/routes/yxrb/home.ts @@ -30,8 +30,8 @@ export const route: Route = { maintainers: ['TonyRL'], handler, description: `| 资讯 | 访谈 | 服务 | 游理游据 | - | ---- | ------- | ------- | -------- | - | info | talking | service | comments |`, +| ---- | ------- | ------- | -------- | +| info | talking | service | comments |`, }; async function handler(ctx) { diff --git a/lib/routes/yxrb/namespace.ts b/lib/routes/yxrb/namespace.ts index 144793e970cc68..ad5a85448ca7fe 100644 --- a/lib/routes/yxrb/namespace.ts +++ b/lib/routes/yxrb/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '游戏日报', url: 'news.yxrb.net', + lang: 'zh-CN', }; diff --git a/lib/routes/yyets/article.ts b/lib/routes/yyets/article.ts index aa23e9e015e5ec..1a964fb00528ba 100644 --- a/lib/routes/yyets/article.ts +++ b/lib/routes/yyets/article.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'; @@ -9,9 +9,24 @@ const baseURL = 'https://yysub.net'; export const route: Route = { path: '/article/:type?', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Articles, example: '/yyets/article', - parameters: { type: '[' }, + parameters: { + type: { + description: '类型', + options: [ + { value: 'all', label: '全部' }, + { value: 'news', label: '影视资讯' }, + { value: 'report', label: '收视快报' }, + { value: 'm_review', label: '人人影评' }, + { value: 't_review', label: '人人剧评' }, + { value: 'new_review', label: '新剧评测' }, + { value: 'recom', label: '片单推荐' }, + ], + default: 'all', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -24,8 +39,8 @@ export const route: Route = { maintainers: ['wb121017405'], handler, description: `| 全部 | 影视资讯 | 收视快报 | 人人影评 | 人人剧评 | 新剧评测 | 片单推荐 | - | ---- | -------- | -------- | --------- | --------- | ----------- | -------- | - | | news | report | m\_review | t\_review | new\_review | recom |`, +| ---- | -------- | -------- | --------- | --------- | ----------- | -------- | +| | news | report | m\_review | t\_review | new\_review | recom |`, }; async function handler(ctx) { diff --git a/lib/routes/yyets/namespace.ts b/lib/routes/yyets/namespace.ts index 854e9fade24859..f77cc5b53cba57 100644 --- a/lib/routes/yyets/namespace.ts +++ b/lib/routes/yyets/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '人人影视', url: 'yysub.net', + lang: 'zh-CN', }; diff --git a/lib/routes/yyets/today.ts b/lib/routes/yyets/today.ts index 0bd05556babb7f..1602febfcc7d91 100644 --- a/lib/routes/yyets/today.ts +++ b/lib/routes/yyets/today.ts @@ -1,10 +1,11 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { load } from 'cheerio'; export const route: Route = { path: '/today', - categories: ['multimedia'], + categories: ['multimedia', 'popular'], + view: ViewType.Notifications, example: '/yyets/today', parameters: {}, features: { diff --git a/lib/routes/yystv/category.ts b/lib/routes/yystv/category.ts index 0bfafa04510dc4..ba92ad6a842cd2 100644 --- a/lib/routes/yystv/category.ts +++ b/lib/routes/yystv/category.ts @@ -21,8 +21,8 @@ export const route: Route = { maintainers: ['LightStrawberry'], handler, description: `| 推游 | 游戏史 | 大事件 | 文化 | 趣闻 | 经典回顾 | - | --------- | ------- | ------ | ------- | ---- | -------- | - | recommend | history | big | culture | news | retro |`, +| --------- | ------- | ------ | ------- | ---- | -------- | +| recommend | history | big | culture | news | retro |`, }; async function handler(ctx) { diff --git a/lib/routes/yystv/docs.ts b/lib/routes/yystv/docs.ts index a98f55e0c19c60..350a99cadea882 100644 --- a/lib/routes/yystv/docs.ts +++ b/lib/routes/yystv/docs.ts @@ -1,7 +1,8 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; +import type { Route, DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import { parseRelativeDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; export const route: Route = { path: '/docs', @@ -22,34 +23,42 @@ export const route: Route = { }, ], name: '游研社 - 全部文章', - maintainers: ['HaitianLiu'], + maintainers: ['HaitianLiu', 'yy4382'], handler, url: 'yystv.cn/docs', }; async function handler() { const url = `https://www.yystv.cn/docs`; - const response = await got({ - method: 'get', - url, - }); + const response = await ofetch(url); - const data = response.data; - const $ = load(data); + const $ = load(response); - const items = $('.list-container li') - .slice(0, 18) - .map(function () { + const itemList = $('.list-container li') + .toArray() + .map((item) => { + const itemElement = $(item); const info = { - title: $('.list-article-title', this).text(), - link: 'https://www.yystv.cn' + $('a', this).attr('href'), - pubDate: parseRelativeDate($('.c-999', this).text()), - author: $('.handler-author-link', this).text(), - description: $('.list-article-intro', this).text(), + title: itemElement.find('.list-article-title').text(), + link: 'https://www.yystv.cn' + itemElement.find('a').attr('href'), + pubDate: parseRelativeDate(itemElement.find('.c-999').text()), + author: itemElement.find('.handler-author-link').text(), + description: itemElement.find('.list-article-intro').text(), }; return info; - }) - .get(); + }) satisfies DataItem[]; + + const items = (await Promise.all( + itemList.map( + (item) => + cache.tryGet(item.link, async () => { + const resp = await ofetch(item.link); + const $ = load(resp); + item.description = $('#main section.article-section .doc-content > div').html() || item.description; + return item; + }) as Promise<DataItem> + ) + )) satisfies DataItem[]; return { title: '游研社-' + $('title').text(), diff --git a/lib/routes/yystv/namespace.ts b/lib/routes/yystv/namespace.ts index d9dd87dee36316..bce52f56992696 100644 --- a/lib/routes/yystv/namespace.ts +++ b/lib/routes/yystv/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '游研社', url: 'yystv.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/zagg/namespace.ts b/lib/routes/zagg/namespace.ts index e1cd7691d38e18..9cb4ccfbdc45f9 100644 --- a/lib/routes/zagg/namespace.ts +++ b/lib/routes/zagg/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Zagg', url: 'zagg.com', + lang: 'en', }; diff --git a/lib/routes/zaker/channel.ts b/lib/routes/zaker/channel.ts new file mode 100644 index 00000000000000..e4e6f536fe7545 --- /dev/null +++ b/lib/routes/zaker/channel.ts @@ -0,0 +1,44 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import * as cheerio from 'cheerio'; +import { baseUrl, fetchItem, getSafeLineCookieWithData, parseList } from './utils'; +import asyncPool from 'tiny-async-pool'; + +export const route: Route = { + path: '/channel/:id?', + parameters: { + id: '分类 ID,可在 URL 中找到,默认为 `1`', + }, + radar: [ + { + source: ['www.myzaker.com/channel/:id'], + target: '/channel/:id', + }, + ], + name: '分类', + example: '/zaker/channel/13', + maintainers: ['LogicJake', 'kt286', 'TonyRL'], + handler, +}; + +async function handler(ctx) { + const id = ctx.req.param('id') ?? 1; + const link = `${baseUrl}/channel/${id}`; + + const { cookie, data } = await getSafeLineCookieWithData(link); + + const $ = cheerio.load(data); + const feedTitle = $('head title').text(); + const list = parseList($); + + const items = []; + for await (const item of asyncPool(2, list, (item) => cache.tryGet(item.link!, () => fetchItem(item, cookie)))) { + items.push(item); + } + + return { + title: feedTitle, + link, + item: items, + }; +} diff --git a/lib/routes/zaker/focus.ts b/lib/routes/zaker/focus.ts new file mode 100644 index 00000000000000..4349f9d2a00d97 --- /dev/null +++ b/lib/routes/zaker/focus.ts @@ -0,0 +1,39 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import * as cheerio from 'cheerio'; +import asyncPool from 'tiny-async-pool'; +import { baseUrl, fetchItem, getSafeLineCookieWithData, parseList } from './utils'; + +export const route: Route = { + path: '/focusread', + radar: [ + { + source: ['www.myzaker.com/'], + target: '/focusread', + }, + ], + name: '精读', + example: '/zaker/focusread', + maintainers: ['AlexdanerZe', 'TonyRL'], + handler, +}; + +async function handler() { + const link = `${baseUrl}/?pos=selected_article`; + + const { cookie, data } = await getSafeLineCookieWithData(link); + + const $ = cheerio.load(data); + const list = parseList($); + + const items = []; + for await (const item of asyncPool(2, list, (item) => cache.tryGet(item.link!, () => fetchItem(item, cookie)))) { + items.push(item); + } + + return { + title: 'ZAKER 精读新闻', + link, + item: items, + }; +} diff --git a/lib/routes/zaker/index.ts b/lib/routes/zaker/index.ts deleted file mode 100644 index 59ccbaeebb2ea7..00000000000000 --- a/lib/routes/zaker/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import { parseRelativeDate } from '@/utils/parse-date'; -import timezone from '@/utils/timezone'; - -export const route: Route = { - path: '/:type/:id?', - radar: [ - { - source: ['myzaker.com/:type/:id'], - target: '/:type/:id', - }, - ], - name: 'Unknown', - maintainers: ['LogicJake', 'kt286', 'AlexdanerZe', 'TonyRL'], - handler, -}; - -async function handler(ctx) { - const type = ctx.req.param('type') ?? 'channel'; - const id = ctx.req.param('id') ?? 1; - const rootUrl = 'http://www.myzaker.com'; - const link = type === 'focusread' ? `${rootUrl}/?pos=selected_article` : `${rootUrl}/${type}/${id}`; - - const response = await got({ - url: link, - headers: { - Referer: rootUrl, - }, - }); - const $ = load(response.data); - const feedTitle = $('head title').text(); - - let items = $('div.content-block') - .slice(0, 10) - .map((_, item) => { - item = $(item); - return { - title: item.find('.article-title').text(), - link: 'http:' + item.find('.article-wrap > a').attr('href').replace('http://', ''), - }; - }) - .get(); - - items = await Promise.all( - items.map((item) => - cache.tryGet(item.link, async () => { - const response = await got({ - url: item.link, - headers: { - Referer: link, - }, - }); - - const $ = load(response.data); - - item.description = $('div.article_content div').html() ?? '原文已被删除'; - item.author = $('a.article-auther.line').text(); - item.category = $('.lebel-list') - .find('a') - .toArray() - .map((item) => $(item).text()); - const date = $('span.time').text() ?? undefined; - if (date) { - item.pubDate = timezone(parseRelativeDate(date), +8); - } - - return item; - }) - ) - ); - - return { - title: type === 'focusread' ? 'ZAKER 精读新闻' : feedTitle, - link, - item: items.filter((t) => t.description !== '原文已被删除'), - }; -} diff --git a/lib/routes/zaker/namespace.ts b/lib/routes/zaker/namespace.ts index 1039159e7b7593..fa33e72fc23703 100644 --- a/lib/routes/zaker/namespace.ts +++ b/lib/routes/zaker/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ZAKER', url: 'myzaker.com', + lang: 'zh-CN', }; diff --git a/lib/routes/zaker/utils.ts b/lib/routes/zaker/utils.ts new file mode 100644 index 00000000000000..b100b51a359eeb --- /dev/null +++ b/lib/routes/zaker/utils.ts @@ -0,0 +1,186 @@ +import { DataItem } from '@/types'; + +import CryptoJS from 'crypto-js'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { config } from '@/config'; +import logger from '@/utils/logger'; +import * as cheerio from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const hints = ['globalThis', 'headless', 'languages', 'permHook', 'vendor', 'webDriverValue', 'webdriver']; +export const baseUrl = 'https://www.myzaker.com'; + +const generateSalt = (input: string, targetZeros = 20) => { + for (let nonce = 0; nonce < 100_000_000; nonce++) { + const hash = CryptoJS.SHA256(input + nonce).toString(); + let leadingZeros = 0; + + for (const char of hash) { + if (char !== '0') { + leadingZeros += 4 - Number.parseInt(char, 16).toString(2).length; + break; + } + leadingZeros += 4; + } + + if (leadingZeros >= targetZeros) { + return nonce; + } + } + return 0; +}; + +const padSeed = (seed: string) => { + const padding = '0'.repeat(16); + return CryptoJS.enc.Utf8.parse((seed + padding).slice(0, 16)); +}; + +const encrypt = (data, seed: string) => { + const iv = CryptoJS.enc.Utf8.parse('1234567890123456'); + return CryptoJS.AES.encrypt(JSON.stringify(data), padSeed(seed), { + iv, + padding: CryptoJS.pad.Pkcs7, + }); +}; + +const encryptPayload = (data, seed: string) => encrypt(data, seed).ciphertext.toString(); + +export const getSafeLineCookieWithData = async (link): Promise<{ cookie: string; data: string }> => { + const cacheKey = 'zaker:cookie'; + const cacheAge = 3600; + const cacheIn = await cache.get(cacheKey, false); + if (cacheIn) { + return JSON.parse(cacheIn); + } + const apiBaseUrl = 'https://challenge.rivers.chaitin.cn/captcha/api'; + + const headerResponse = await ofetch.raw(link); + const session = headerResponse.headers + .getSetCookie() + .find((e) => e.startsWith('sl-session')) + ?.split(';')[0] + .split('sl-session=')[1]; + const onceId = headerResponse._data.match(/once_id:\s*"(.*?)",/)?.[1]; + logger.debug(`getSafeLineCookie: sl-session=${session}, onceId=${onceId}`); + if (!/window\.captcha/.test(headerResponse._data)) { + logger.debug('getSafeLineCookie: Failed to get once_id'); + return { + cookie: headerResponse.headers + .getSetCookie() + .map((c) => c.split(';')[0]) + .join('; '), + data: headerResponse._data, + }; + } + + // await ofetch(`${apiBaseUrl}/index.html?${Math.random()}`); + // await ofetch('${apiBaseUrl}/sdk.js'); + const seedResponse = await ofetch<{ req_id: string; seed: string }>(`${apiBaseUrl}/seed`, { + headers: { + Referer: `${baseUrl}/`, + }, + query: { + once_id: onceId, + v: '1.0.0', + hints: hints.sort(() => Math.random() - 0.5).join(','), + }, + }); + + const ua = config.ua; + const seed = seedResponse.seed; + const takeTime = Math.trunc(Math.random() * 2000 + 1000); + logger.debug(`getSafeLineCookie: ua=${ua}, seed=${seed}, takeTime=${takeTime}`); + const payload = encryptPayload( + { + resolution: '1920x1080', + languages: ['en-US'], + useragents: [ua, ua, ua], + hint: 0, + salt: String(generateSalt(seed, 16)), + taketime: takeTime, + }, + seed + ); + + const inspectResponse = await ofetch<{ req_id: string; jwt: string; reason: string }>(`${apiBaseUrl}/inspect`, { + method: 'POST', + headers: { + Referer: `${baseUrl}/`, + 'Content-Type': 'text/plain', + }, + query: { + seed, + }, + body: payload, + }); + logger.debug(`getSafeLineCookie: inspectResponse=${JSON.stringify(inspectResponse)}`); + if (inspectResponse.reason) { + logger.error(`getSafeLineCookie: reason=${inspectResponse.reason}`); + return { + cookie: headerResponse.headers + .getSetCookie() + .map((c) => c.split(';')[0]) + .join('; '), + data: headerResponse._data, + }; + } + + const response = await ofetch.raw(link, { + headers: { + Cookie: `sl-session=${session}; sl_waf_recap=${inspectResponse.jwt}`, + }, + }); + + const cookie = response.headers + .getSetCookie() + .map((c) => c.split(';')[0]) + .join('; '); + logger.debug(`getSafeLineCookie: ${cookie}`); + + cache.set(cacheKey, JSON.stringify(cookie), cacheAge); + return { + cookie, + data: response._data, + }; +}; + +export const parseList = ($: cheerio.CheerioAPI) => { + const winPageData = JSON.parse( + $('script:contains("window.WinPageData")') + .text() + .match(/window\.WinPageData\s*=\s*({.*})/)?.[1] ?? '{}' + ); + + return winPageData.data.article.map((item) => ({ + title: item.title, + description: item.desc, + link: 'https:' + item.url, + author: item.author_name, + pubDate: timezone(parseDate(item.date, 'MM月DD日'), +8), + category: item.tag.map((t) => t.tag), + image: item.thumbnail_mpic, + })) as DataItem[]; +}; + +export const fetchItem = async (item: DataItem, cookie: string) => { + const response = await ofetch(item.link!, { + headers: { + Cookie: cookie as string, + }, + }); + + const $ = cheerio.load(response); + + const content = $('div.article_content div'); + content.find('img').each((_, img) => { + const $img = $(img); + $img.attr('src', $img.attr('data-original')); + $img.removeAttr('data-original'); + }); + + item.description = content.html(); + + return item; +}; diff --git a/lib/routes/zaobao/interactive.ts b/lib/routes/zaobao/interactive.ts index 2cc01cd62dc931..e2b3a624cb7d58 100644 --- a/lib/routes/zaobao/interactive.ts +++ b/lib/routes/zaobao/interactive.ts @@ -6,27 +6,18 @@ export const route: Route = { path: '/interactive-graphics', categories: ['traditional-media'], example: '/zaobao/interactive-graphics', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, name: '互动新闻', maintainers: ['shunf4'], handler, }; async function handler(ctx) { - const sectionLink = `/interactive-graphics`; + const sectionLink = '/interactive-graphics'; const { resultList } = await parseList(ctx, sectionLink); return { - title: `《联合早报》互动新闻`, + title: '《联合早报》互动新闻', link: baseUrl + sectionLink, description: '新加坡、中国、亚洲和国际的即时、评论、商业、体育、生活、科技与多媒体新闻,尽在联合早报。', item: resultList, diff --git a/lib/routes/zaobao/namespace.ts b/lib/routes/zaobao/namespace.ts index 15e9a0e18f6f58..fde1ee1f4e29eb 100644 --- a/lib/routes/zaobao/namespace.ts +++ b/lib/routes/zaobao/namespace.ts @@ -3,7 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '联合早报', url: 'www.zaobao.com', - description: `:::warning + description: `::: warning 由于 [RSSHub#10309](https://github.com/DIYgod/RSSHub/issues/10309) 中的问题,使用靠近香港的服务器部署将从 hk 版联合早报爬取内容,造成输出的新闻段落顺序错乱。如有订阅此源的需求,建议寻求部署在远离香港的服务器上的 RSSHub,或者在自建时选择远离香港的服务器。 :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/zaobao/index.ts b/lib/routes/zaobao/other.ts similarity index 82% rename from lib/routes/zaobao/index.ts rename to lib/routes/zaobao/other.ts index e7a7bc6d6c4fab..5bd482d43b686a 100644 --- a/lib/routes/zaobao/index.ts +++ b/lib/routes/zaobao/other.ts @@ -3,18 +3,10 @@ import { parseList } from './util'; const baseUrl = 'https://www.zaobao.com'; export const route: Route = { - path: '/:type?/:section?', + path: '/other/:type?/:section?', categories: ['traditional-media'], - example: '/zaobao/lifestyle/health', + example: '/zaobao/other/lifestyle/health', parameters: { type: 'https://www.zaobao.com/**lifestyle**/health 中的 **lifestyle**', section: 'https://www.zaobao.com/lifestyle/**health** 中的 **health**' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, name: '其他栏目', maintainers: ['shunf4'], handler, diff --git a/lib/routes/zaobao/realtime.ts b/lib/routes/zaobao/realtime.ts index 2402f4087d252e..9c3761ed5d8320 100644 --- a/lib/routes/zaobao/realtime.ts +++ b/lib/routes/zaobao/realtime.ts @@ -7,20 +7,12 @@ export const route: Route = { categories: ['traditional-media'], example: '/zaobao/realtime/china', parameters: { section: '分类,缺省为 china' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, name: '即时新闻', maintainers: ['shunf4'], handler, description: `| 中国 | 新加坡 | 国际 | 财经 | - | ----- | --------- | ----- | -------- | - | china | singapore | world | zfinance |`, +| ----- | --------- | ----- | -------- | +| china | singapore | world | zfinance |`, }; async function handler(ctx) { diff --git a/lib/routes/zaobao/util.ts b/lib/routes/zaobao/util.ts index 8bd5a2a40d9855..bbb0fb653882ce 100644 --- a/lib/routes/zaobao/util.ts +++ b/lib/routes/zaobao/util.ts @@ -22,19 +22,23 @@ const got_ins = got.extend({ * * @param {*} ctx RSSHub 的 ctx 参数,用来设置缓存 * @param {string} sectionUrl 形如 /realtime/china 的字符串 - * @returns {Promise<{ - * title: string; - * resultList: { - * title: string; - * description: string; - * pubDate: string; - * link: string; - * }[];}>} 新闻标题以及新闻列表 + * @returns 新闻标题以及新闻列表 */ -const parseList = async (ctx, sectionUrl) => { +const parseList = async ( + ctx, + sectionUrl: string +): Promise<{ + title: string; + resultList: { + title: string; + description: string; + pubDate: string; + link: string; + }[]; +}> => { const response = await got_ins.get(baseUrl + sectionUrl); const $ = load(response.data); - let data = $('.article-list').find('.article-type'); + let data = $('.card-listing .card'); if (data.length === 0) { // for HK version data = $('.clearfix').find('.list-block'); @@ -82,16 +86,31 @@ const parseList = async (ctx, sectionUrl) => { const article = await got_ins.get(link); const $1 = load(article.data); - const time = (() => - $1("head script[type='application/json']").text() === '' - ? new Date(JSON.parse($1("head script[type='application/ld+json']").eq(1).text())?.datePublished) // HK - : new Date(Number(JSON.parse($1("head script[type='application/json']").text())?.articleDetails?.created) * 1000))(); // SG + let title, time; + if ($1('#seo-article-page').text() === '') { + // HK + title = $1('h1.article-title').text(); + const jsonText = $1("head script[type='application/ld+json']") + .eq(1) + .text() + .replaceAll(/[\u0000-\u001F\u007F-\u009F]/g, ''); + time = new Date(JSON.parse(jsonText)?.datePublished); + } else { + // SG + const jsonText = $1('#seo-article-page') + .text() + .replaceAll(/[\u0000-\u001F\u007F-\u009F]/g, ''); + const json = JSON.parse(jsonText); + title = json['@graph'][0]?.headline; + time = new Date(json['@graph'][0]?.datePublished); + } $1('.overlay-microtransaction').remove(); $1('#video-freemium-player').remove(); $1('script').remove(); + $1('.bff-google-ad').remove(); - let articleBodyNode = $1('.article-content-rawhtml'); + let articleBodyNode = $1('.articleBody'); if (articleBodyNode.length === 0) { // for HK version orderContent($1('.article-body')); @@ -100,52 +119,11 @@ const parseList = async (ctx, sectionUrl) => { const articleBody = articleBodyNode.html(); - const imageDataArray = []; - if ($1('.inline-figure-img').length) { - // for SG version - imageDataArray.push({ - type: 'normalHTML', - html: $1('.inline-figure-img') - .html() - .replace(/\/\/.*\.com\/s3fs-public/, '//static.zaobao.com/s3fs-public') - .replace(/s3\/files/, 's3fs-public'), - }); - } - if ($1('.body-content .loadme picture img').length) { - // Unused? - imageDataArray.push({ - type: 'data', - src: $1('.body-content .loadme picture source') - .attr('data-srcset') - .replace(/\/\/.*\.com\/s3fs-public/, '//static.zaobao.com/s3fs-public') - .replace(/s3\/files/, 's3fs-public'), - title: $1('.body-content .loadme picture img').attr('title'), - }); - } - if ($1('.inline-figure-gallery').length) { - // for SG version - imageDataArray.push({ - type: 'normalHTML', - html: $1('.inline-figure-gallery') - .html() - .replaceAll(/\/\/.*\.com\/s3fs-public/g, '//static.zaobao.com/s3fs-public') - .replaceAll('s3/files', 's3fs-public'), - }); - } - if ($1('#carousel-article').length) { - // for HK version, HK version of multi images use same selector as single image, so g is needed for all pages - imageDataArray.push({ - type: 'normalHTML', - html: $1('#carousel-article .carousel-inner') - .html() - .replaceAll(/\/\/.*\.com\/s3fs-public/g, '//static.zaobao.com/s3fs-public') - .replaceAll('s3/files', 's3fs-public'), - }); - } + const imageDataArray = processImageData($1); return { - // <- for SG version -> for HK version - title: $1('h1', '.content').text().trim() || $1('h1.article-title').text(), + // <- for HK version -> for SG version + title, description: art(path.join(__dirname, 'templates/zaobao.art'), { articleBody, imageDataArray, @@ -178,4 +156,43 @@ const orderContent = (parent) => { } }; +interface ImageData { + type: string; + html: string; + src?: string; + title?: string; +} + +const processImageData = ($1) => { + const imageDataArray: ImageData[] = []; + + const imageSelectors = [ + '.inline-figure-img', // for SG version + '.body-content .loadme picture img', // Unused? + '.inline-figure-gallery', // for SG version + '#carousel-article', // for HK version, HK version of multi images use same selector as single image, so g is needed for all pages + ]; + + for (const selector of imageSelectors) { + if ($1(selector).length) { + let html = $1(selector === '#carousel-article' ? '#carousel-article .carousel-inner' : selector).html(); + + if (html) { + html = html.replaceAll(/\/\/.*\.com\/s3fs-public/g, '//static.zaobao.com/s3fs-public').replaceAll('s3/files', 's3fs-public'); + + imageDataArray.push({ + type: selector === '.body-content .loadme picture img' ? 'data' : 'normalHTML', + html, + ...(selector === '.body-content .loadme picture img' && { + src: $1('.body-content .loadme picture source').attr('data-srcset'), + title: $1(selector).attr('title'), + }), + }); + } + } + } + + return imageDataArray; +}; + export { parseList, orderContent }; diff --git a/lib/routes/zaobao/znews.ts b/lib/routes/zaobao/znews.ts index 71492e4a701515..7aa4daa09c9cc7 100644 --- a/lib/routes/zaobao/znews.ts +++ b/lib/routes/zaobao/znews.ts @@ -19,8 +19,8 @@ export const route: Route = { maintainers: ['shunf4'], handler, description: `| 中国 | 新加坡 | 东南亚 | 国际 | 体育 | - | ----- | --------- | ------ | ----- | ------ | - | china | singapore | sea | world | sports |`, +| ----- | --------- | ------ | ----- | ------ | +| china | singapore | sea | world | sports |`, }; async function handler(ctx) { diff --git a/lib/routes/zaozao/article.ts b/lib/routes/zaozao/article.ts index dd778ed2336514..2d7458354bbb95 100644 --- a/lib/routes/zaozao/article.ts +++ b/lib/routes/zaozao/article.ts @@ -25,8 +25,8 @@ export const route: Route = { maintainers: ['shaomingbo'], handler, description: `| 精品推荐 | 技术干货 | 职场成长 | 社区动态 | 组件物料 | 行业动态 | - | --------- | -------- | -------- | --------- | -------- | -------- | - | recommend | quality | growth | community | material | industry |`, +| --------- | -------- | -------- | --------- | -------- | -------- | +| recommend | quality | growth | community | material | industry |`, }; async function handler(ctx) { diff --git a/lib/routes/zaozao/namespace.ts b/lib/routes/zaozao/namespace.ts index e48ed3bbca6a97..de8cf9845b8899 100644 --- a/lib/routes/zaozao/namespace.ts +++ b/lib/routes/zaozao/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '前端早早聊', url: 'www.zaozao.run', + lang: 'zh-CN', }; diff --git a/lib/routes/zcmu/jwc/index.ts b/lib/routes/zcmu/jwc/index.ts index 160b28d35b197c..3c15935b9e0afd 100644 --- a/lib/routes/zcmu/jwc/index.ts +++ b/lib/routes/zcmu/jwc/index.ts @@ -31,8 +31,8 @@ export const route: Route = { maintainers: ['CCraftY'], handler, description: `| 教务管理 | 成绩管理 | 学籍管理 | 考试管理 | 选课管理 | 排课管理 | - | -------- | -------- | -------- | -------- | -------- | -------- | - | 0 | 1 | 2 | 3 | 4 | 5 |`, +| -------- | -------- | -------- | -------- | -------- | -------- | +| 0 | 1 | 2 | 3 | 4 | 5 |`, }; async function handler(ctx) { diff --git a/lib/routes/zcmu/namespace.ts b/lib/routes/zcmu/namespace.ts index c6227eb73436b6..af1ee8ce0dbe75 100644 --- a/lib/routes/zcmu/namespace.ts +++ b/lib/routes/zcmu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '浙江中医药大学', url: 'jwc.zcmu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/zcmu/yxy/index.ts b/lib/routes/zcmu/yxy/index.ts index dee075d844191e..502649dbdad974 100644 --- a/lib/routes/zcmu/yxy/index.ts +++ b/lib/routes/zcmu/yxy/index.ts @@ -32,8 +32,8 @@ export const route: Route = { maintainers: ['CCraftY'], handler, description: `| 通知公告 | 评优评奖 | 文明规范 | 创新创业 | 校园文化 | 心理驿站 | 日常通知 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | - | 0 | 1 | 2 | 3 | 4 | 5 | 6 |`, +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| 0 | 1 | 2 | 3 | 4 | 5 | 6 |`, }; async function handler(ctx) { diff --git a/lib/routes/zcool/discover.ts b/lib/routes/zcool/discover.ts index f3d9cda27f9867..6e4c653a306c6e 100644 --- a/lib/routes/zcool/discover.ts +++ b/lib/routes/zcool/discover.ts @@ -42,113 +42,113 @@ export const route: Route = { 在 **精选** 分类下的 **运营设计** 子分类全部内容基础上,筛选出有 **视频**,且城市选择 **北京**,可直接使用路由 [\`/zcool/discover/0/617/1/北京\`](https://rsshub.app/zcool/discover/0/617/1/北京) - :::tip +::: tip 下方仅提供 **分类及其子分类** 参数的代码。**学校** 参数的代码可以在 [站酷发现页](https://www.zcool.com.cn/discover) 中选择跳转后,从浏览器地址栏中找到。 - ::: +::: 分类 cate - | 精选 | 平面 | 插画 | UI | 网页 | 摄影 | 三维 | 影视 | 空间 | 工业 / 产品 | 动漫 | 纯艺术 | 手工艺 | 服装 | 其他 | - | ---- | ---- | ---- | -- | ---- | ---- | ---- | ---- | ---- | ----------- | ---- | ------ | ------ | ---- | ---- | - | 0 | 8 | 1 | 17 | 607 | 33 | 24 | 610 | 609 | 499 | 608 | 612 | 611 | 613 | 44 | +| 精选 | 平面 | 插画 | UI | 网页 | 摄影 | 三维 | 影视 | 空间 | 工业 / 产品 | 动漫 | 纯艺术 | 手工艺 | 服装 | 其他 | +| ---- | ---- | ---- | -- | ---- | ---- | ---- | ---- | ---- | ----------- | ---- | ------ | ------ | ---- | ---- | +| 0 | 8 | 1 | 17 | 607 | 33 | 24 | 610 | 609 | 499 | 608 | 612 | 611 | 613 | 44 | 子分类 subCate 精选 0 - | 运营设计 | 包装 | 动画 / 影视 | 人像摄影 | 商业插画 | 电商 | APP 界面 | 艺术插画 | 家装设计 | 海报 | 文章 | - | -------- | ---- | ----------- | -------- | -------- | ---- | -------- | -------- | -------- | ---- | ------ | - | 617 | 9 | 30 | 34 | 2 | 616 | 757 | 292 | 637 | 10 | 809824 | +| 运营设计 | 包装 | 动画 / 影视 | 人像摄影 | 商业插画 | 电商 | APP 界面 | 艺术插画 | 家装设计 | 海报 | 文章 | +| -------- | ---- | ----------- | -------- | -------- | ---- | -------- | -------- | -------- | ---- | ------ | +| 617 | 9 | 30 | 34 | 2 | 616 | 757 | 292 | 637 | 10 | 809824 | 平面 8 - | 包装 | 海报 | 品牌 | IP 形象 | 字体 / 字形 | Logo | 书籍 / 画册 | 宣传物料 | 图案 | 信息图表 | PPT/Keynote | 其他平面 | 文章 | - | ---- | ---- | ---- | ------- | ----------- | ---- | ----------- | -------- | ---- | -------- | ----------- | -------- | ---- | - | 9 | 10 | 15 | 779 | 14 | 13 | 12 | 534 | 624 | 625 | 626 | 11 | 809 | +| 包装 | 海报 | 品牌 | IP 形象 | 字体 / 字形 | Logo | 书籍 / 画册 | 宣传物料 | 图案 | 信息图表 | PPT/Keynote | 其他平面 | 文章 | +| ---- | ---- | ---- | ------- | ----------- | ---- | ----------- | -------- | ---- | -------- | ----------- | -------- | ---- | +| 9 | 10 | 15 | 779 | 14 | 13 | 12 | 534 | 624 | 625 | 626 | 11 | 809 | 插画 1 - | 商业插画 | 概念设定 | 游戏原画 | 绘本 | 儿童插画 | 艺术插画 | 创作习作 | 新锐潮流插画 | 像素画 | 文章 | - | -------- | -------- | -------- | ---- | -------- | -------- | -------- | ------------ | ------ | ---- | - | 2 | 5 | 685 | 631 | 684 | 292 | 7 | 3 | 4 | 819 | +| 商业插画 | 概念设定 | 游戏原画 | 绘本 | 儿童插画 | 艺术插画 | 创作习作 | 新锐潮流插画 | 像素画 | 文章 | +| -------- | -------- | -------- | ---- | -------- | -------- | -------- | ------------ | ------ | ---- | +| 2 | 5 | 685 | 631 | 684 | 292 | 7 | 3 | 4 | 819 | UI 17 - | APP 界面 | 游戏 UI | 软件界面 | 图标 | 主题 / 皮肤 | 交互 / UE | 动效设计 | 闪屏 / 壁纸 | 其他 UI | 文章 | - | -------- | ------- | -------- | ---- | ----------- | --------- | -------- | ----------- | ------- | ---- | - | 757 | 692 | 621 | 20 | 19 | 623 | 797 | 21 | 23 | 822 | +| APP 界面 | 游戏 UI | 软件界面 | 图标 | 主题 / 皮肤 | 交互 / UE | 动效设计 | 闪屏 / 壁纸 | 其他 UI | 文章 | +| -------- | ------- | -------- | ---- | ----------- | --------- | -------- | ----------- | ------- | ---- | +| 757 | 692 | 621 | 20 | 19 | 623 | 797 | 21 | 23 | 822 | 网页 607 - | 电商 | 企业官网 | 游戏 / 娱乐 | 运营设计 | 移动端网页 | 门户网站 | 个人网站 | 其他网页 | 文章 | - | ---- | -------- | ----------- | -------- | ---------- | -------- | -------- | -------- | ---- | - | 616 | 614 | 693 | 617 | 777 | 615 | 618 | 620 | 823 | +| 电商 | 企业官网 | 游戏 / 娱乐 | 运营设计 | 移动端网页 | 门户网站 | 个人网站 | 其他网页 | 文章 | +| ---- | -------- | ----------- | -------- | ---------- | -------- | -------- | -------- | ---- | +| 616 | 614 | 693 | 617 | 777 | 615 | 618 | 620 | 823 | 摄影 33 - | 人像摄影 | 风光摄影 | 人文 / 纪实摄影 | 美食摄影 | 产品摄影 | 环境 / 建筑摄影 | 时尚 / 艺术摄影 | 修图 / 后期 | 宠物摄影 | 婚礼摄影 | 其他摄影 | 文章 | - | -------- | -------- | --------------- | -------- | -------- | --------------- | --------------- | ----------- | -------- | -------- | -------- | ---- | - | 34 | 35 | 36 | 825 | 686 | 38 | 800 | 687 | 40 | 808 | 43 | 810 | +| 人像摄影 | 风光摄影 | 人文 / 纪实摄影 | 美食摄影 | 产品摄影 | 环境 / 建筑摄影 | 时尚 / 艺术摄影 | 修图 / 后期 | 宠物摄影 | 婚礼摄影 | 其他摄影 | 文章 | +| -------- | -------- | --------------- | -------- | -------- | --------------- | --------------- | ----------- | -------- | -------- | -------- | ---- | +| 34 | 35 | 36 | 825 | 686 | 38 | 800 | 687 | 40 | 808 | 43 | 810 | 三维 24 - | 动画 / 影视 | 机械 / 交通 | 人物 / 生物 | 产品 | 场景 | 建筑 / 空间 | 其他三维 | 文章 | - | ----------- | ----------- | ----------- | ---- | ---- | ----------- | -------- | ---- | - | 30 | 25 | 27 | 807 | 26 | 29 | 32 | 818 | +| 动画 / 影视 | 机械 / 交通 | 人物 / 生物 | 产品 | 场景 | 建筑 / 空间 | 其他三维 | 文章 | +| ----------- | ----------- | ----------- | ---- | ---- | ----------- | -------- | ---- | +| 30 | 25 | 27 | 807 | 26 | 29 | 32 | 818 | 影视 610 - | 短片 | Motion Graphic | 宣传片 | 影视后期 | 栏目片头 | MV | 设定 / 分镜 | 其他影视 | 文章 | - | ---- | -------------- | ------ | -------- | -------- | --- | ----------- | -------- | ---- | - | 645 | 649 | 804 | 646 | 647 | 644 | 650 | 651 | 817 | +| 短片 | Motion Graphic | 宣传片 | 影视后期 | 栏目片头 | MV | 设定 / 分镜 | 其他影视 | 文章 | +| ---- | -------------- | ------ | -------- | -------- | --- | ----------- | -------- | ---- | +| 645 | 649 | 804 | 646 | 647 | 644 | 650 | 651 | 817 | 空间 609 - | 家装设计 | 酒店餐饮设计 | 商业空间设计 | 建筑设计 | 舞台美术 | 展陈设计 | 景观设计 | 其他空间 | 文章 | - | -------- | ------------ | ------------ | -------- | -------- | -------- | -------- | -------- | ---- | - | 637 | 811 | 641 | 636 | 638 | 639 | 640 | 642 | 812 | +| 家装设计 | 酒店餐饮设计 | 商业空间设计 | 建筑设计 | 舞台美术 | 展陈设计 | 景观设计 | 其他空间 | 文章 | +| -------- | ------------ | ------------ | -------- | -------- | -------- | -------- | -------- | ---- | +| 637 | 811 | 641 | 636 | 638 | 639 | 640 | 642 | 812 | 工业 / 产品 499 - | 生活用品 | 电子产品 | 交通工具 | 工业用品 / 机械 | 人机交互 | 玩具 | 其他工业 / 产品 | 文章 | - | -------- | -------- | -------- | --------------- | -------- | ---- | --------------- | ---- | - | 508 | 506 | 509 | 511 | 510 | 689 | 514 | 813 | +| 生活用品 | 电子产品 | 交通工具 | 工业用品 / 机械 | 人机交互 | 玩具 | 其他工业 / 产品 | 文章 | +| -------- | -------- | -------- | --------------- | -------- | ---- | --------------- | ---- | +| 508 | 506 | 509 | 511 | 510 | 689 | 514 | 813 | 动漫 608 - | 短篇 / 格漫 | 中 / 长篇漫画 | 网络表情 | 单幅漫画 | 动画片 | 其他动漫 | 文章 | - | ----------- | ------------- | -------- | -------- | ------ | -------- | ---- | - | 628 | 629 | 632 | 627 | 633 | 635 | 820 | +| 短篇 / 格漫 | 中 / 长篇漫画 | 网络表情 | 单幅漫画 | 动画片 | 其他动漫 | 文章 | +| ----------- | ------------- | -------- | -------- | ------ | -------- | ---- | +| 628 | 629 | 632 | 627 | 633 | 635 | 820 | 纯艺术 612 - | 绘画 | 雕塑 | 书法 | 实验艺术 | 文章 | - | ---- | ---- | ---- | -------- | ---- | - | 659 | 662 | 668 | 657 | 821 | +| 绘画 | 雕塑 | 书法 | 实验艺术 | 文章 | +| ---- | ---- | ---- | -------- | ---- | +| 659 | 662 | 668 | 657 | 821 | 手工艺 611 - | 工艺品设计 | 手办 / 模玩 | 首饰设计 | 其他手工艺 | 文章 | - | ---------- | ----------- | -------- | ---------- | ---- | - | 654 | 656 | 756 | 658 | 816 | +| 工艺品设计 | 手办 / 模玩 | 首饰设计 | 其他手工艺 | 文章 | +| ---------- | ----------- | -------- | ---------- | ---- | +| 654 | 656 | 756 | 658 | 816 | 服装 613 - | 休闲 / 流行服饰 | 正装 / 礼服 | 传统 / 民族服饰 | 配饰 | 鞋履设计 | 儿童服饰 | 其他服装 | 文章 | - | --------------- | ----------- | --------------- | ---- | -------- | -------- | -------- | ---- | - | 672 | 671 | 814 | 677 | 676 | 673 | 680 | 815 | +| 休闲 / 流行服饰 | 正装 / 礼服 | 传统 / 民族服饰 | 配饰 | 鞋履设计 | 儿童服饰 | 其他服装 | 文章 | +| --------------- | ----------- | --------------- | ---- | -------- | -------- | -------- | ---- | +| 672 | 671 | 814 | 677 | 676 | 673 | 680 | 815 | 其他 44 - | 文案 / 策划 | VR 设计 | 独立游戏 | 其他 | 文章 | - | ----------- | ------- | -------- | ---- | ---- | - | 417 | 798 | 683 | 45 | 824 | +| 文案 / 策划 | VR 设计 | 独立游戏 | 其他 | 文章 | +| ----------- | ------- | -------- | ---- | ---- | +| 417 | 798 | 683 | 45 | 824 | 推荐等级 recommendLevel - | 全部 | 编辑精选 | 首页推荐 | 全部推荐 | - | ---- | -------- | -------- | -------- | - | 0 | 2 | 3 | 1 |`, +| 全部 | 编辑精选 | 首页推荐 | 全部推荐 | +| ---- | -------- | -------- | -------- | +| 0 | 2 | 3 | 1 |`, }; async function handler(ctx) { @@ -219,7 +219,8 @@ async function handler(ctx) { queries.subCate = '809824'; queries.recommendLevel = '2'; break; - case 'editor' || 'edit': + case 'editor': + case 'edit': queries.recommendLevel = '2'; break; default: diff --git a/lib/routes/zcool/namespace.ts b/lib/routes/zcool/namespace.ts index 8a9dc33167fc53..9b34045666b2c9 100644 --- a/lib/routes/zcool/namespace.ts +++ b/lib/routes/zcool/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '站酷', url: 'www.zcool.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/zcool/top.ts b/lib/routes/zcool/top.ts index 7e619e690015c4..f3647d67af408f 100644 --- a/lib/routes/zcool/top.ts +++ b/lib/routes/zcool/top.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'; @@ -8,9 +8,18 @@ const baseUrl = 'https://www.zcool.com.cn'; export const route: Route = { path: '/top/:type', - categories: ['design'], + categories: ['design', 'popular'], + view: ViewType.Pictures, example: '/zcool/top/design', - parameters: { type: '推荐类型,详见下面的表格' }, + parameters: { + type: { + description: '推荐类型', + options: [ + { value: 'design', label: '作品榜单' }, + { value: 'article', label: '文章榜单' }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -22,11 +31,6 @@ export const route: Route = { name: '作品总榜单', maintainers: ['yuuow'], handler, - description: `榜单类型 - - | design | article | - | -------- | -------- | - | 作品榜单 | 文章榜单 |`, }; async function handler(ctx) { diff --git a/lib/routes/zcool/user.ts b/lib/routes/zcool/user.ts index beac412846ad91..2b5cc4dd549e6d 100644 --- a/lib/routes/zcool/user.ts +++ b/lib/routes/zcool/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'; @@ -9,7 +9,8 @@ import InvalidParameterError from '@/errors/types/invalid-parameter'; export const route: Route = { path: '/user/:uid', - categories: ['design'], + categories: ['design', 'popular'], + view: ViewType.Pictures, example: '/zcool/user/baiyong', parameters: { uid: '个性域名前缀或者用户ID' }, features: { diff --git a/lib/routes/zhibo8/namespace.ts b/lib/routes/zhibo8/namespace.ts index b7e6b35661057d..922250269c40a6 100644 --- a/lib/routes/zhibo8/namespace.ts +++ b/lib/routes/zhibo8/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '直播吧', url: 'zhibo8.cc', + lang: 'zh-CN', }; diff --git a/lib/routes/zhihu/activities.ts b/lib/routes/zhihu/activities.ts index 8e760bd3c4fb86..62b45b3644eabb 100644 --- a/lib/routes/zhihu/activities.ts +++ b/lib/routes/zhihu/activities.ts @@ -1,15 +1,13 @@ -import { Route } from '@/types'; -import cache from '@/utils/cache'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; -import ofetch from '@/utils/ofetch'; -import utils from './utils'; +import { header, processImage, getSignedHeader } from './utils'; import { parseDate } from '@/utils/parse-date'; -import g_encrypt from './execlib/x-zse-96-v3'; -import md5 from '@/utils/md5'; +import sanitizeHtml from 'sanitize-html'; export const route: Route = { path: '/people/activities/:id', - categories: ['social-media'], + categories: ['social-media', 'popular'], + view: ViewType.Articles, example: '/zhihu/people/activities/diygod', parameters: { id: '作者 id,可在用户主页 URL 中找到' }, features: { @@ -33,43 +31,17 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - // Because the API of zhihu.com has changed, we must use the value of `d_c0` (extracted from cookies) to calculate - // `x-zse-96`. So first get `d_c0`, then get the actual data of a ZhiHu question. In this way, we don't need to - // require users to set the cookie in environmental variables anymore. - - // fisrt: get cookie(dc_0) from zhihu.com - const cookie_mes = await cache.tryGet('zhihu:cookies:d_c0', async () => { - const response = await ofetch.raw(`https://www.zhihu.com/people/${id}`, { - headers: { - ...utils.header, - }, - }); - - const cookie_org = response.headers.get('set-cookie'); - const cookie = cookie_org?.toString(); - const match = cookie?.match(/d_c0=(\S+?)(?:;|$)/); - const cookie_mes = match && match[1]; - if (!cookie_mes) { - throw new Error('Failed to extract `d_c0` from cookies'); - } - return cookie_mes; - }); - const cookie = `d_c0=${cookie_mes}`; - // second: get real data from zhihu const apiPath = `/api/v3/moments/${id}/activities?limit=7&desktop=true`; - // calculate x-zse-96, refer to https://github.com/srx-2000/spider_collection/issues/18 - const f = `101_3_3.0+${apiPath}+${cookie_mes}`; - const xzse96 = '2.0_' + g_encrypt(md5(f)); - const _header = { cookie, 'x-zse-96': xzse96, 'x-app-za': 'OS=Web', 'x-zse-93': '101_3_3.0' }; + const signedHeader = await getSignedHeader(`https://www.zhihu.com/people/${id}`, apiPath); const response = await got(`https://www.zhihu.com${apiPath}`, { headers: { - ...utils.header, - ..._header, + ...header, + ...signedHeader, Referer: `https://www.zhihu.com/people/${id}/activities`, - Authorization: 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20', // hard-coded in js + // Authorization: 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20', // hard-coded in js }, }); @@ -85,7 +57,7 @@ async function handler(ctx) { let title; let description; let url; - const images = []; + const images: string[] = []; let text = ''; let link = ''; let author = ''; @@ -94,17 +66,17 @@ async function handler(ctx) { case 'answer': title = detail.question.title; author = detail.author.name; - description = utils.ProcessImage(detail.content); + description = processImage(detail.content); url = `https://www.zhihu.com/question/${detail.question.id}/answer/${detail.id}`; break; case 'article': title = detail.title; author = detail.author.name; - description = utils.ProcessImage(detail.content); + description = processImage(detail.content); url = `https://zhuanlan.zhihu.com/p/${detail.id}`; break; case 'pin': - title = detail.excerpt_title; + title = sanitizeHtml(detail.excerpt_title); author = detail.author.name; for (const contentItem of detail.content) { switch (contentItem.type) { @@ -142,7 +114,7 @@ async function handler(ctx) { case 'question': title = detail.title; author = detail.author.name; - description = utils.ProcessImage(detail.detail); + description = processImage(detail.detail); url = `https://www.zhihu.com/question/${detail.id}`; break; case 'collection': @@ -169,6 +141,8 @@ async function handler(ctx) { description = detail.description; url = `https://www.zhihu.com/roundtable/${detail.id}`; break; + default: + description = `未知类型 ${item.target.type},请点击<a href="https://github.com/DIYgod/RSSHub/issues">链接</a>提交issue`; } return { diff --git a/lib/routes/zhihu/all-collections.ts b/lib/routes/zhihu/all-collections.ts new file mode 100644 index 00000000000000..31d0e0c3859206 --- /dev/null +++ b/lib/routes/zhihu/all-collections.ts @@ -0,0 +1,109 @@ +import { Route, ViewType, Collection, CollectionItem } from '@/types'; +import got from '@/utils/got'; +import { header } from './utils'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/people/allCollections/:id', + categories: ['social-media'], + view: ViewType.Articles, + example: '/zhihu/people/allCollections/87-44-49-67', + parameters: { id: '作者 id,可在用户主页 URL 中找到' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.zhihu.com/people/:id'], + // target: 'people/allCollections/:id', + }, + ], + name: '用户全部收藏内容', + maintainers: ['Healthyyue'], + handler, +}; + +async function handler(ctx) { + const id = ctx.req.param('id'); + const apiPath = `https://api.zhihu.com/people/${id}/collections`; + + const response = await got(apiPath, { + headers: { + Referer: `https://www.zhihu.com/people/${id}/collections`, + }, + }); + + const collections = response.data.data as Collection[]; + + const allCollectionItems = await Promise.all( + collections.map(async (collection) => { + const firstPageResponse = await got(`https://www.zhihu.com/api/v4/collections/${collection.id}/items?offset=0&limit=20`, { + headers: { + ...header, + Referer: `https://www.zhihu.com/collection/${collection.id}`, + }, + }); + + const { + data: items, + paging: { totals }, + } = firstPageResponse.data; + + if (totals > 20) { + const offsetList = Array.from({ length: Math.ceil(totals / 20) - 1 }, (_, index) => (index + 1) * 20); + + const otherPages = await Promise.all( + offsetList.map((offset) => + cache.tryGet(`https://www.zhihu.com/api/v4/collections/${collection.id}/items?offset=${offset}&limit=20`, async () => { + const response = await got(`https://www.zhihu.com/api/v4/collections/${collection.id}/items?offset=${offset}&limit=20`, { + headers: { + ...header, + Referer: `https://www.zhihu.com/collection/${collection.id}`, + }, + }); + return response.data.data; + }) + ) + ); + + items.push(...otherPages.flat()); + } + + return { + collectionId: collection.id, + collectionTitle: collection.title, + items, + }; + }) + ); + + const items = allCollectionItems.flatMap( + (collection) => + collection.items.map((item) => ({ + ...item, + collectionTitle: collection.collectionTitle, + })) as CollectionItem[] + ); + + return { + title: `${collections[0].creator.name}的知乎收藏`, + link: `https://www.zhihu.com/people/${id}/collections`, + item: items.map((item) => { + const content = item.content; + + return { + title: content.type === 'article' || content.type === 'zvideo' ? content.title : content.question.title, + link: content.url, + description: content.type === 'zvideo' ? `<img src=${content.video.url}/>` : content.content, + pubDate: parseDate((content.type === 'article' ? content.updated : content.updated_time) * 1000), + category: [item.collectionTitle], + }; + }), + }; +} diff --git a/lib/routes/zhihu/answers.ts b/lib/routes/zhihu/answers.ts index 6dd7f44b4d93bd..2ec0f9de35a4bb 100644 --- a/lib/routes/zhihu/answers.ts +++ b/lib/routes/zhihu/answers.ts @@ -1,7 +1,6 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; import got from '@/utils/got'; -import utils from './utils'; +import { header, getSignedHeader } from './utils'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { @@ -29,63 +28,40 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const headers = { - 'User-Agent': 'ZhihuHybrid com.zhihu.android/Futureve/6.59.0 Mozilla/5.0 (Linux; Android 10; GM1900 Build/QKQ1.190716.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/85.0.4183.127 Mobile Safari/537.36', - Referer: `https://www.zhihu.com/people/${id}/answers`, - }; - const response = await got({ - method: 'get', - url: `https://api.zhihu.com/people/${id}/answers?order_by=created&offset=0&limit=10`, - headers, - }); + // second: get real data from zhihu + const apiPath = `/api/v4/members/${id}/answers?limit=7&include=data[*].is_normal,content`; - const data = response.data.data; - let name = data[0].author.name; + const signedHeader = await getSignedHeader(`https://www.zhihu.com/people/${id}`, apiPath); - if (name === '知乎用户') { - const userProfile = await cache.tryGet(`zhihu:profile:${id}`, async () => { - const apiPath = `/api/v4/members/${id}`; + const response = await got(`https://www.zhihu.com${apiPath}`, { + headers: { + ...header, + ...signedHeader, + Referer: `https://www.zhihu.com/people/${id}/activities`, + // Authorization: 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20', // hard-coded in js + }, + }); - const { data } = await got({ - method: 'get', - url: `https://www.zhihu.com${apiPath}`, - headers: { - ...utils.header, - Referer: `https://www.zhihu.com/people/${id}`, - }, - }); - return data; - }); - name = userProfile.name; - } + const data = response.data.data; + const items = data.map((item) => { + const title = item.question.title; + // let description = processImage(detail.content); + const url = `https://www.zhihu.com/question/${item.question.id}/answer/${item.id}`; + const author = item.author.name; + const description = item.content; - const items = await Promise.all( - data.map(async (item) => { - let description; - const link = `https://www.zhihu.com/question/${item.question.id}/answer/${item.id}`; - const title = item.question.title; - try { - const detail = await got({ - method: 'get', - url: `https://api.zhihu.com/appview/api/v4/answers/${item.id}?include=content&is_appview=true`, - headers, - }); - description = utils.ProcessImage(detail.data.content); - } catch { - description = `<a href="${link}" target="_blank">${title}</a>`; - } - return { - title, - description, - pubDate: parseDate(item.created_time * 1000), - link, - }; - }) - ); + return { + title, + author, + description, + pubDate: parseDate(item.created_time * 1000), + link: url, + }; + }); return { - title: `${name}的知乎回答`, + title: `${data[0].author.name}的知乎回答`, link: `https://www.zhihu.com/people/${id}/answers`, item: items, }; diff --git a/lib/routes/zhihu/collection.ts b/lib/routes/zhihu/collection.ts index 1844031e141e01..43489c3ba555af 100644 --- a/lib/routes/zhihu/collection.ts +++ b/lib/routes/zhihu/collection.ts @@ -2,7 +2,7 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; -import utils from './utils'; +import { header } from './utils'; import { generateData } from './pin/utils'; import { parseDate } from '@/utils/parse-date'; @@ -38,7 +38,7 @@ async function handler(ctx) { method: 'get', url: `https://www.zhihu.com/api/v4/collections/${id}/items?offset=0&limit=20`, headers: { - ...utils.header, + ...header, Referer: `https://www.zhihu.com/collection/${id}`, }, }); @@ -56,7 +56,7 @@ async function handler(ctx) { method: 'get', url: `https://www.zhihu.com/api/v4/collections/${id}/items?offset=${offset}&limit=20`, headers: { - ...utils.header, + ...header, Referer: `https://www.zhihu.com/collection/${id}`, }, }); @@ -72,7 +72,7 @@ async function handler(ctx) { method: 'get', url: `https://www.zhihu.com/collection/${id}`, headers: { - ...utils.header, + ...header, Referer: `https://www.zhihu.com/collection/${id}`, }, }); diff --git a/lib/routes/zhihu/daily-section.ts b/lib/routes/zhihu/daily-section.ts index d594717f71ab75..7ca1cf6ea91f68 100644 --- a/lib/routes/zhihu/daily-section.ts +++ b/lib/routes/zhihu/daily-section.ts @@ -1,7 +1,7 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; -import utils from './utils'; +import { header, processImage } from './utils'; import { parseDate } from '@/utils/parse-date'; // 参考:https://github.com/izzyleung/ZhihuDailyPurify/wiki/%E7%9F%A5%E4%B9%8E%E6%97%A5%E6%8A%A5-API-%E5%88%86%E6%9E%90 @@ -38,7 +38,7 @@ async function handler(ctx) { method: 'get', url: `https://news-at.zhihu.com/api/7/section/${sectionId}`, headers: { - ...utils.header, + ...header, Referer: `https://news-at.zhihu.com/api/7/section/${sectionId}`, }, }); @@ -61,7 +61,7 @@ async function handler(ctx) { Referer: url, }, }); - item.description = utils.ProcessImage(storyDetail.data.body.replaceAll(/<div class="meta">([\S\s]*?)<\/div>/g, '<strong>$1</strong>').replaceAll(/<\/?h2.*?>/g, '')); + item.description = processImage(storyDetail.data.body.replaceAll(/<div class="meta">([\S\s]*?)<\/div>/g, '<strong>$1</strong>').replaceAll(/<\/?h2.*?>/g, '')); return item; }); diff --git a/lib/routes/zhihu/daily.ts b/lib/routes/zhihu/daily.ts index 5125087ea8c19b..5cd788ed78b3ed 100644 --- a/lib/routes/zhihu/daily.ts +++ b/lib/routes/zhihu/daily.ts @@ -1,25 +1,7 @@ import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; import cache from '@/utils/cache'; -import got from '@/utils/got'; -import utils from './utils'; -import { parseDate } from '@/utils/parse-date'; - -// 参考:https://github.com/izzyleung/ZhihuDailyPurify/wiki/%E7%9F%A5%E4%B9%8E%E6%97%A5%E6%8A%A5-API-%E5%88%86%E6%9E%90 -// 文章给出了v4版 api的信息,包含全文api - -async function dohResolve(name) { - const response = await got('https://223.5.5.5/resolve', { - searchParams: { - name, - type: 'A', - edns_client_subnet: '223.5.5.5', // 使用国内的ip地址 - }, - headers: { - accept: 'application/dns-json', - }, - }); - return response.data.Answer.map((item) => item.data); -} export const route: Route = { path: '/daily', @@ -40,60 +22,44 @@ export const route: Route = { }, ], name: '知乎日报', - maintainers: ['DHPO'], + maintainers: ['DHPO', 'pseudoyu'], handler, url: 'daily.zhihu.com/*', }; async function handler() { - const api = 'https://news-at.zhihu.com/api/4/news'; - const HOST = 'news-at.zhihu.com'; - let address = HOST; - try { - await got(`${api}/latest`); - } catch { - address = (await dohResolve(HOST)).pop(); // 国外ip证书配置有误,解析到国内地址 - } - const listRes = await got({ - method: 'get', - url: `${api}/latest`, - headers: { - ...utils.header, - Referer: `${api}/latest`, - Host: address, - }, - // host: address, + const key = 'zhihu:daily'; + const response = await cache.tryGet(key, async () => { + const response = await ofetch('https://daily.zhihu.com/'); + return response; }); - // 根据api的说明,过滤掉极个别站外链接 - const storyList = listRes.data.stories.filter((el) => el.type === 0); - const date = listRes.data.date; - const articleList = storyList.map((item) => ({ - title: item.title, - pubDate: parseDate(date, 'YYYYMMDD'), - link: item.url, - guid: item.url, - storyId: item.id, - })); + const $ = load(response); const items = await Promise.all( - articleList.map(async (item) => { - const url = `${api}/${item.storyId}`; - const description = await cache.tryGet(item.link, async () => { - const storyDetail = await got({ - method: 'get', - url, - headers: { - Referer: url, - Host: address, - }, - // host: address, + $('.box') + .toArray() + .map(async (item) => { + item = $(item); + const linkElem = item.find('.link-button'); + const storyUrl = 'https://daily.zhihu.com' + linkElem.attr('href'); + + // Fetch full story content + const storyResponse = await cache.tryGet(storyUrl, async () => { + const response = await ofetch(storyUrl); + return response; }); - return utils.ProcessImage(storyDetail.data.body.replaceAll(/<div class="meta">([\S\s]*?)<\/div>/g, '<strong>$1</strong>').replaceAll(/<\/?h2.*?>/g, '')); - }); - item.description = description; - return item; - }) + + const $story = load(storyResponse); + const storyTitle = $story('.DailyHeader-title').text(); + const storyContent = $story('.DailyRichText').html(); + + return { + title: storyTitle, + description: storyContent, + link: storyUrl, + }; + }) ); return { diff --git a/lib/routes/zhihu/execlib/x-zse-96-v3.ts b/lib/routes/zhihu/execlib/x-zse-96-v3.ts index 42c5cc57826820..bdaf9304dfc8e2 100644 --- a/lib/routes/zhihu/execlib/x-zse-96-v3.ts +++ b/lib/routes/zhihu/execlib/x-zse-96-v3.ts @@ -2,6 +2,8 @@ // https://blog.csdn.net/zjq592767809/article/details/126512798 // https://blog.csdn.net/zhoumi_/article/details/126659351 +/* eslint-disable @typescript-eslint/no-unused-expressions */ + function i(e, t, n) { (t[n] = 255 & (e >>> 24)), (t[n + 1] = 255 & (e >>> 16)), (t[n + 2] = 255 & (e >>> 8)), (t[n + 3] = 255 & e); } diff --git a/lib/routes/zhihu/hot.ts b/lib/routes/zhihu/hot.ts index aad6e96b3020bf..5abd8ea2aec5e1 100644 --- a/lib/routes/zhihu/hot.ts +++ b/lib/routes/zhihu/hot.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; @@ -18,9 +18,61 @@ const titles = { export const route: Route = { path: '/hot/:category?', - categories: ['social-media'], + categories: ['social-media', 'popular'], example: '/zhihu/hot', - parameters: { category: '分类,见下表,默认为全站' }, + view: ViewType.Articles, + parameters: { + category: { + description: '分类', + default: 'total', + options: [ + { + value: 'total', + label: '全站', + }, + { + value: 'focus', + label: '国际', + }, + { + value: 'science', + label: '科学', + }, + { + value: 'car', + label: '汽车', + }, + { + value: 'zvideo', + label: '视频', + }, + { + value: 'fashion', + label: '时尚', + }, + { + value: 'depth', + label: '时事', + }, + { + value: 'digital', + label: '数码', + }, + { + value: 'sport', + label: '体育', + }, + { + value: 'school', + label: '校园', + }, + { + value: 'film', + label: '影视', + }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -29,12 +81,9 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, - name: '知乎分类热榜', + name: '知乎热榜', maintainers: ['nczitzk'], handler, - description: `| 全站 | 国际 | 科学 | 汽车 | 视频 | 时尚 | 时事 | 数码 | 体育 | 校园 | 影视 | - | ----- | ----- | ------- | ---- | ------ | ------- | ----- | ------- | ----- | ------ | ---- | - | total | focus | science | car | zvideo | fashion | depth | digital | sport | school | film |`, }; async function handler(ctx) { diff --git a/lib/routes/zhihu/hotlist.ts b/lib/routes/zhihu/hotlist.ts deleted file mode 100644 index a227637c4fdf79..00000000000000 --- a/lib/routes/zhihu/hotlist.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import utils from './utils'; -import { parseDate } from '@/utils/parse-date'; - -export const route: Route = { - path: '/hotlist', - categories: ['social-media'], - example: '/zhihu/hotlist', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['www.zhihu.com/hot'], - }, - ], - name: '知乎热榜', - maintainers: ['DIYgod'], - handler, - url: 'www.zhihu.com/hot', -}; - -async function handler() { - const { - data: { data }, - } = await got({ - method: 'get', - url: 'https://www.zhihu.com/api/v3/explore/guest/feeds?limit=40', - }); - - return { - title: '知乎热榜', - link: 'https://www.zhihu.com/billboard', - description: '知乎热榜', - item: data.map((item) => { - switch (item.target.type) { - case 'answer': - return { - title: item.target.question.title, - description: `${item.target.author.name}的回答<br/><br/>${utils.ProcessImage(item.target.content)}`, - author: item.target.author.name, - pubDate: parseDate(item.target.updated_time * 1000), - guid: item.target.id.toString(), - link: `https://www.zhihu.com/question/${item.target.question.id}/answer/${item.target.id}`, - }; - case 'article': - return { - title: item.target.title, - description: `${item.target.author.name}的文章<br/><br/>${utils.ProcessImage(item.target.content)}`, - author: item.target.author.name, - pubDate: parseDate(item.updated * 1000), - guid: item.target.id.toString(), - link: `https://zhuanlan.zhihu.com/p/${item.target.id}`, - }; - default: - return { - title: '未知类型', - description: '请点击链接提交issue', - guid: item.target.type, - link: 'https://github.com/DIYgod/RSSHub/issues', - }; - } - }), - }; -} diff --git a/lib/routes/zhihu/namespace.ts b/lib/routes/zhihu/namespace.ts index e06bffd81dadc7..f04ce1a32ed466 100644 --- a/lib/routes/zhihu/namespace.ts +++ b/lib/routes/zhihu/namespace.ts @@ -3,4 +3,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '知乎', url: 'www.zhihu.com', + description: `::: tip +自2024年7月,未登录状态下大部分路由[无法获取全文](https://github.com/DIYgod/RSSHub/issues/16260)。若有需要请在登陆知乎后寻找并添加包含\`z_c0\`的Cookies至环境变量\`ZHIHU_COOKIES\`。 +:::`, + lang: 'zh-CN', }; diff --git a/lib/routes/zhihu/posts.ts b/lib/routes/zhihu/posts.ts index 8fff51e4507dcd..dac4cf053a1386 100644 --- a/lib/routes/zhihu/posts.ts +++ b/lib/routes/zhihu/posts.ts @@ -1,8 +1,9 @@ import { Route } from '@/types'; -import got from '@/utils/got'; -import utils from './utils'; -import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { header, getSignedHeader, processImage } from './utils'; import { parseDate } from '@/utils/parse-date'; +import { Articles, Profile } from './types'; export const route: Route = { path: '/posts/:usertype/:id', @@ -19,7 +20,7 @@ export const route: Route = { }, radar: [ { - source: ['www.zhihu.com/:usertype/:id/posts'], + source: ['www.zhihu.com/:usertype/:id/posts', 'www.zhihu.com/:usertype/:id'], }, ], name: '用户文章', @@ -34,47 +35,53 @@ async function handler(ctx) { const id = ctx.req.param('id'); const usertype = ctx.req.param('usertype'); - const { data } = await got(`https://www.zhihu.com/${usertype}/${id}/posts`, { - headers: { - ...utils.header, - Referer: `https://www.zhihu.com/${usertype}/${id}/`, - }, + const userProfile = await cache.tryGet(`zhihu:posts:profile:${id}`, async () => { + const userAPIPath = `/api/v4/${usertype === 'people' ? 'members' : 'org'}/${id}?${new URLSearchParams({ + include: 'allow_message,is_followed,is_following,is_org,is_blocking,employments,answer_count,follower_count,articles_count,gender,badge[?(type=best_answerer)].topics', + })}`; + + return await ofetch<Profile>(`https://www.zhihu.com${userAPIPath}`, { + headers: { + ...header, + ...(await getSignedHeader(`https://www.zhihu.com/${usertype}/${id}/`, userAPIPath)), + Referer: `https://www.zhihu.com/${usertype}/${id}/`, + }, + }); }); - const $ = load(data); - const jsondata = $('#js-initialData'); - const authorname = $('.ProfileHeader-name') - .contents() - .filter((_index, element) => element.type === 'text') - .text(); - const authordescription = $('.ProfileHeader-headline').text(); - const parsed = JSON.parse(jsondata.html()); - const articlesdata = parsed.initialState.entities.articles; + const apiPath = `/api/v4/${usertype === 'people' ? 'members' : 'org'}/${id}/articles?${new URLSearchParams({ + include: + 'data[*].comment_count,suggest_edit,is_normal,thumbnail_extra_info,thumbnail,can_comment,comment_permission,admin_closed_comment,content,voteup_count,created,updated,upvoted_followees,voting,review_info,reaction_instruction,is_labeled,label_info;data[*].vessay_info;data[*].author.badge[?(type=best_answerer)].topics;data[*].author.vip_info;', + offset: '0', + limit: '20', + sort_by: 'created', + })}`; - const list = Object.keys(articlesdata).map((key) => { - const $ = load(articlesdata[key].content, null, false); - $('noscript').remove(); - $('img').each((_, item) => { - if (item.attribs['data-actualsrc'] || item.attribs['data-original']) { - item.attribs['data-actualsrc'] = item.attribs['data-actualsrc'] ? item.attribs['data-actualsrc'].split('?source')[0] : null; - item.attribs['data-original'] = item.attribs['data-original'] ? item.attribs['data-original'].split('?source')[0] : null; - item.attribs.src = item.attribs['data-original'] || item.attribs['data-actualsrc']; - delete item.attribs['data-actualsrc']; - delete item.attribs['data-original']; - } - }); - return { - title: articlesdata[key].title, - description: $.html(), - link: articlesdata[key].url, - pubDate: parseDate(articlesdata[key].created, 'X'), - }; + const signedHeader = await getSignedHeader(`https://www.zhihu.com/${usertype}/${id}/posts`, apiPath); + + const articleResponse = await ofetch<Articles>(`https://www.zhihu.com${apiPath}`, { + headers: { + ...header, + ...signedHeader, + Referer: `https://www.zhihu.com/${usertype}/${id}/posts`, + }, }); + const items = articleResponse.data.map((item) => ({ + title: item.title, + description: processImage(item.content), + link: `https://zhuanlan.zhihu.com/p/${item.id}`, + pubDate: parseDate(item.created, 'X'), + updated: parseDate(item.updated, 'X'), + author: item.author.name, + })); + return { - title: `${authorname} 的知乎文章`, + title: `${userProfile.name} 的知乎文章`, link: `https://www.zhihu.com/${usertype}/${id}/posts`, - description: authordescription, - item: list, + description: userProfile.headline, + image: userProfile.avatar_url.split('?')[0], + // banner: userData?.coverUrl?.split('?')[0], + item: items, }; } diff --git a/lib/routes/zhihu/question.ts b/lib/routes/zhihu/question.ts index e95f0d984fa87e..bc2c324e05c71f 100644 --- a/lib/routes/zhihu/question.ts +++ b/lib/routes/zhihu/question.ts @@ -1,9 +1,6 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; import got from '@/utils/got'; -import utils from './utils'; -import md5 from '@/utils/md5'; -import g_encrypt from './execlib/x-zse-96-v3'; +import { header, processImage, getSignedHeader } from './utils'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { @@ -35,50 +32,25 @@ async function handler(ctx) { questionId, sortBy = 'default', // default,created,updated } = ctx.req.param(); - // Because the API of zhihu.com has changed, we must use the value of `d_c0` (extracted from cookies) to calculate - // `x-zse-96`. So first get `d_c0`, then get the actual data of a ZhiHu question. In this way, we don't need to - // require users to set the cookie in environmental variables anymore. - - // fisrt: get cookie(dc_0) from zhihu.com - const cookie_mes = await cache.tryGet('zhihu:cookies:d_c0', async () => { - const response = await got({ - method: 'get', - url: `https://www.zhihu.com/question/${questionId}`, - headers: { - ...utils.header, - }, - }); - - const cookie_org = response.headers['set-cookie']; - const cookie = cookie_org.toString(); - const match = cookie.match(/d_c0=(\S+?)(?:;|$)/); - const cookie_mes = match && match[1]; - if (!cookie_mes) { - throw new Error('Failed to extract `d_c0` from cookies'); - } - return cookie_mes; - }); - const cookie = `d_c0=${cookie_mes}`; // second: get real data from zhihu - const limit = 20; - const include = - 'data%5B*%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cattachment%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Cis_labeled%2Cpaid_info%2Cpaid_info_content%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_recognized%3Bdata%5B*%5D.mark_infos%5B*%5D.url%3Bdata%5B*%5D.author.follower_count%2Cbadge%5B*%5D.topics%3Bdata%5B*%5D.settings.table_of_content.enabled&offset=0'; const rootUrl = 'https://www.zhihu.com'; - const apiPath = `/api/v4/questions/${questionId}/answers?include=${include}&limit=${limit}&sort_by=${sortBy}&platform=desktop`; - const url = rootUrl + apiPath; + const apiPath = `/api/v4/questions/${questionId}/answers?${new URLSearchParams({ + include: + 'data[*].is_normal,admin_closed_comment,reward_info,is_collapsed,annotation_action,annotation_detail,collapse_reason,is_sticky,collapsed_by,suggest_edit,comment_count,can_comment,content,editable_content,attachment,voteup_count,reshipment_settings,comment_permission,created_time,updated_time,review_info,relevant_info,question,excerpt,is_labeled,paid_info,paid_info_content,relationship.is_authorized,is_author,voting,is_thanked,is_nothelp,is_recognized;data[*].mark_infos[*].url;data[*].author.follower_count,badge[*].topics;data[*].settings.table_of_content.enabled&offset=0', + limit: '20', + sort_by: sortBy, + platform: 'desktop', + })}`; - // calculate x-zse-96, refer to https://github.com/srx-2000/spider_collection/issues/18 - const f = `101_3_3.0+${apiPath}+${cookie_mes}`; - const xzse96 = '2.0_' + g_encrypt(md5(f)); - const _header = { cookie, 'x-zse-96': xzse96, 'x-app-za': 'OS=Web', 'x-zse-93': '101_3_3.0' }; + const signedHeader = await getSignedHeader(`https://www.zhihu.com/question/${questionId}`, apiPath); const response = await got({ method: 'get', - url, + url: rootUrl + apiPath, headers: { - ...utils.header, - ..._header, + ...header, + ...signedHeader, Referer: `https://www.zhihu.com/question/${questionId}`, // Authorization: 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20', // previously hard-coded in js, outdated }, @@ -91,7 +63,7 @@ async function handler(ctx) { link: `https://www.zhihu.com/question/${questionId}`, item: listRes.map((item) => { const title = `${item.author.name}的回答:${item.excerpt}`; - const description = `${item.author.name}的回答<br/><br/>${utils.ProcessImage(item.content)}`; + const description = `${item.author.name}的回答<br/><br/>${processImage(item.content)}`; return { title, diff --git a/lib/routes/zhihu/timeline.ts b/lib/routes/zhihu/timeline.ts index cdd7f28a40f742..1c4c6b1cd0028a 100644 --- a/lib/routes/zhihu/timeline.ts +++ b/lib/routes/zhihu/timeline.ts @@ -1,7 +1,7 @@ import { Route } from '@/types'; import got from '@/utils/got'; import { config } from '@/config'; -import utils from './utils'; +import { processImage } from './utils'; import { parseDate } from '@/utils/parse-date'; import ConfigNotFoundError from '@/errors/types/config-not-found'; @@ -26,9 +26,9 @@ export const route: Route = { name: '用户关注时间线', maintainers: ['SeanChao'], handler, - description: `:::warning + description: `::: warning 用户关注动态需要登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。 - :::`, +:::`, }; async function handler(ctx) { @@ -60,12 +60,14 @@ async function handler(ctx) { const questionId = e.target.question.id; return `${urlBase}/question/${questionId}/answer/${id}`; } + case 'pin': case 'article': return e.target.url; case 'question': return `${urlBase}/question/${id}`; + default: + return; } - return ''; }; /** @@ -92,7 +94,7 @@ async function handler(ctx) { return ( content .map((e) => e.content) - .filter((e) => e instanceof String && !!e) + .filter((e) => !!e && typeof e === 'string') // some content may not be wrapped in tag, it will cause error when parsing .map((e) => `<div>${e}</div>`) .join('') @@ -106,16 +108,17 @@ async function handler(ctx) { const link = buildLink(e); return { title: `${e.action_text_tpl.replace('{}', buildActors(e))}: ${getOne([e.target.title, e.target.question ? e.target.question.title : ''])}`, - description: utils.ProcessImage(`<div>${getOne([e.target.content_html, getContent(e.target.content), e.target.detail, e.target.excerpt, ''])}</div>`), + description: processImage(`<div>${getOne([e.target.content_html, getContent(e.target.content), e.target.detail, e.target.excerpt, ''])}</div>`), pubDate: parseDate(e.updated_time * 1000), link, author: e.target.author ? e.target.author.name : '', guid: link, + category: [e.verb], }; }; const out = feeds - .filter((e) => e.verb && e.verb !== 'MEMBER_VOTEUP_ARTICLE' && e.verb !== 'MEMBER_VOTEUP_ANSWER') + .filter((e) => e.verb) .map((e) => { if (e && e.type && e.type === 'feed_group') { // A feed group contains a list of feeds whose structure is the same as a single feed @@ -134,6 +137,7 @@ async function handler(ctx) { description, pubDate, guid, + category: [e.verb], }; } return buildItem(e); diff --git a/lib/routes/zhihu/topic.ts b/lib/routes/zhihu/topic.ts index e181dd33a70177..190b1e4df2934c 100644 --- a/lib/routes/zhihu/topic.ts +++ b/lib/routes/zhihu/topic.ts @@ -1,10 +1,8 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; -import utils from './utils'; +import { header, processImage, getSignedHeader } from './utils'; import { parseDate } from '@/utils/parse-date'; -import g_encrypt from './execlib/x-zse-96-v3'; -import md5 from '@/utils/md5'; export const route: Route = { path: '/topic/:topicId/:isTop?', @@ -34,22 +32,6 @@ async function handler(ctx) { const { topicId, isTop = false } = ctx.req.param(); const link = `https://www.zhihu.com/topic/${topicId}/${isTop ? 'top-answers' : 'newest'}`; - const cookieDc0 = await cache.tryGet('zhihu:cookies:d_c0', async () => { - const { headers } = await got(link, { - headers: { - ...utils.header, - }, - }); - const cookie = headers['set-cookie'].toString(); - const match = cookie.match(/d_c0=(\S+?)(?:;|$)/); - const cookieMatched = match?.[1]; - if (!cookieMatched) { - throw new Error('Failed to extract `d_c0` from cookies'); - } - return cookieMatched; - }); - const cookie = `d_c0=${cookieDc0}`; - const topicMeta = await cache.tryGet(`zhihu:topic:${topicId}`, async () => { const { data: response } = await got(`https://www.zhihu.com/api/v4/topics/${topicId}/intro`, { searchParams: { @@ -60,15 +42,12 @@ async function handler(ctx) { }); const apiPath = `/api/v5.1/topics/${topicId}/feeds/${isTop ? 'top_activity' : 'timeline_activity'}`; - const xzse93 = '101_3_3.0'; - const f = `${xzse93}+${apiPath}+${cookieDc0}`; - const xzse96 = '2.0_' + g_encrypt(md5(f)); - const _header = { cookie, 'x-zse-96': xzse96, 'x-zse-93': xzse93 }; + const signedHeader = await getSignedHeader(link, apiPath); const { data: response } = await got(`https://www.zhihu.com${apiPath}`, { headers: { - ...utils.header, - ..._header, + ...header, + ...signedHeader, Referer: link, }, }); @@ -78,13 +57,13 @@ async function handler(ctx) { let title = ''; let description = ''; let link = ''; - let pubDate = ''; + let pubDate: Date; let author = ''; switch (type) { case 'answer': title = `${item.question.title}-${item.author.name}的回答:${item.excerpt}`; - description = `<strong>${item.question.title}</strong><br>${item.author.name}的回答<br/>${utils.ProcessImage(item.content)}`; + description = `<strong>${item.question.title}</strong><br>${item.author.name}的回答<br/>${processImage(item.content)}`; link = `https://www.zhihu.com/question/${item.question.id}/answer/${item.id}`; pubDate = parseDate(item.updated_time * 1000); author = item.author.name; @@ -99,7 +78,7 @@ async function handler(ctx) { case 'article': title = item.title; - description = utils.ProcessImage(item.content); + description = processImage(item.content); link = item.url; pubDate = parseDate(item.created * 1000); break; @@ -116,6 +95,7 @@ async function handler(ctx) { default: description = '未知类型,请点击<a href="https://github.com/DIYgod/RSSHub/issues">链接</a>提交issue'; + pubDate = parseDate(Date.now()); } return { diff --git a/lib/routes/zhihu/types.ts b/lib/routes/zhihu/types.ts new file mode 100644 index 00000000000000..d03563c5311bd9 --- /dev/null +++ b/lib/routes/zhihu/types.ts @@ -0,0 +1,130 @@ +export interface Profile { + id: string; + url_token: string; + name: string; + use_default_avatar: boolean; + avatar_url: string; + avatar_url_template: string; + is_org: boolean; + type: string; + url: string; + user_type: string; + headline: string; + headline_render: string; + gender: number; + is_advertiser: boolean; + ip_info: string; + vip_info: { + is_vip: boolean; + vip_type: number; + rename_days: string; + entrance_v2: null; + rename_frequency: number; + rename_await_days: number; + }; + badge: any[]; + badge_v2: { + title: string; + merged_badges: any[]; + detail_badges: any[]; + icon: string; + night_icon: string; + }; + allow_message: boolean; + is_following: boolean; + is_followed: boolean; + is_blocking: boolean; + follower_count: number; + answer_count: number; + articles_count: number; + available_medals_count: number; + employments: any[]; + is_realname: boolean; + has_applying_column: boolean; +} + +interface Article { + image_url: string; + updated: number; + is_jump_native: boolean; + is_labeled: boolean; + copyright_permission: string; + vessay_info: { + enable_video_translate: boolean; + }; + excerpt: string; + admin_closed_comment: boolean; + article_type: string; + excerpt_title: string; + reaction_instruction: { + REACTION_CONTENT_SEGMENT_LIKE: string; + }; + id: number; + voteup_count: number; + upvoted_followees: []; + can_comment: { + status: boolean; + reason: string; + }; + author: Profile; + url: string; + comment_permission: string; + created: number; + image_width: number; + content: string; + comment_count: number; + linkbox: { + url: string; + category: string; + pic: string; + title: string; + }; + title: string; + voting: number; + type: string; + suggest_edit: { + status: boolean; + url: string; + reason: string; + tip: string; + title: string; + }; + is_normal: boolean; +} + +export interface Articles { + paging: { + is_end: boolean; + totals: number; + previous: string; + is_start: boolean; + next: string; + }; + data: Article[]; +} + +export interface CollectionItem { + content: { + type: string; + title?: string; + question?: { + title: string; + }; + url: string; + content: string; + video?: { + url: string; + }; + updated?: number; + updated_time?: number; + }; + collectionTitle?: string; +} + +export interface Collection { + id: string; + title: string; + creator: { + name: string; + }; +} diff --git a/lib/routes/zhihu/utils.ts b/lib/routes/zhihu/utils.ts index 217bb9ebbed56f..0800c7fbbff321 100644 --- a/lib/routes/zhihu/utils.ts +++ b/lib/routes/zhihu/utils.ts @@ -1,51 +1,114 @@ import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import g_encrypt from './execlib/x-zse-96-v3'; +import md5 from '@/utils/md5'; +import { config } from '@/config'; -export default { - header: { - 'x-api-version': '3.0.91', - }, - ProcessImage(content) { - const $ = load(content, null, false); - - $('noscript').remove(); - - $('a[data-draft-type="mcn-link-card"]').remove(); - - $('a').each((_, elem) => { - const href = $(elem).attr('href'); - if (href?.startsWith('https://link.zhihu.com/?target=')) { - const url = new URL(href); - const target = url.searchParams.get('target') || ''; - try { - $(elem).attr('href', decodeURIComponent(target)); - } catch { - // sometimes the target is not a valid url - } - } - }); - - $('img.content_image, img.origin_image, img.content-image, img.data-actualsrc, figure>img').each((i, e) => { - if (e.attribs['data-actualsrc']) { - $(e).attr({ - src: e.attribs['data-actualsrc'].replace('_b.jpg', '_1440w.jpg'), - width: null, - height: null, - }); - } else if (e.attribs['data-original']) { - $(e).attr({ - src: e.attribs['data-original'].replace('_r.jpg', '_1440w.jpg'), - width: null, - height: null, - }); - } else { - $(e).attr({ - src: e.attribs.src.replace('_b.jpg', '_1440w.jpg'), - width: null, - height: null, - }); +export const header = { + 'x-api-version': '3.0.91', +}; + +const fixImageUrl = (url: string) => url.split('?')[0].replace('_b.jpg', '.jpg').replace('_r.jpg', '.jpg').replace('_720w.jpg', '.jpg'); + +export const processImage = (content: string) => { + const $ = load(content, null, false); + + $('noscript, a[data-draft-type="mcn-link-card"]').remove(); + + $('a').each((_, elem) => { + const href = $(elem).attr('href'); + if (href?.startsWith('http://link.zhihu.com/?target=') || href?.startsWith('https://link.zhihu.com/?target=')) { + const url = new URL(href); + const target = url.searchParams.get('target') || ''; + try { + $(elem).attr('href', decodeURIComponent(target)); + } catch { + // sometimes the target is not a valid url } - }); + } + }); + + $('img.content_image, img.origin_image, img.content-image, img.data-actualsrc, figure>img').each((i, e) => { + if (e.attribs['data-actualsrc']) { + $(e).attr({ + src: fixImageUrl(e.attribs['data-actualsrc']), + width: null, + height: null, + }); + $(e).removeAttr('data-actualsrc'); + } else if (e.attribs['data-original']) { + $(e).attr({ + src: fixImageUrl(e.attribs['data-original']), + width: null, + height: null, + }); + $(e).removeAttr('data-original'); + } else { + $(e).attr({ + src: fixImageUrl(e.attribs.src), + width: null, + height: null, + }); + } + }); + + return $.html(); +}; + +export const getCookieValueByKey = (key: string) => + config.zhihu.cookies + ?.split(';') + .map((e) => e.trim()) + .find((e) => e.startsWith(key + '=')) + ?.slice(key.length + 1) || ''; + +export const getSignedHeader = async (url: string, apiPath: string) => { + // Because the API of zhihu.com has changed, we must use the value of `d_c0` (extracted from cookies) to calculate + // `x-zse-96`. So first get `d_c0`, then get the actual data of a ZhiHu question. In this way, we don't need to + // require users to set the cookie in environmental variables anymore. + + // fisrt: get cookie(dc_0) from zhihu.com + const dc0 = await cache.tryGet('zhihu:cookies:d_c0', async () => { + if (getCookieValueByKey('d_c0')) { + return getCookieValueByKey('d_c0'); + } + const response1 = await ofetch.raw('https://static.zhihu.com/zse-ck/v3.js'); + const script = await response1._data.text(); + const zseCk = script.match(/__g\.ck\|\|"([\w+/=\\]*?)",_=/)?.[1]; + const response2 = zseCk + ? await ofetch.raw(url, { + headers: { + cookie: `${response1.headers + .getSetCookie() + .map((s) => s.split(';')[0]) + .join('; ')}; __zse_ck=${zseCk}`, + }, + }) + : null; + + const dc0 = + (response2 || response1).headers + .getSetCookie() + .find((s) => s.startsWith('d_c0=')) + ?.split(';')[0] + .trim() + .slice('d_c0='.length) || ''; + + return dc0; + }); + + // calculate x-zse-96, refer to https://github.com/srx-2000/spider_collection/issues/18 + const xzse93 = '101_3_3.0'; + const f = `${xzse93}+${apiPath}+${dc0}`; + const xzse96 = '2.0_' + g_encrypt(md5(f)); + + const zc0 = getCookieValueByKey('z_c0'); - return $.html(); - }, + return { + cookie: `d_c0=${dc0}${zc0 ? `;z_c0=${zc0}` : ''}`, + 'x-zse-96': xzse96, + 'x-app-za': 'OS=Web', + 'x-zse-93': xzse93, + }; }; diff --git a/lib/routes/zhihu/xhu/activities.ts b/lib/routes/zhihu/xhu/activities.ts index 2f48794f464c94..d5c892a366bde5 100644 --- a/lib/routes/zhihu/xhu/activities.ts +++ b/lib/routes/zhihu/xhu/activities.ts @@ -1,7 +1,7 @@ import { Route } from '@/types'; import got from '@/utils/got'; import auth from './auth'; -import utils from '../utils'; +import { processImage } from '../utils'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { @@ -28,16 +28,16 @@ export const route: Route = { handler, description: `[xhu](https://github.com/REToys/xhu) - :::tip +::: tip 用户的 16 进制 id 获取方式: 1. 可以通过 RSSHub Radar 扩展获取; 2. 或者在用户主页打开 F12 控制台,执行以下代码:\`console.log(/"id":"([0-9a-f]*?)","urlToken"/.exec(document.getElementById('js-initialData').innerHTML)[1]);\` 即可获取用户的 16 进制 id。 - :::`, +:::`, }; async function handler(ctx) { - const xhuCookie = await auth.getCookie(ctx); + const xhuCookie = await auth.getCookie(); const hexId = ctx.req.param('hexId'); const link = `https://www.zhihu.com/people/${hexId}`; const url = `https://api.zhihuvvv.workers.dev/people/${hexId}/activities?before_id=0&limit=20`; @@ -62,7 +62,7 @@ async function handler(ctx) { let title; let description; let url; - const images = []; + const images: string[] = []; let text = ''; let link = ''; let author = ''; @@ -71,13 +71,13 @@ async function handler(ctx) { case 'answer': title = detail.question.title; author = detail.author.name; - description = utils.ProcessImage(detail.content); + description = processImage(detail.content); url = `https://www.zhihu.com/question/${detail.question.id}/answer/${detail.id}`; break; case 'article': title = detail.title; author = detail.author.name; - description = utils.ProcessImage(detail.content); + description = processImage(detail.content); url = `https://zhuanlan.zhihu.com/p/${detail.id}`; break; case 'pin': @@ -119,7 +119,7 @@ async function handler(ctx) { case 'question': title = detail.title; author = detail.author.name; - description = utils.ProcessImage(detail.detail); + description = processImage(detail.detail); url = `https://www.zhihu.com/question/${detail.id}`; break; case 'collection': @@ -146,6 +146,8 @@ async function handler(ctx) { description = detail.description; url = `https://www.zhihu.com/roundtable/${detail.id}`; break; + default: + description = `未知类型 ${item.target.type},请点击<a href="https://github.com/DIYgod/RSSHub/issues">链接</a>提交issue`; } return { diff --git a/lib/routes/zhihu/xhu/answers.ts b/lib/routes/zhihu/xhu/answers.ts index 31b894dc953cc1..39dc67f992f96b 100644 --- a/lib/routes/zhihu/xhu/answers.ts +++ b/lib/routes/zhihu/xhu/answers.ts @@ -28,7 +28,7 @@ export const route: Route = { }; async function handler(ctx) { - const xhuCookie = await auth.getCookie(ctx); + const xhuCookie = await auth.getCookie(); const hexId = ctx.req.param('hexId'); const link = `https://www.zhihu.com/people/${hexId}/answers`; const url = `https://api.zhihuvvv.workers.dev/people/${hexId}/answers?limit=20&offset=0`; diff --git a/lib/routes/zhihu/xhu/collection.ts b/lib/routes/zhihu/xhu/collection.ts index ff978af28bdd87..b0ac9a7051bc05 100644 --- a/lib/routes/zhihu/xhu/collection.ts +++ b/lib/routes/zhihu/xhu/collection.ts @@ -28,7 +28,7 @@ export const route: Route = { }; async function handler(ctx) { - const xhuCookie = await auth.getCookie(ctx); + const xhuCookie = await auth.getCookie(); const id = ctx.req.param('id'); const link = `https://www.zhihu.com/collection/${id}`; diff --git a/lib/routes/zhihu/xhu/posts.ts b/lib/routes/zhihu/xhu/posts.ts index b36cc767cc1ac1..c71ea46dedf6c5 100644 --- a/lib/routes/zhihu/xhu/posts.ts +++ b/lib/routes/zhihu/xhu/posts.ts @@ -22,7 +22,7 @@ export const route: Route = { }; async function handler(ctx) { - const xhuCookie = await auth.getCookie(ctx); + const xhuCookie = await auth.getCookie(); const hexId = ctx.req.param('hexId'); const link = `https://www.zhihu.com/people/${hexId}/posts`; const url = `https://api.zhihuvvv.workers.dev/people/${hexId}/articles?limit=20&offset=0`; diff --git a/lib/routes/zhihu/xhu/question.ts b/lib/routes/zhihu/xhu/question.ts index 3bb0074842251e..7eeaee78e2e941 100644 --- a/lib/routes/zhihu/xhu/question.ts +++ b/lib/routes/zhihu/xhu/question.ts @@ -1,7 +1,7 @@ import { Route } from '@/types'; import got from '@/utils/got'; import auth from './auth'; -import utils from '../utils'; +import { processImage } from '../utils'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { @@ -29,7 +29,7 @@ export const route: Route = { }; async function handler(ctx) { - const xhuCookie = await auth.getCookie(ctx); + const xhuCookie = await auth.getCookie(); const { questionId, sortBy = 'default', // default,created,updated @@ -54,7 +54,7 @@ async function handler(ctx) { const link = `https://www.zhihu.com/question/${questionId}/answer/${item.id}`; const author = item.author.name; const title = `${author}的回答:${item.excerpt}`; - const description = `${author}的回答<br/><br/>${utils.ProcessImage(item.excerpt)}`; + const description = `${author}的回答<br/><br/>${processImage(item.excerpt)}`; return { title, diff --git a/lib/routes/zhihu/xhu/topic.ts b/lib/routes/zhihu/xhu/topic.ts index e98c7a1452bfa3..da6556f141110e 100644 --- a/lib/routes/zhihu/xhu/topic.ts +++ b/lib/routes/zhihu/xhu/topic.ts @@ -1,7 +1,7 @@ import { Route } from '@/types'; import got from '@/utils/got'; import auth from './auth'; -import utils from '../utils'; +import { processImage } from '../utils'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { @@ -28,7 +28,7 @@ export const route: Route = { }; async function handler(ctx) { - const xhuCookie = await auth.getCookie(ctx); + const xhuCookie = await auth.getCookie(); const topicId = ctx.req.param('topicId'); const link = `https://www.zhihu.com/topic/${topicId}/newest`; const url = `https://api.zhihuvvv.workers.dev/topics/${topicId}/feeds/timeline_activity?before_id=0&limit=20`; @@ -51,13 +51,13 @@ async function handler(ctx) { let title = ''; let description = ''; let link = ''; - let pubDate = ''; + let pubDate = new Date(); let author = ''; switch (type) { case 'answer': title = `${item.question.title}-${item.author.name}的回答:${item.excerpt}`; - description = `<strong>${item.question.title}</strong><br>${item.author.name}的回答<br/>${utils.ProcessImage(item.content)}`; + description = `<strong>${item.question.title}</strong><br>${item.author.name}的回答<br/>${processImage(item.content)}`; link = `https://www.zhihu.com/question/${item.question.id}/answer/${item.id}`; pubDate = parseDate(item.updated_time * 1000); author = item.author.name; diff --git a/lib/routes/zhihu/zhuanlan.ts b/lib/routes/zhihu/zhuanlan.ts index 95bebd91a1a7b9..0088997fbcedc8 100644 --- a/lib/routes/zhihu/zhuanlan.ts +++ b/lib/routes/zhihu/zhuanlan.ts @@ -1,6 +1,6 @@ import { Route } from '@/types'; import got from '@/utils/got'; -import utils from './utils'; +import { getSignedHeader, header } from './utils'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; @@ -29,12 +29,18 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); + // 知乎专栏链接存在两种格式, 一种以 'zhuanlan.' 开头, 另一种新增的以 'c_' 结尾 + let url = `https://zhuanlan.zhihu.com/${id}`; + if (id.search('c_') === 0) { + url = `https://www.zhihu.com/column/${id}`; + } + const signedHeader = await getSignedHeader(url, `/api/v4/columns/${id}/items`); const listRes = await got({ method: 'get', url: `https://www.zhihu.com/api/v4/columns/${id}/items`, headers: { - ...utils.header, + ...signedHeader, Referer: `https://zhuanlan.zhihu.com/${id}`, }, }); @@ -43,20 +49,20 @@ async function handler(ctx) { method: 'get', url: `https://www.zhihu.com/api/v4/columns/${id}/pinned-items`, headers: { - ...utils.header, + ...header, + ...signedHeader, Referer: `https://zhuanlan.zhihu.com/${id}`, }, }); listRes.data.data = [...listRes.data.data, ...pinnedRes.data.data]; - // 知乎专栏链接存在两种格式, 一种以 'zhuanlan.' 开头, 另一种新增的以 'c_' 结尾 - let url = `https://zhuanlan.zhihu.com/${id}`; - if (id.search('c_') === 0) { - url = `https://www.zhihu.com/column/${id}`; - } - - const infoRes = await got(url); + const infoRes = await got(url, { + headers: { + ...signedHeader, + Referer: url, + }, + }); const $ = load(infoRes.data); const title = $('.css-zyehvu').text(); const description = $('.css-1bnklpv').text(); @@ -73,7 +79,7 @@ async function handler(ctx) { let title = ''; let link = ''; let author = ''; - let pubDate = ''; + let pubDate: Date; switch (item.type) { case 'answer': diff --git a/lib/routes/zhitongcaijing/index.ts b/lib/routes/zhitongcaijing/index.ts index 9d574bc26b407a..b9d0e30d817363 100644 --- a/lib/routes/zhitongcaijing/index.ts +++ b/lib/routes/zhitongcaijing/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); @@ -70,7 +70,8 @@ const ids = { export const route: Route = { path: '/:id?/:category?', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Articles, example: '/zhitongcaijing', parameters: { id: '栏目 id,可在对应栏目页 URL 中找到,默认为 recommend,即推荐', category: '分类 id,可在对应栏目子分类页 URL 中找到,默认为全部' }, features: { @@ -85,21 +86,21 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| id | 栏目 | - | ------------ | ---- | - | recommend | 推荐 | - | hkstock | 港股 | - | meigu | 美股 | - | agu | 沪深 | - | ct | 创投 | - | esg | ESG | - | aqs | 券商 | - | ajj | 基金 | - | focus | 要闻 | - | announcement | 公告 | - | research | 研究 | - | shares | 新股 | - | bazaar | 市场 | - | company | 公司 |`, +| ------------ | ---- | +| recommend | 推荐 | +| hkstock | 港股 | +| meigu | 美股 | +| agu | 沪深 | +| ct | 创投 | +| esg | ESG | +| aqs | 券商 | +| ajj | 基金 | +| focus | 要闻 | +| announcement | 公告 | +| research | 研究 | +| shares | 新股 | +| bazaar | 市场 | +| company | 公司 |`, }; async function handler(ctx) { diff --git a/lib/routes/zhitongcaijing/namespace.ts b/lib/routes/zhitongcaijing/namespace.ts index 12f2eb5cce9b24..d23e16e3df7049 100644 --- a/lib/routes/zhitongcaijing/namespace.ts +++ b/lib/routes/zhitongcaijing/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '智通财经网', url: 'zhitongcaijing.com', + lang: 'zh-CN', }; diff --git a/lib/routes/zhiy/namespace.ts b/lib/routes/zhiy/namespace.ts index 3eadab44176a71..82cdf439980b8c 100644 --- a/lib/routes/zhiy/namespace.ts +++ b/lib/routes/zhiy/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '知园', url: 'zhiy.cc', + lang: 'zh-CN', }; diff --git a/lib/routes/zhonglun/index.ts b/lib/routes/zhonglun/index.ts index 01e7ed05a3b725..5e62a39e5005c3 100644 --- a/lib/routes/zhonglun/index.ts +++ b/lib/routes/zhonglun/index.ts @@ -20,22 +20,21 @@ export const handler = async (ctx) => { const $ = load(response); - let items = $('div#dataList h3') + let items = $('div#dataList > dl > dd, div#dataList > ul > li') .slice(0, limit) .toArray() .map((item) => { item = $(item); - const title = item.text(); const description = art(path.join(__dirname, 'templates/description.art'), { - intro: item.next().text(), + intro: item.find('p').text(), }); return { - title, + title: item.find('h3 > a').text(), description, - pubDate: parseDate(item.find('span').first().text()), - link: item.find('a').prop('href'), + pubDate: parseDate(item.find('span').text()), + link: item.find('h3 > a').prop('href'), language, }; }); @@ -95,9 +94,9 @@ export const route: Route = { example: '/zhonglun/research/article/zh', parameters: { category: '语言,默认为 zh,即简体中文,可在对应分类页 URL 中找到' }, description: ` - | ENG | 简体中文 | 日本語 | 한국어 | - | --- | -------- | ------ | ------ | - | en | zh | ja | kr | +| ENG | 简体中文 | 日本語 | 한국어 | +| --- | -------- | ------ | ------ | +| en | zh | ja | kr | `, categories: ['new-media'], diff --git a/lib/routes/zhonglun/namespace.ts b/lib/routes/zhonglun/namespace.ts index 47b205794dd27c..8eebd5d582b98c 100644 --- a/lib/routes/zhonglun/namespace.ts +++ b/lib/routes/zhonglun/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: 'zhonglun.com', categories: ['new-media'], description: '', + lang: 'zh-CN', }; diff --git a/lib/routes/zhubai/index.ts b/lib/routes/zhubai/index.ts index 32a500b928a503..bc8e9b5ee0db62 100644 --- a/lib/routes/zhubai/index.ts +++ b/lib/routes/zhubai/index.ts @@ -20,9 +20,9 @@ export const route: Route = { name: '文章', maintainers: ['naixy28'], handler, - description: `:::tip + description: `::: tip 在路由末尾处加上 \`?limit=限制获取数目\` 来限制获取条目数量,默认值为\`20\` - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/zhubai/namespace.ts b/lib/routes/zhubai/namespace.ts index 0a03a0fac5d4ee..96fcb953dd8ee3 100644 --- a/lib/routes/zhubai/namespace.ts +++ b/lib/routes/zhubai/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '竹白', url: 'zhubai.love', + lang: 'zh-CN', }; diff --git a/lib/routes/zhubai/top20.ts b/lib/routes/zhubai/top20.ts index 81527e75e6869c..275d36c960a10d 100644 --- a/lib/routes/zhubai/top20.ts +++ b/lib/routes/zhubai/top20.ts @@ -51,7 +51,7 @@ async function handler(ctx) { let items = response.data.slice(0, limit).map((item) => ({ title: item.pn, - link: item.fp ?? item.pq ?? item.pu, + link: item.pu ?? item.pq ?? item.fp, description: item.pa, author: item.zn, pubDate: parseRelativeDate(item.lu.replace(/\.\d+/, '')), @@ -60,7 +60,7 @@ async function handler(ctx) { items = await Promise.all( items.map((item) => cache.tryGet(item.link, async () => { - const matches = item.link.match(/\/(?:fp|pq|pu)\/([\w-]+)\/(\d+)/); + const matches = item.link.match(/\/(?:pl|pq|fp)\/([\w-]+)\/(\d+)/); const { data } = await got(`https://${matches[1]}.zhubai.love/api/posts/${matches[2]}`); diff --git a/lib/routes/zhujiceping/namespace.ts b/lib/routes/zhujiceping/namespace.ts index cf6c4c11085020..a400563195fdf8 100644 --- a/lib/routes/zhujiceping/namespace.ts +++ b/lib/routes/zhujiceping/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '国外主机测评', url: 'zhujiceping.com', + lang: 'zh-CN', }; diff --git a/lib/routes/zhuwang/index.ts b/lib/routes/zhuwang/index.ts index e6b6099681d5af..40201cd8de85c5 100644 --- a/lib/routes/zhuwang/index.ts +++ b/lib/routes/zhuwang/index.ts @@ -26,7 +26,7 @@ export const route: Route = { }; async function handler() { - const baseUrl = 'https://zhujia.zhuwang.cc/'; + const baseUrl = 'https://zhujia.zhuwang.com.cn/'; const now = new Date(); const date = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`; const response = await got(`${baseUrl}/api/chartData`, { diff --git a/lib/routes/zhuwang/namespace.ts b/lib/routes/zhuwang/namespace.ts index d068e11f470675..375ae3acdcd7d9 100644 --- a/lib/routes/zhuwang/namespace.ts +++ b/lib/routes/zhuwang/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国养猪网', url: 'zhujia.zhuwang.cc', + lang: 'zh-CN', }; diff --git a/lib/routes/zimuxia/index.ts b/lib/routes/zimuxia/index.ts index 66e0908ad6f1e8..7939ba9f66aaa8 100644 --- a/lib/routes/zimuxia/index.ts +++ b/lib/routes/zimuxia/index.ts @@ -20,8 +20,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| ALL | FIX 德语社 | 欧美剧集 | 欧美电影 | 综艺 & 纪录 | FIX 日语社 | FIX 韩语社 | FIX 法语社 | - | --- | ---------- | -------- | -------- | ----------- | ---------- | ---------- | ---------- | - | | 昆仑德语社 | 欧美剧集 | 欧美电影 | 综艺纪录 | fix 日语社 | fix 韩语社 | fix 法语社 |`, +| --- | ---------- | -------- | -------- | ----------- | ---------- | ---------- | ---------- | +| | 昆仑德语社 | 欧美剧集 | 欧美电影 | 综艺纪录 | fix 日语社 | fix 韩语社 | fix 法语社 |`, }; async function handler(ctx) { diff --git a/lib/routes/zimuxia/namespace.ts b/lib/routes/zimuxia/namespace.ts index f8cd437829dc54..a57f519c49985e 100644 --- a/lib/routes/zimuxia/namespace.ts +++ b/lib/routes/zimuxia/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'FIX 字幕侠', url: 'zimuxia.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/zimuxia/portfolio.ts b/lib/routes/zimuxia/portfolio.ts index e3502aebb1d3d2..7e59141ce2811c 100644 --- a/lib/routes/zimuxia/portfolio.ts +++ b/lib/routes/zimuxia/portfolio.ts @@ -24,7 +24,7 @@ export const route: Route = { name: '剧集', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 本路由以 \`magnet\` 为默认 linktype,可以通过在路由后方加上 \`?linktype=链接类型\` 指定导出的链接类型。比如路由为 [\`/zimuxia/portfolio/我们这一天?linktype=baidu\`](https://rsshub.app/zimuxia/portfolio/我们这一天?linktype=baidu) 来导出百度盘链接。目前,你可以选择的 \`链接类型\` 包括: \`magnet\`(默认), \`all\`(所有), \`ed2k\`(电驴), \`baidu\`(百度盘), \`quark\`(夸克盘), \`115\`(115 盘), \`subhd\`(字幕). :::`, }; diff --git a/lib/routes/zjgtjy/index.ts b/lib/routes/zjgtjy/index.ts index 54f6495ec88475..47826ad0de2ec2 100644 --- a/lib/routes/zjgtjy/index.ts +++ b/lib/routes/zjgtjy/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'; export const route: Route = { path: '/:type?', @@ -13,10 +13,7 @@ async function handler(ctx) { const type = ctx.req.param('type') === 'all' ? '' : ctx.req.param('type').toUpperCase(); const host = `https://td.zjgtjy.cn:8553/devops/noticeInfo/queryNoticeInfoList?pageSize=10&pageNumber=1¬iceType=${type}&sort=DESC`; - const response = await got({ - method: 'get', - url: host, - }).json(); + const response = await ofetch(host); const data = response.data; const items = await Promise.all( @@ -25,10 +22,7 @@ async function handler(ctx) { const pageLink = `https://td.zjgtjy.cn/view/trade/announcement/detail?id=${item.GGID}&category=${item.ZYLB}&type=${item.JYFS}`; const desc = await cache.tryGet(pageUrl, async () => { - let desc = await got({ - method: 'get', - url: pageUrl, - }).json(); + let desc = await ofetch(pageUrl); desc = desc.queryNoticeContent.GGNR; desc = desc.replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"'); diff --git a/lib/routes/zjgtjy/namespace.ts b/lib/routes/zjgtjy/namespace.ts index 36caf6f7fc48a8..cbe164abe36c5b 100644 --- a/lib/routes/zjgtjy/namespace.ts +++ b/lib/routes/zjgtjy/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '浙江省土地使用权网上交易系统', url: 'zjgtjy.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/zjol/namespace.ts b/lib/routes/zjol/namespace.ts index a9276423ae9d9b..197f177514dd68 100644 --- a/lib/routes/zjol/namespace.ts +++ b/lib/routes/zjol/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '浙江在线', url: 'zjol.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/zjol/paper.ts b/lib/routes/zjol/paper.ts index d1d988312eb83d..3462674a2679a9 100644 --- a/lib/routes/zjol/paper.ts +++ b/lib/routes/zjol/paper.ts @@ -22,8 +22,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 浙江日报 | 钱江晚报 | 美术报 | 浙江老年报 | 浙江法制报 | 江南游报 | - | -------- | -------- | ------ | ---------- | ---------- | -------- | - | zjrb | qjwb | msb | zjlnb | zjfzb | jnyb |`, +| -------- | -------- | ------ | ---------- | ---------- | -------- | +| zjrb | qjwb | msb | zjlnb | zjfzb | jnyb |`, }; async function handler(ctx) { diff --git a/lib/routes/zju/career/index.ts b/lib/routes/zju/career/index.ts index f934986a7d9c2f..ce1a06bc60d5b9 100644 --- a/lib/routes/zju/career/index.ts +++ b/lib/routes/zju/career/index.ts @@ -30,8 +30,8 @@ export const route: Route = { maintainers: ['Caicailiushui'], handler, description: `| 新闻动态 | 活动通知 | 学院通知 | 告示通知 | - | -------- | -------- | -------- | -------- | - | 1 | 2 | 3 | 4 |`, +| -------- | -------- | -------- | -------- | +| 1 | 2 | 3 | 4 |`, }; async function handler(ctx) { diff --git a/lib/routes/zju/cst/custom.ts b/lib/routes/zju/cst/custom.ts index 26c0bc5ecd7af8..c2b329e00ae7fd 100644 --- a/lib/routes/zju/cst/custom.ts +++ b/lib/routes/zju/cst/custom.ts @@ -48,8 +48,8 @@ export const route: Route = { maintainers: ['zwithz'], handler, description: `| 全部通知 | 招生信息 | 教务管理 | 论文管理 | 思政工作 | 评奖评优 | 实习就业 | 国际实习 | 国内合作科研 | 国际合作科研 | 校园服务 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ------------ | ------------ | -------- | - | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ------------ | ------------ | -------- | +| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | #### 自定义聚合通知 {#zhe-jiang-da-xue-ruan-jian-xue-yuan-zi-ding-yi-ju-he-tong-zhi}`, }; diff --git a/lib/routes/zju/cst/index.ts b/lib/routes/zju/cst/index.ts index ad4af0678c1269..6d117693e28eb9 100644 --- a/lib/routes/zju/cst/index.ts +++ b/lib/routes/zju/cst/index.ts @@ -37,7 +37,7 @@ async function getPage(id) { return { title: item.find('a').text(), pubDate: parseDate(item.find('.fr').text()), - link: new URL(item.find('a').attr('href'), res.url).href, + link: new URL(item.find('a').attr('href'), host).href, }; }) .get() @@ -59,8 +59,8 @@ export const route: Route = { }, name: '软件学院', description: `| 全部通知 | 招生信息 | 教务管理 | 论文管理 | 思政工作 | 评奖评优 | 实习就业 | 国际实习 | 国内合作科研 | 国际合作科研 | 校园服务 | - | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ------------ | ------------ | -------- | - | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |`, +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ------------ | ------------ | -------- | +| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |`, maintainers: ['yonvenne', 'zwithz'], handler, }; diff --git a/lib/routes/zju/grs/index.ts b/lib/routes/zju/grs/index.ts index afe65543bee35a..3be7ba620068eb 100644 --- a/lib/routes/zju/grs/index.ts +++ b/lib/routes/zju/grs/index.ts @@ -31,8 +31,8 @@ export const route: Route = { maintainers: ['Caicailiushui'], handler, description: `| 全部公告 | 教学管理 | 各类资助 | 学科建设 | 海外交流 | - | -------- | -------- | -------- | -------- | -------- | - | 1 | 2 | 3 | 4 | 5 |`, +| -------- | -------- | -------- | -------- | -------- | +| 1 | 2 | 3 | 4 | 5 |`, }; async function handler(ctx) { diff --git a/lib/routes/zju/namespace.ts b/lib/routes/zju/namespace.ts index f73eb4aab09d7b..9961052f9c6061 100644 --- a/lib/routes/zju/namespace.ts +++ b/lib/routes/zju/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '浙江大学', url: 'physics.zju.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/zju/physics/index.ts b/lib/routes/zju/physics/index.ts index 88110e0061dd68..a31aef62e30dc8 100644 --- a/lib/routes/zju/physics/index.ts +++ b/lib/routes/zju/physics/index.ts @@ -40,8 +40,8 @@ export const route: Route = { maintainers: ['Caicailiushui'], handler, description: `| 本院动态 | 科研进展 | 研究生教育最新消息 | - | -------- | -------- | ------------------ | - | 1 | 2 | 3 |`, +| -------- | -------- | ------------------ | +| 1 | 2 | 3 |`, }; async function handler(ctx) { diff --git a/lib/routes/zjut/cs/index.ts b/lib/routes/zjut/cs/index.ts new file mode 100644 index 00000000000000..bc55eb701e1ac3 --- /dev/null +++ b/lib/routes/zjut/cs/index.ts @@ -0,0 +1,105 @@ +import { Data, Route } from '@/types'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import timezone from '@/utils/timezone'; + +const rootUrl = 'https://cs.zjut.edu.cn/jsp/newsclass.jsp?wcId='; +const host = 'cs.zjut.edu.cn'; + +export const route: Route = { + path: '/cs/:type', + categories: ['university'], + example: '/zjut/cs/54', + parameters: { type: '分类,见下表' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '浙江工业大学计算机科学与技术学院、软件学院', + maintainers: ['zhullyb'], + url: 'cs.zjut.edu.cn', + handler, + radar: [ + { + source: ['cs.zjut.edu.cn/jsp/newsclass.jsp'], + target: '/cs/:type', + }, + ], + description: `| 新闻资讯 | 学术动态 | 通知公告 | +| ------- | ------- | ------- | +| 54 | 55 | 53 |`, +}; + +async function handler(ctx) { + const type = Number.parseInt(ctx.req.param('type')); + const response = await ofetch(rootUrl + type); + const $ = load(response); + + const list = $('dl.news') + .toArray() + .map((item) => { + const cheerioItem = $(item); + const a = cheerioItem.find('a'); + + try { + const title = a.text() || ''; + let link = a.attr('href'); + if (!link) { + link = ''; + } else if (!link.startsWith('http')) { + link = 'https://' + host + '/jsp/' + link; + } + const pubDate = timezone(parseDate(cheerioItem.find('.datetime').text().slice(1, -1)), +8); + + return { + title, + link, + pubDate, + }; + } catch { + return { + title: '', + link: '', + pubDate: Date.now(), + }; + } + }) + .filter((item) => item.title && item.link); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const newItem = { + ...item, + description: '', + }; + if (host === new URL(item.link).hostname) { + if (new URL(item.link).pathname.startsWith('/upload')) { + // 链接为一个文件,直接返回链接 + newItem.description = item.link; + } else { + const response = await ofetch(item.link); + const $ = load(response); + newItem.description = $('div.news1content').html() || ''; + } + } else { + // 涉及到其他站点,不方便做统一的 html 解析,直接返回链接 + newItem.description = item.link; + } + return newItem; + }) + ) + ); + + return { + title: $('li#classname').text() + ' - 浙江工业大学计算机科学与技术学院、软件学院', + link: rootUrl + type, + item: items, + } as Data; +} diff --git a/lib/routes/zjut/jwc/index.ts b/lib/routes/zjut/jwc/index.ts new file mode 100644 index 00000000000000..1708484809d9d3 --- /dev/null +++ b/lib/routes/zjut/jwc/index.ts @@ -0,0 +1,117 @@ +import { Data, Route } from '@/types'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import timezone from '@/utils/timezone'; + +const rootUrl = 'http://www.jwc.zjut.edu.cn/'; +const host = 'www.jwc.zjut.edu.cn'; + +export const route: Route = { + path: '/jwc/:type', + categories: ['university'], + example: '/zjut/jwc/1839', + parameters: { type: '分类,见下表' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '浙江工业大学教务处', + maintainers: ['zhullyb'], + url: 'www.jwc.zjut.edu.cn', + handler, + radar: [ + { + source: ['www.jwc.zjut.edu.cn/:type/list.htm'], + target: '/jwc/:type', + }, + ], + description: `| 板块 | 参数 | +| ------- | ------- | +| 新闻动态 | 1838 | +| 课程思政 | 1842 | +| 校内动态 | 2613 | +| 学习思考 | 2614 | +| 成果展示 | 2615 | +| 媒体聚焦 | 2616 | +| 制度文件 | 2617 | +| 教学运行 | 1849 | +| 实践竞赛 | 1850 | +| 留学生Notice | 1851 | +| 项目申报 | 1852 | +| 学籍管理 | 1853 | +| 办事指南 | 1839 |`, +}; + +async function handler(ctx) { + const type = Number.parseInt(ctx.req.param('type')); + const response = await ofetch(rootUrl + type + '/list.htm'); + const $ = load(response); + + const list = $('.news.clearfix') + .toArray() + .map((item) => { + const cheerioItem = $(item); + const a = cheerioItem.find('a'); + + try { + const title = a.attr('title') || ''; + let link = a.attr('href'); + if (!link) { + link = ''; + } else if (!link.startsWith('http')) { + link = rootUrl.slice(0, -1) + link; + } + const pubDate = timezone(parseDate(cheerioItem.find('.news_meta').text()), +8); + + return { + title, + link, + pubDate, + }; + } catch { + return { + title: '', + link: '', + pubDate: Date.now(), + }; + } + }) + .filter((item) => item.title && item.link); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const newItem = { + ...item, + description: '', + }; + if (host === new URL(item.link).hostname) { + if (new URL(item.link).pathname.startsWith('/_upload')) { + // 链接为一个文件,直接返回链接 + newItem.description = item.link; + } else { + const response = await ofetch(item.link); + const $ = load(response); + newItem.description = $('.wp_articlecontent').html() || ''; + } + } else { + // 涉及到其他站点,不方便做统一的 html 解析,直接返回链接 + newItem.description = item.link; + } + return newItem; + }) + ) + ); + + return { + title: $('head > title').text() + ' - 浙江工业大学教务处', + link: rootUrl + type + '/list.htm', + item: items, + } as Data; +} diff --git a/lib/routes/zjut/namespace.ts b/lib/routes/zjut/namespace.ts index 6f7d00f6e4ed3e..4670a812eaa116 100644 --- a/lib/routes/zjut/namespace.ts +++ b/lib/routes/zjut/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '浙江工业大学', url: 'www.zjut.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/zjut/www/index.ts b/lib/routes/zjut/www/index.ts new file mode 100644 index 00000000000000..4f4fb3ad1f74d5 --- /dev/null +++ b/lib/routes/zjut/www/index.ts @@ -0,0 +1,120 @@ +import { Data, Route } from '@/types'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import timezone from '@/utils/timezone'; + +const rootUrl = 'https://www.zjut.edu.cn/'; +const host = 'www.zjut.edu.cn'; + +export const route: Route = { + path: '/www/:type', + categories: ['university'], + example: '/zjut/www/4528', + parameters: { type: '分类,见下表' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '浙江工业大学首页', + maintainers: ['zhullyb'], + url: 'www.zjut.edu.cn', + handler, + radar: [ + { + source: ['www.zjut.edu.cn/:type/list.htm'], + target: '/www/:type', + }, + ], + description: `| 板块 | 参数 | +| ------- | ------- | +| 学术动态 | xsdt_4662 | +| 三创·人物 | 4527 | +| 通知公告 | 4528 | +| 美誉工大 | 5389 | +| 智库工大 | 5390 | +| 工大校历 | 4520 | +| 校区班车 | xqbc |`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type'); + const response = await ofetch(rootUrl + type + '/list.htm'); + const $ = load(response); + + const list = $('li.news.clearfix') + .toArray() + .map((item) => { + const cheerioItem = $(item); + const a = cheerioItem.find('a'); + + try { + const title = a.text() || ''; + let link = a.attr('href'); + if (!link) { + link = ''; + } else if (!link.startsWith('http')) { + link = rootUrl.slice(0, -1) + link; + } + const dateText = cheerioItem.find('.news_meta').text(); + if (!dateText) { + // This should not be included, return an empty item to filter out + return { + title: '', + link: '', + pubDate: Date.now(), + }; + } + const pubDate = timezone(parseDate(dateText), +8); + + return { + title, + link, + pubDate, + }; + } catch { + return { + title: '', + link: '', + pubDate: Date.now(), + }; + } + }) + .filter((item) => item.title && item.link); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const newItem = { + ...item, + description: '', + }; + if (host === new URL(item.link).hostname) { + if (new URL(item.link).pathname.startsWith('/upload')) { + // 链接为一个文件,直接返回链接 + newItem.description = item.link; + } else { + const response = await ofetch(item.link); + const $ = load(response); + newItem.description = $('div.wp_articlecontent').html() || ''; + } + } else { + // 涉及到其他站点,不方便做统一的 html 解析,直接返回链接 + newItem.description = item.link; + } + return newItem; + }) + ) + ); + + return { + title: $('head > title').text() + ' - 浙江工业大学', + link: rootUrl + type, + item: items, + } as Data; +} diff --git a/lib/routes/zjuvag/namespace.ts b/lib/routes/zjuvag/namespace.ts index 911d9abb17453e..4109fe29e619fd 100644 --- a/lib/routes/zjuvag/namespace.ts +++ b/lib/routes/zjuvag/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '浙江大学可视分析小组', url: 'zjuvag.org', + lang: 'zh-CN', }; diff --git a/lib/routes/zodgame/namespace.ts b/lib/routes/zodgame/namespace.ts index a9c4ce7bbcafe5..c806e92899f2b3 100644 --- a/lib/routes/zodgame/namespace.ts +++ b/lib/routes/zodgame/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ZodGame', url: 'zodgame.xyz', + lang: 'en', }; diff --git a/lib/routes/zotero/namespace.ts b/lib/routes/zotero/namespace.ts index ad2b5b6dd23432..5f81425d8f8540 100644 --- a/lib/routes/zotero/namespace.ts +++ b/lib/routes/zotero/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Zotero', url: 'zotero.org', + lang: 'en', }; diff --git a/lib/routes/zrblog/namespace.ts b/lib/routes/zrblog/namespace.ts index 6d9be6adbe9c73..8171cf4eab5b5e 100644 --- a/lib/routes/zrblog/namespace.ts +++ b/lib/routes/zrblog/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '赵容部落', url: 'zrblog.net', + lang: 'zh-CN', }; diff --git a/lib/routes/zsxq/group.ts b/lib/routes/zsxq/group.ts index 6bb7d991600621..6b743266b748ec 100644 --- a/lib/routes/zsxq/group.ts +++ b/lib/routes/zsxq/group.ts @@ -31,8 +31,8 @@ export const route: Route = { }, handler, description: `| all | digests | by_owner | questions | tasks | - | ---- | ------ | --------- | -------- | ------ | - | 最新 | 精华 | 只看星主 | 问答 | 作业 |`, +| ---- | ------ | --------- | -------- | ------ | +| 最新 | 精华 | 只看星主 | 问答 | 作业 |`, }; async function handler(ctx: Context): Promise<Data> { diff --git a/lib/routes/zsxq/namespace.ts b/lib/routes/zsxq/namespace.ts index bca329d26d85d3..0e541958e5f997 100644 --- a/lib/routes/zsxq/namespace.ts +++ b/lib/routes/zsxq/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '知识星球', url: 'zsxq.com', + lang: 'zh-CN', }; diff --git a/lib/routes/zsxq/types.ts b/lib/routes/zsxq/types.ts index 5ec0851cef9e79..cefce350454bf0 100644 --- a/lib/routes/zsxq/types.ts +++ b/lib/routes/zsxq/types.ts @@ -63,6 +63,13 @@ export interface QATopic extends BasicTopic { question: { images?: TopicImage[]; text?: string; + owner?: { + avatar_url: string; + description: string; + location: string; + name: string; + user_id: number; + }; }; } @@ -140,6 +147,8 @@ export interface TopicImage { export type Topic = TalkTopic | QATopic | TaskTopic | SolutionTopic; +export type ResponseData = UserInfo | GroupInfo | Topic[]; + export type UserInfoResponse = BasicResponse<UserInfo>; export type GroupInfoResponse = BasicResponse<GroupInfo>; diff --git a/lib/routes/zsxq/utils.ts b/lib/routes/zsxq/utils.ts index 7c659a07f74258..82e21abcb9cd4e 100644 --- a/lib/routes/zsxq/utils.ts +++ b/lib/routes/zsxq/utils.ts @@ -1,10 +1,10 @@ import got from '@/utils/got'; -import type { TopicImage, Topic, BasicResponse } from './types'; +import type { TopicImage, Topic, BasicResponse, ResponseData } from './types'; import { parseDate } from '@/utils/parse-date'; import { config } from '@/config'; import type { DataItem } from '@/types'; -export async function customFetch<T extends BasicResponse<any>>(path: string, retryCount = 0): Promise<T['resp_data']> { +export async function customFetch<T extends BasicResponse<ResponseData>>(path: string, retryCount = 0): Promise<T['resp_data']> { const apiUrl = 'https://api.zsxq.com/v2'; const response = await got(apiUrl + path, { @@ -38,6 +38,7 @@ export function generateTopicDataItem(topics: Topic[]): DataItem[] { return topics.map((topic) => { let description: string | undefined; let title = ''; + const url = `https://wx.zsxq.com/topic/${topic.topic_id}`; switch (topic.type) { case 'talk': title = topic.talk?.text?.split('\n')[0] ?? '文章'; @@ -46,8 +47,9 @@ export function generateTopicDataItem(topics: Topic[]): DataItem[] { case 'q&a': title = topic.question?.text?.split('\n')[0] ?? '问答'; description = parseTopicContent(topic.question?.text, topic.question?.images); + description = `<blockquote>${String(topic.question?.owner?.name ?? '匿名用户')} 提问:${description}</blockquote>`; if (topic.answered) { - description += '<br><br>'; + description += '<br>' + topic.answer?.owner.name + ' 回答:<br><br>'; description += parseTopicContent(topic.answer?.text, topic.answer?.images); } break; @@ -65,6 +67,7 @@ export function generateTopicDataItem(topics: Topic[]): DataItem[] { title: topic.title ?? title, description, pubDate: parseDate(topic.create_time), + link: url, }; }); } diff --git a/lib/routes/zuel/namespace.ts b/lib/routes/zuel/namespace.ts index 91d26273c4b302..aaf67ee1717a54 100644 --- a/lib/routes/zuel/namespace.ts +++ b/lib/routes/zuel/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中南财经政法大学', url: 'wap.zuel.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/zuvio/namespace.ts b/lib/routes/zuvio/namespace.ts index a61932d4735b09..f8454defd4949c 100644 --- a/lib/routes/zuvio/namespace.ts +++ b/lib/routes/zuvio/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Zuvio', url: 'irs.zuvio.com.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/zyshow/namespace.ts b/lib/routes/zyshow/namespace.ts index cc8275ae613eeb..823e7c410dd2f6 100644 --- a/lib/routes/zyshow/namespace.ts +++ b/lib/routes/zyshow/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '综艺秀', url: 'zyshow.net', + lang: 'zh-CN', }; diff --git a/lib/routes/zyw/hot.ts b/lib/routes/zyw/hot.ts index 6e5c2e6a0e85d7..e7a0da314f3acc 100644 --- a/lib/routes/zyw/hot.ts +++ b/lib/routes/zyw/hot.ts @@ -17,12 +17,12 @@ export const route: Route = { name: '今日热榜', maintainers: ['nczitzk'], handler, - description: `:::tip + description: `::: tip 全部站点请见 [此处](https://hot.zyw.asia/#/list) - ::: +::: - | 哔哩哔哩 | 微博 | 知乎 | 36 氪 | 百度 | 少数派 | IT 之家 | 澎湃新闻 | 今日头条 | 百度贴吧 | 稀土掘金 | 腾讯新闻 | - | -------- | ---- | ---- | ----- | ---- | ------ | ------- | -------- | -------- | -------- | -------- | -------- |`, +| 哔哩哔哩 | 微博 | 知乎 | 36 氪 | 百度 | 少数派 | IT 之家 | 澎湃新闻 | 今日头条 | 百度贴吧 | 稀土掘金 | 腾讯新闻 | +| -------- | ---- | ---- | ----- | ---- | ------ | ------- | -------- | -------- | -------- | -------- | -------- |`, }; async function handler(ctx) { diff --git a/lib/routes/zyw/namespace.ts b/lib/routes/zyw/namespace.ts index eb270d22d52782..79b34167a3e4f0 100644 --- a/lib/routes/zyw/namespace.ts +++ b/lib/routes/zyw/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'zyw', url: 'hot.zyw.asia', + lang: 'zh-CN', }; diff --git a/lib/setup.test.ts b/lib/setup.test.ts index 80c9b2141f5fcc..d4c65a009f949f 100644 --- a/lib/setup.test.ts +++ b/lib/setup.test.ts @@ -31,7 +31,7 @@ const server = setupServer( choices: [ { message: { - content: 'Summary of the article.', + content: 'AI processed content.', }, }, ], diff --git a/lib/types.ts b/lib/types.ts index a45dc653710495..5fb66bb4ac2e62 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -3,6 +3,7 @@ import type { Context } from 'hono'; // Make sure it's synchronise with scripts/workflow/data.ts // and lib/routes/rsshub/routes.ts type Category = + | 'popular' | 'social-media' | 'new-media' | 'traditional-media' @@ -34,7 +35,13 @@ export type DataItem = { pubDate?: number | string | Date; link?: string; category?: string[]; - author?: string | { name: string }[]; + author?: + | string + | { + name: string; + url?: string; + avatar?: string; + }[]; doi?: string; guid?: string; id?: string; @@ -45,7 +52,7 @@ export type DataItem = { image?: string; banner?: string; updated?: number | string | Date; - language?: string; + language?: Language; enclosure_url?: string; enclosure_type?: string; enclosure_title?: string; @@ -53,6 +60,13 @@ export type DataItem = { itunes_duration?: number | string; itunes_item_image?: string; media?: Record<string, Record<string, string>>; + attachments?: { + url: string; + mime_type: string; + title?: string; + size_in_bytes?: number; + duration_in_seconds?: number; + }[]; _extra?: Record<string, any> & { links?: { @@ -71,18 +85,126 @@ export type Data = { allowEmpty?: boolean; image?: string; author?: string; - language?: string; + language?: Language; feedLink?: string; lastBuildDate?: string; itunes_author?: string; itunes_category?: string; itunes_explicit?: string | boolean; id?: string; - + icon?: string; + logo?: string; atomlink?: string; ttl?: number; }; +type Language = + | 'af' + | 'sq' + | 'eu' + | 'be' + | 'bg' + | 'ca' + | 'zh-CN' + | 'zh-TW' + | 'zh-HK' + | 'hr' + | 'cs' + | 'ar-DZ' + | 'ar-SA' + | 'ar-MA' + | 'ar-IQ' + | 'ar-KW' + | 'ar-TN' + | 'da' + | 'nl' + | 'nl-be' + | 'nl-nl' + | 'en' + | 'en-au' + | 'en-bz' + | 'en-ca' + | 'en-ie' + | 'en-jm' + | 'en-nz' + | 'en-ph' + | 'en-za' + | 'en-tt' + | 'en-gb' + | 'en-us' + | 'en-zw' + | 'et' + | 'fo' + | 'fi' + | 'fr' + | 'fr-be' + | 'fr-ca' + | 'fr-fr' + | 'fr-lu' + | 'fr-mc' + | 'fr-ch' + | 'gl' + | 'gd' + | 'de' + | 'de-at' + | 'de-de' + | 'de-li' + | 'de-lu' + | 'de-ch' + | 'el' + | 'haw' + | 'hu' + | 'is' + | 'in' + | 'ga' + | 'it' + | 'it-it' + | 'it-ch' + | 'ja' + | 'ko' + | 'mk' + | 'no' + | 'pl' + | 'pt' + | 'pt-br' + | 'pt-pt' + | 'ro' + | 'ro-mo' + | 'ro-ro' + | 'ru' + | 'ru-mo' + | 'ru-ru' + | 'sr' + | 'sk' + | 'sl' + | 'es' + | 'es-ar' + | 'es-bo' + | 'es-cl' + | 'es-co' + | 'es-cr' + | 'es-do' + | 'es-ec' + | 'es-sv' + | 'es-gt' + | 'es-hn' + | 'es-mx' + | 'es-ni' + | 'es-pa' + | 'es-py' + | 'es-pe' + | 'es-pr' + | 'es-es' + | 'es-uy' + | 'es-ve' + | 'sv' + | 'sv-fi' + | 'sv-se' + | 'tr' + | 'uk' + | 'ne' + | 'other'; + // namespace interface NamespaceItem { /** @@ -105,6 +227,11 @@ interface NamespaceItem { * Hints and additional explanations for users using this namespace, it will be inserted into the documentation */ description?: string; + + /** + * Main Language of the namespace + */ + lang?: Language; } interface Namespace extends NamespaceItem { @@ -118,6 +245,15 @@ interface Namespace extends NamespaceItem { export type { Namespace }; +export enum ViewType { + Articles = 0, + SocialMedia = 1, + Pictures = 2, + Videos = 3, + Audios = 4, + Notifications = 5, +} + // route interface RouteItem { /** @@ -144,7 +280,7 @@ interface RouteItem { /** * The handler function of the route */ - handler: (ctx: Context) => Promise<Data> | Data; + handler: (ctx: Context) => Promise<Data | null> | Data | null; /** * An example URL of the route @@ -154,7 +290,18 @@ interface RouteItem { /** * The description of the route parameters */ - parameters?: Record<string, string>; + parameters?: Record< + string, + | string + | { + description: string; + default?: string; + options?: { + value: string; + label: string; + }[]; + } + >; /** * Hints and additional explanations for users using this route, it will be appended after the route component, supports markdown @@ -205,12 +352,17 @@ interface RouteItem { * The [RSSHub-Radar](https://github.com/DIYgod/RSSHub-Radar) rule of the route */ radar?: RadarItem[]; + + /** + * The [Follow](https://github.com/RSSNext/follow) default view of the route, default to `ViewType.Articles` + */ + view?: ViewType; } interface Route extends RouteItem { - ja?: NamespaceItem; - zh?: NamespaceItem; - 'zh-TW'?: NamespaceItem; + ja?: RouteItem; + zh?: RouteItem; + 'zh-TW'?: RouteItem; } export type { Route }; @@ -241,6 +393,7 @@ export type RadarItem = { * * Using `target` as a function is deprecated in RSSHub-Radar 2.0.19 * @see https://github.com/DIYgod/RSSHub-Radar/commit/5a97647f900bb2bca792787a322b2b1ca512e40b#diff-f84e3c1e16af314bc4ed7c706d7189844663cde9b5142463dc5c0db34c2e8d54L10 + * @see https://github.com/DIYgod/RSSHub-Radar/issues/692 */ target?: | string @@ -253,3 +406,9 @@ export type RadarItem = { document: Document ) => string); }; + +export type RadarDomain = { + _name: string; +} & { + [subdomain: string]: RadarItem[]; +}; diff --git a/lib/utils/cache/index.ts b/lib/utils/cache/index.ts index 422f7e3d5cd77a..5c1750c460a05f 100644 --- a/lib/utils/cache/index.ts +++ b/lib/utils/cache/index.ts @@ -63,6 +63,14 @@ if (config.cache.type === 'redis') { // plz, write these tips in comments! export default { ...cacheModule, + /** + * Try to get the cache. If the cache does not exist, the `getValueFunc` function will be called to get the data, and the data will be cached. + * @param key The key used to store and retrieve the cache. You can use `:` as a separator to create a hierarchy. + * @param getValueFunc A function that returns data to be cached when a cache miss occurs. + * @param maxAge The maximum age of the cache in seconds. This should left to the default value in most cases which is `CACHE_CONTENT_EXPIRE`. + * @param refresh Whether to renew the cache expiration time when the cache is hit. `true` by default. + * @returns + */ tryGet: async (key: string, getValueFunc: () => Promise<string | Record<string, any>>, maxAge = config.cache.contentExpire, refresh = true) => { if (typeof key !== 'string') { throw new TypeError('Cache key must be a string'); diff --git a/lib/utils/cache/redis.ts b/lib/utils/cache/redis.ts index 7830b49febd6d8..a3ba5850c3e8ce 100644 --- a/lib/utils/cache/redis.ts +++ b/lib/utils/cache/redis.ts @@ -64,7 +64,7 @@ export default { } if (key) { if (maxAge !== config.cache.contentExpire) { - // Only set cacheTtlKey if maxAge !== contentExpire + // intentionally store the cache ttl if it is not the default value clients.redisClient.set(getCacheTtlKey(key), maxAge, 'EX', maxAge); } return clients.redisClient.set(key, value, 'EX', maxAge); // setMode: https://redis.io/commands/set diff --git a/lib/utils/camelcase-keys.spec.ts b/lib/utils/camelcase-keys.spec.ts new file mode 100644 index 00000000000000..af413905ddb2b0 --- /dev/null +++ b/lib/utils/camelcase-keys.spec.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from 'vitest'; + +import { camelcase, camelcaseKeys } from './camelcase-keys'; + +describe('test camelcase keys', () => { + it('case 1 normal', () => { + const obj = { + tool: 'too', + tool_name: 'too_name', + + a_b: 1, + a: 1, + b: { + c_d: 1, + }, + }; + + expect(camelcaseKeys(obj)).toStrictEqual({ + tool: 'too', + toolName: 'too_name', + + aB: 1, + a: 1, + b: { + cD: 1, + }, + }); + }); + + it('case 2: key has number', () => { + const obj = { + b147da0eaecbea00aeb62055: { + data: {}, + }, + a_c11ab_Ac: [ + { + a_b: 1, + }, + 1, + ], + }; + + expect(camelcaseKeys(obj)).toStrictEqual({ + b147da0eaecbea00aeb62055: { + data: {}, + }, + aC11abAc: [ + { + aB: 1, + }, + 1, + ], + }); + }); + + it('case 3: not a object', () => { + const value = 1; + expect(camelcaseKeys(value)).toBe(value); + }); + + it('case 4: nullable value', () => { + let value = null as any; + expect(camelcaseKeys(value)).toBe(value); + + value = undefined; + expect(camelcaseKeys(value)).toBe(value); + + value = Number.NaN; + expect(camelcaseKeys(value)).toBe(value); + }); + + it('case 5: array', () => { + const arr = [ + { + a_b: 1, + }, + null, + undefined, + +0, + -0, + Number.POSITIVE_INFINITY, + { + a_b: 1, + }, + ]; + + expect(camelcaseKeys(arr)).toStrictEqual([ + { + aB: 1, + }, + null, + undefined, + +0, + -0, + Number.POSITIVE_INFINITY, + { + aB: 1, + }, + ]); + }); + + it('case 6: filter out mongo id', () => { + const obj = { + _id: '123', + a_b: 1, + collections: { + posts: { + '661bb93307d35005ba96731b': {}, + }, + }, + }; + + expect(camelcaseKeys(obj)).toStrictEqual({ + id: '123', + aB: 1, + collections: { + posts: { + '661bb93307d35005ba96731b': {}, + }, + }, + }); + }); + + it('case 7: start with underscore should not camelcase', () => { + expect(camelcase('_id')).toBe('id'); + }); +}); diff --git a/lib/utils/camelcase-keys.ts b/lib/utils/camelcase-keys.ts new file mode 100644 index 00000000000000..9797d698ea1e4a --- /dev/null +++ b/lib/utils/camelcase-keys.ts @@ -0,0 +1,27 @@ +const isObject = (obj: any) => obj && typeof obj === 'object'; +const isPlainObject = (obj: any) => isObject(obj) && Object.prototype.toString.call(obj) === '[object Object]' && Object.getPrototypeOf(obj) === Object.prototype; + +/** + * A simple camelCase function that only handles strings, but not handling symbol, date, or other complex case. + * If you need to handle more complex cases, please use camelcase-keys package. + */ +export const camelcaseKeys = <T = any>(obj: any): T => { + if (Array.isArray(obj)) { + return obj.map((x) => camelcaseKeys(x)) as any; + } + + if (isPlainObject(obj)) { + return Object.keys(obj).reduce((result: any, key) => { + const nextKey = isMongoId(key) ? key : camelcase(key); + result[nextKey] = camelcaseKeys(obj[key]); + return result; + }, {}) as any; + } + + return obj; +}; + +export function camelcase(str: string) { + return str.replace(/^_+/, '').replaceAll(/([_-][a-z])/gi, ($1) => $1.toUpperCase().replace('-', '').replace('_', '')); +} +const isMongoId = (id: string) => id.length === 24 && /^[\dA-Fa-f]{24}$/.test(id); diff --git a/lib/utils/common-utils.ts b/lib/utils/common-utils.ts index 6ad4120d0e964f..7072132b999d58 100644 --- a/lib/utils/common-utils.ts +++ b/lib/utils/common-utils.ts @@ -39,6 +39,7 @@ const getLocalhostAddress = () => { .filter((iface) => iface?.family === 'IPv4' && !iface.internal) .map((iface) => iface?.address) .filter(Boolean); + address.push('[::]'); return address; }; diff --git a/lib/utils/got.ts b/lib/utils/got.ts index 1f623d9d6c256c..372fe125b33e51 100644 --- a/lib/utils/got.ts +++ b/lib/utils/got.ts @@ -1,5 +1,6 @@ import { destr } from 'destr'; import ofetch from '@/utils/ofetch'; +import { getSearchParamsString } from './helpers'; const getFakeGot = (defaultOptions?: any) => { const fakeGot = (request, options?: any) => { @@ -35,7 +36,7 @@ const getFakeGot = (defaultOptions?: any) => { delete options.form; } if (options?.searchParams) { - request += '?' + new URLSearchParams(options.searchParams).toString(); + request += '?' + getSearchParamsString(options.searchParams); delete options.searchParams; } diff --git a/lib/utils/helpers.test.ts b/lib/utils/helpers.test.ts index 4870e54fc58540..23ac83ca0cb64d 100644 --- a/lib/utils/helpers.test.ts +++ b/lib/utils/helpers.test.ts @@ -1,8 +1,20 @@ import { describe, expect, it } from 'vitest'; -import { getRouteNameFromPath } from '@/utils/helpers'; +import { getRouteNameFromPath, getSearchParamsString } from '@/utils/helpers'; describe('helpers', () => { it('getRouteNameFromPath', () => { expect(getRouteNameFromPath('/test/1')).toBe('test'); }); + + it('getSearchParamsString', () => { + expect(getSearchParamsString({ a: 1, b: 2 })).toBe('a=1&b=2'); + expect(getSearchParamsString({ a: 1, b: undefined })).toBe('a=1'); + expect(getSearchParamsString({ a: undefined })).toBe(''); + expect(getSearchParamsString({})).toBe(''); + + const searchParams = new URLSearchParams(); + searchParams.append('ids[]', '1'); + searchParams.append('ids[]', '2'); + expect(getSearchParamsString(searchParams)).toBe('ids%5B%5D=1&ids%5B%5D=2'); + }); }); diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts index ff78e73a184940..bc222464e97060 100644 --- a/lib/utils/helpers.ts +++ b/lib/utils/helpers.ts @@ -1,5 +1,6 @@ import { fileURLToPath } from 'url'; import path from 'node:path'; +import { stringifyQuery } from 'ufo'; export const getRouteNameFromPath = (path: string) => { const p = path.split('/').filter(Boolean); @@ -30,3 +31,12 @@ export const getCurrentPath = (metaUrl: string) => { const __filename = path.join(fileURLToPath(metaUrl)); return path.dirname(__filename); }; + +function isPureObject(o: any) { + return Object.prototype.toString.call(o) === '[object Object]'; +} + +export function getSearchParamsString(searchParams: any) { + const searchParamsString = isPureObject(searchParams) ? stringifyQuery(searchParams) : null; + return searchParamsString ?? new URLSearchParams(searchParams).toString(); +} diff --git a/lib/utils/ofetch.ts b/lib/utils/ofetch.ts index 06f77e379232ae..bb344318369862 100644 --- a/lib/utils/ofetch.ts +++ b/lib/utils/ofetch.ts @@ -1,17 +1,39 @@ import { createFetch } from 'ofetch'; import { config } from '@/config'; import logger from '@/utils/logger'; +import { register } from 'node-network-devtools'; + +config.enableRemoteDebugging && process.env.NODE_ENV === 'dev' && register(); const rofetch = createFetch().create({ + retryStatusCodes: [400, 408, 409, 425, 429, 500, 502, 503, 504], retry: config.requestRetry, retryDelay: 1000, - timeout: config.requestTimeout, + // timeout: config.requestTimeout, + onResponseError({ request, response, options }) { + if (options.retry) { + logger.warn(`Request ${request} with error ${response.status} remaining retry attempts: ${options.retry}`); + if (!options.headers) { + options.headers = {}; + } + if (options.headers instanceof Headers) { + options.headers.set('x-prefer-proxy', '1'); + } else { + options.headers['x-prefer-proxy'] = '1'; + } + } + }, onRequestError({ request, error }) { logger.error(`Request ${request} fail: ${error}`); }, headers: { 'user-agent': config.ua, }, + onResponse({ request, response }) { + if (response.redirected) { + logger.http(`Redirecting to ${response.url} for ${request}`); + } + }, }); export default rofetch; diff --git a/lib/utils/otel/index.ts b/lib/utils/otel/index.ts new file mode 100644 index 00000000000000..7649f22dba8dde --- /dev/null +++ b/lib/utils/otel/index.ts @@ -0,0 +1,2 @@ +export * from './metric'; +export * from './trace'; diff --git a/lib/utils/otel/metric.ts b/lib/utils/otel/metric.ts new file mode 100644 index 00000000000000..97241ed76fea8a --- /dev/null +++ b/lib/utils/otel/metric.ts @@ -0,0 +1,67 @@ +import { resourceFromAttributes } from '@opentelemetry/resources'; +import { PrometheusExporter, PrometheusSerializer } from '@opentelemetry/exporter-prometheus'; +import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; +import { MeterProvider } from '@opentelemetry/sdk-metrics'; +import type { Attributes } from '@opentelemetry/api'; +import { config } from '@/config'; + +interface IMetricAttributes extends Attributes { + method: string; + path: string; + status: number; +} + +interface IHistogramAttributes extends IMetricAttributes { + unit: string; +} + +const METRIC_PREFIX = 'rsshub'; + +const exporter = new PrometheusExporter({}); + +const provider = new MeterProvider({ + resource: resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'rsshub', + }), + readers: [exporter], +}); + +const serializer = new PrometheusSerializer(); + +const meter = provider.getMeter('rsshub'); + +const requestTotal = meter.createCounter<IMetricAttributes>(`${METRIC_PREFIX}_request_total`); +const requestErrorTotal = meter.createCounter<IMetricAttributes>(`${METRIC_PREFIX}_request_error_total`); +const requestDurationSecondsBucket = meter.createHistogram<IHistogramAttributes>(`${METRIC_PREFIX}_request_duration_seconds_bucket`, { + advice: { + explicitBucketBoundaries: config.otel.seconds_bucket?.split(',').map(Number), + }, +}); +const request_duration_milliseconds_bucket = meter.createHistogram<IHistogramAttributes>(`${METRIC_PREFIX}_request_duration_milliseconds_bucket`, { + advice: { + explicitBucketBoundaries: config.otel.milliseconds_bucket?.split(',').map(Number), + }, +}); + +export const requestMetric = { + success: (value: number, attributes: IMetricAttributes) => { + requestTotal.add(1, attributes); + request_duration_milliseconds_bucket.record(value, { unit: 'millisecond', ...attributes }); + requestDurationSecondsBucket.record(value / 1000, { unit: 'second', ...attributes }); + }, + error: (attributes: IMetricAttributes) => { + requestErrorTotal.add(1, attributes); + }, +}; + +export const getContext = () => + new Promise<string>((resolve, reject) => { + exporter + .collect() + .then((value) => { + resolve(serializer.serialize(value.resourceMetrics)); + }) + .finally(() => { + reject(''); + }); + }); diff --git a/lib/utils/otel/trace.ts b/lib/utils/otel/trace.ts new file mode 100644 index 00000000000000..144602b1bb9667 --- /dev/null +++ b/lib/utils/otel/trace.ts @@ -0,0 +1,28 @@ +import { resourceFromAttributes } from '@opentelemetry/resources'; +import { BasicTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { trace } from '@opentelemetry/api'; + +const exporter = new OTLPTraceExporter({ + // optional OTEL_EXPORTER_OTLP_ENDPOINT=https://localhost:4318 +}); + +const provider = new BasicTracerProvider({ + resource: resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'rsshub', + }), + spanProcessors: [ + new BatchSpanProcessor(exporter, { + // The maximum queue size. After the size is reached spans are dropped. + maxQueueSize: 4096, + // The interval between two consecutive exports + scheduledDelayMillis: 30000, + }), + ], +}); + +trace.setGlobalTracerProvider(provider); + +export const tracer = provider.getTracer('rsshub'); +export const mainSpan = tracer.startSpan('main'); diff --git a/lib/utils/puppeteer-utils.test.ts b/lib/utils/puppeteer-utils.test.ts index edd261a74319e5..8916c6aad2c7d3 100644 --- a/lib/utils/puppeteer-utils.test.ts +++ b/lib/utils/puppeteer-utils.test.ts @@ -5,9 +5,9 @@ import type { Browser } from 'puppeteer'; let browser: Browser | null = null; -afterEach(() => { +afterEach(async () => { if (browser) { - browser.close(); + await browser.close(); browser = null; } diff --git a/lib/utils/puppeteer.test.ts b/lib/utils/puppeteer.test.ts index ce77809fa3a446..1fd8f1d5aef62e 100644 --- a/lib/utils/puppeteer.test.ts +++ b/lib/utils/puppeteer.test.ts @@ -4,12 +4,12 @@ import { type Browser } from 'puppeteer'; let browser: Browser | null = null; -afterEach(() => { +afterEach(async () => { if (browser) { // double insurance to close unclosed browser immediately after each test // if a test closure fails before it can close the browser, the browser process will probably be unclosed, // especially when the test unit is run through `npm run vitest puppeteer` - browser.close(); + await browser.close(); browser = null; } delete process.env.PROXY_URI; diff --git a/lib/utils/puppeteer.ts b/lib/utils/puppeteer.ts index 24c36163244490..fe261c11f3d56f 100644 --- a/lib/utils/puppeteer.ts +++ b/lib/utils/puppeteer.ts @@ -54,8 +54,8 @@ const outPuppeteer = async ( } : options )); - setTimeout(() => { - browser.close(); + setTimeout(async () => { + await browser.close(); }, 30000); return browser; diff --git a/lib/utils/request-rewriter.test.ts b/lib/utils/request-rewriter.test.ts index 44b1a37f4cb84f..e274128f354b28 100644 --- a/lib/utils/request-rewriter.test.ts +++ b/lib/utils/request-rewriter.test.ts @@ -134,4 +134,18 @@ describe('request-rewriter', () => { expect(options?.agent).toBeUndefined(); } }); + + it('rate limiter', async () => { + const time = Date.now(); + await Promise.all( + Array.from({ length: 20 }).map(async () => { + try { + await fetch('http://rsshub.test/headers'); + } catch { + // ignore + } + }) + ); + expect(Date.now() - time).toBeGreaterThan(1500); + }); }); diff --git a/lib/utils/request-rewriter/fetch.test.ts b/lib/utils/request-rewriter/fetch.test.ts new file mode 100644 index 00000000000000..e8de9773bc1332 --- /dev/null +++ b/lib/utils/request-rewriter/fetch.test.ts @@ -0,0 +1,94 @@ +import { getCurrentCell, setCurrentCell } from 'node-network-devtools'; +import { useCustomHeader } from './fetch'; +import { describe, beforeEach, afterEach, test, expect } from 'vitest'; + +const getInitRequest = () => + ({ + requestHeaders: {} as Record<string, string>, + id: '', + loadCallFrames: () => {}, + cookies: '', + requestData: '', + responseData: '', + responseHeaders: {}, + responseInfo: {}, + }) satisfies NonNullable<ReturnType<typeof getCurrentCell>>['request']; + +enum Env { + dev = 'dev', + production = 'production', + test = 'test', +} + +describe('useCustomHeader', () => { + let originalEnv: string; + + beforeEach(() => { + originalEnv = process.env.NODE_ENV || Env.test; + process.env.ENABLE_REMOTE_DEBUGGING = 'true'; + }); + + afterEach(() => { + process.env.NODE_ENV = originalEnv; + }); + + test('should register request with custom headers in dev environment', () => { + process.env.NODE_ENV = Env.dev; + + const headers = new Headers(); + const headerText = 'authorization'; + const headerValue = 'Bearer token'; + headers.set(headerText, headerValue); + + const req = getInitRequest(); + setCurrentCell({ + request: req, + pipes: [], + isAborted: false, + }); + + useCustomHeader(headers); + + const cell = getCurrentCell(); + expect(cell).toBeDefined(); + + let request = req; + if (cell) { + for (const { pipe } of cell.pipes) { + request = pipe(request); + } + } + + expect(request.requestHeaders[headerText]).toEqual(headerValue); + }); + + test('should not register request in non-dev environment', () => { + process.env.NODE_ENV = Env.production; + + const headers = new Headers(); + const headerText = 'content-type'; + const headerValue = 'application/json'; + + headers.set(headerText, headerValue); + const req = getInitRequest(); + + setCurrentCell({ + request: req, + pipes: [], + isAborted: false, + }); + useCustomHeader(headers); + + const cell = getCurrentCell(); + expect(cell).toBeDefined(); + + let request = req; + if (cell) { + for (const { pipe } of cell.pipes) { + request = pipe(request); + } + } + + expect(req.requestHeaders[headerText]).toBeUndefined(); + }); +}); diff --git a/lib/utils/request-rewriter/fetch.ts b/lib/utils/request-rewriter/fetch.ts index 69861eb0b55491..a04d6a0607a7eb 100644 --- a/lib/utils/request-rewriter/fetch.ts +++ b/lib/utils/request-rewriter/fetch.ts @@ -2,8 +2,30 @@ import logger from '@/utils/logger'; import { config } from '@/config'; import undici, { Request, RequestInfo, RequestInit } from 'undici'; import proxy from '@/utils/proxy'; +import { RateLimiterMemory, RateLimiterQueue } from 'rate-limiter-flexible'; +import { useRegisterRequest } from 'node-network-devtools'; -const wrappedFetch: typeof undici.fetch = (input: RequestInfo, init?: RequestInit) => { +const limiter = new RateLimiterMemory({ + points: 10, + duration: 1, + execEvenly: true, +}); + +const limiterQueue = new RateLimiterQueue(limiter, { + maxQueueSize: 4800, +}); + +export const useCustomHeader = (headers: Headers) => { + process.env.NODE_ENV === 'dev' && + useRegisterRequest((req) => { + for (const [key, value] of headers.entries()) { + req.requestHeaders[key] = value; + } + return req; + }); +}; + +const wrappedFetch: typeof undici.fetch = async (input: RequestInfo, init?: RequestInit) => { const request = new Request(input, init); const options: RequestInit = {}; @@ -29,8 +51,16 @@ const wrappedFetch: typeof undici.fetch = (input: RequestInfo, init?: RequestIni } } + let isRetry = false; + if (request.headers.get('x-prefer-proxy')) { + isRetry = true; + request.headers.delete('x-prefer-proxy'); + } + + config.enableRemoteDebugging && useCustomHeader(request.headers); + // proxy - if (!options.dispatcher && proxy.dispatcher) { + if (!init?.dispatcher && proxy.dispatcher && (proxy.proxyObj.strategy !== 'on_retry' || isRetry)) { const proxyRegex = new RegExp(proxy.proxyObj.url_regex); let urlHandler; try { @@ -41,9 +71,11 @@ const wrappedFetch: typeof undici.fetch = (input: RequestInfo, init?: RequestIni if (proxyRegex.test(request.url) && request.url.startsWith('http') && !(urlHandler && urlHandler.host === proxy.proxyUrlHandler?.host)) { options.dispatcher = proxy.dispatcher; + logger.debug(`Proxying request: ${request.url}`); } } + await limiterQueue.removeTokens(1); return undici.fetch(request, options); }; diff --git a/lib/utils/timezone.ts b/lib/utils/timezone.ts index 00ae95beb986dd..2e5439fdd1b8db 100644 --- a/lib/utils/timezone.ts +++ b/lib/utils/timezone.ts @@ -8,7 +8,7 @@ export default function timezone(date, timezone = serverTimezone) { date = new Date(date); } - assert(date instanceof Date); + assert.ok(date instanceof Date); return new Date(date.getTime() - millisInAnHour * (timezone - serverTimezone)); } diff --git a/lib/utils/wechat-mp.ts b/lib/utils/wechat-mp.ts index 13fc6aa31a7409..24537c33f2db1d 100644 --- a/lib/utils/wechat-mp.ts +++ b/lib/utils/wechat-mp.ts @@ -132,7 +132,7 @@ class ExtractMetadata { private static genExtractFunc = ( varName: string, { - valuePattern = '\\w+', + valuePattern = String.raw`\w+`, assignPattern = '=', allowNotFound = false, multiple = false, @@ -170,9 +170,9 @@ class ExtractMetadata { }; private static commonMetadataToBeExtracted = { - showType: this.genExtractFunc('item_show_type', { valuePattern: '\\d+' }), - realShowType: this.genExtractFunc('real_item_show_type', { valuePattern: '\\d+' }), - createTime: this.genExtractFunc('ct', { valuePattern: '\\d+', allowNotFound: true }), + showType: this.genExtractFunc('item_show_type', { valuePattern: String.raw`\d+` }), + realShowType: this.genExtractFunc('real_item_show_type', { valuePattern: String.raw`\d+` }), + createTime: this.genExtractFunc('ct', { valuePattern: String.raw`\d+`, allowNotFound: true }), sourceUrl: this.genExtractFunc('msg_source_url', { valuePattern: `https?://[^'"]*`, allowNotFound: true }), }; @@ -207,7 +207,7 @@ class ExtractMetadata { private static audioMetadataToBeExtracted = { voiceId: this.genExtractFunc('voiceid', { assignPattern: ':' }), - duration: this.genExtractFunc('duration', { valuePattern: '\\d*', assignPattern: ':', allowNotFound: true }), + duration: this.genExtractFunc('duration', { valuePattern: String.raw`\d*`, assignPattern: ':', allowNotFound: true }), }; // never seen a audio article containing multiple audio, waiting for examples @@ -623,19 +623,21 @@ const fetchArticle = (url: string, bypassHostCheck: boolean = false) => { * @return {Promise<object>} - The incoming `item` object, with the article and its metadata filled in. */ const finishArticleItem = async (item, setMpNameAsAuthor = false, skipLink = false) => { - const fetchedItem = await fetchArticle(item.link); - for (const key in fetchedItem) { - switch (key) { - case 'author': - item.author = setMpNameAsAuthor - ? fetchedItem.mpName || item.author // the Official Account itself. if your route return articles from different accounts, you may want to use this - : fetchedItem.author || item.author; // the real author of the article. if your route return articles from a certain account, use this - break; - case 'link': - item.link = skipLink ? item.link : fetchedItem.link || item.link; - break; - default: - item[key] = item[key] || fetchedItem[key]; + if (item.link) { + const fetchedItem = await fetchArticle(item.link); + for (const key in fetchedItem) { + switch (key) { + case 'author': + item.author = setMpNameAsAuthor + ? fetchedItem.mpName || item.author // the Official Account itself. if your route return articles from different accounts, you may want to use this + : fetchedItem.author || item.author; // the real author of the article. if your route return articles from a certain account, use this + break; + case 'link': + item.link = skipLink ? item.link : fetchedItem.link || item.link; + break; + default: + item[key] = item[key] || fetchedItem[key]; + } } } return item; diff --git a/lib/views/atom.tsx b/lib/views/atom.tsx index d9ebad6d3e234a..42f5bc2fa4da09 100644 --- a/lib/views/atom.tsx +++ b/lib/views/atom.tsx @@ -6,9 +6,9 @@ const RSS: FC<{ data: Data }> = ({ data }) => ( <title>{data.title || 'RSSHub'} {data.id || data.link} - {data.description || data.title} - Made with love by RSSHub(https://github.com/DIYgod/RSSHub) + {data.description || data.title} - Powered by RSSHub RSSHub - i@diygod.me (DIYgod) + contact@rsshub.app (RSSHub) {data.language || 'en'} {data.lastBuildDate} diff --git a/lib/views/error.tsx b/lib/views/error.tsx index 50e724b8f50e43..f24e9b27d8446d 100644 --- a/lib/views/error.tsx +++ b/lib/views/error.tsx @@ -11,13 +11,13 @@ const Index: FC<{ }> = ({ requestPath, message, errorRoute, nodeVersion }) => (
    -
    +
    RSSHub

    Looks like something went wrong

    @@ -26,7 +26,7 @@ const Index: FC<{

    Error Message:
    - {message} + {message}

    Route: {errorRoute} @@ -81,7 +81,7 @@ const Index: FC<{ Telegram channel {' '} and{' '} - + Twitter {' '} to get community support and news. @@ -96,7 +96,7 @@ const Index: FC<{ Telegram 频道 和{' '} - + Twitter {' '} 获取社区支持和新闻。 @@ -104,7 +104,7 @@ const Index: FC<{

    -
    +

    github @@ -115,8 +115,8 @@ const Index: FC<{ telegram channel - - github + + X

    diff --git a/lib/views/index.tsx b/lib/views/index.tsx index cc5394000c06e9..4cef59cb8a958f 100644 --- a/lib/views/index.tsx +++ b/lib/views/index.tsx @@ -129,28 +129,25 @@ const Index: FC<{ debugQuery: string | undefined }> = ({ debugQuery }) => { return (

    -
    - RSSHub +
    + RSSHub

    Welcome to RSSHub!

    +

    The world's largest RSS Network.

    If you see this page, the RSSHub is successfully installed and working.

    -

    Everything is RSSible

    {info.showDebug ? ( @@ -165,7 +162,8 @@ const Index: FC<{ debugQuery: string | undefined }> = ({ debugQuery }) => { ) : null}
    -
    + +

    github @@ -176,8 +174,8 @@ const Index: FC<{ debugQuery: string | undefined }> = ({ debugQuery }) => { telegram channel - - github + + X

    diff --git a/lib/views/json.ts b/lib/views/json.ts index 20111b8df4d63d..c6f00226fb3428 100644 --- a/lib/views/json.ts +++ b/lib/views/json.ts @@ -11,7 +11,7 @@ const json = (data: Data) => { title: data.title || 'RSSHub', home_page_url: data.link || 'https://docs.rsshub.app', feed_url: data.feedLink, - description: `${data.description || data.title} - Made with love by RSSHub(https://github.com/DIYgod/RSSHub)`, + description: `${data.description || data.title} - Powered by RSSHub`, icon: data.image, authors: typeof data.author === 'string' ? [{ name: data.author }] : data.author, language: data.language || 'zh-cn', @@ -21,24 +21,26 @@ const json = (data: Data) => { title: item.title, content_html: (item.content && item.content.html) || item.description || item.title, content_text: item.content && item.content.text, - image: item.image, + image: item.image || item.itunes_item_image, banner_image: item.banner, date_published: item.pubDate, date_modified: item.updated, authors: typeof item.author === 'string' ? [{ name: item.author }] : item.author, tags: typeof item.category === 'string' ? [item.category] : item.category, language: item.language, - attachments: item.enclosure_url - ? [ - { - url: item.enclosure_url, - mime_type: item.enclosure_type, - title: item.enclosure_title, - size_in_bytes: item.enclosure_length, - duration_in_seconds: item.itunes_duration, - }, - ] - : undefined, + attachments: + item.attachments || + (item.enclosure_url + ? [ + { + url: item.enclosure_url, + mime_type: item.enclosure_type, + title: item.enclosure_title, + size_in_bytes: item.enclosure_length, + duration_in_seconds: item.itunes_duration, + }, + ] + : undefined), _extra: item._extra || undefined, })), }; diff --git a/lib/views/layout.tsx b/lib/views/layout.tsx index f988bda2d40b10..11818378e7089e 100644 --- a/lib/views/layout.tsx +++ b/lib/views/layout.tsx @@ -45,6 +45,6 @@ export const Layout: FC = (props) => ( `} - {props.children} + {props.children} ); diff --git a/lib/views/rss.tsx b/lib/views/rss.tsx index f7420789a445a5..4d36c551e57e5d 100644 --- a/lib/views/rss.tsx +++ b/lib/views/rss.tsx @@ -4,6 +4,7 @@ import { Data } from '@/types'; const RSS: FC<{ data: Data }> = ({ data }) => { const hasItunes = data.itunes_author || data.itunes_category || (data.item && data.item.some((i) => i.itunes_item_image || i.itunes_duration)); const hasMedia = data.item?.some((i) => i.media); + const isTelegramLink = data.link?.startsWith('https://t.me/s/'); return ( @@ -11,9 +12,9 @@ const RSS: FC<{ data: Data }> = ({ data }) => { {data.title || 'RSSHub'} {data.link || 'https://docs.rsshub.app'} - {data.description || data.title} - Made with love by RSSHub(https://github.com/DIYgod/RSSHub) + {data.description || data.title} - Powered by RSSHub RSSHub - i@diygod.me (DIYgod) + contact@rsshub.app (RSSHub) {data.itunes_author && {data.itunes_author}} {data.itunes_category && } {data.itunes_author && {data.itunes_explicit || 'false'}} @@ -23,6 +24,12 @@ const RSS: FC<{ data: Data }> = ({ data }) => { {data.image} {data.title || 'RSSHub'} {data.link} + {isTelegramLink && ( + <> + 31 + 88 + + )} )} {data.lastBuildDate} @@ -35,6 +42,7 @@ const RSS: FC<{ data: Data }> = ({ data }) => { {item.guid || item.link || item.title} {item.pubDate && {item.pubDate}} {item.author && {item.author}} + {item.image && } {item.itunes_item_image && } {item.enclosure_url && } {item.itunes_duration && {item.itunes_duration}} diff --git a/lib/views/rss3.test.ts b/lib/views/rss3.test.ts new file mode 100644 index 00000000000000..c226cd2a980c20 --- /dev/null +++ b/lib/views/rss3.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from 'vitest'; +import rss3 from './rss3'; + +const NETWORK = 'rsshub'; +const TAG = 'RSS'; +const TYPE = 'feed'; +const PLATFORM = 'RSSHub'; + +describe('rss3', () => { + it('should return UMS Result', () => { + const data = { + item: [ + { + link: 'https://example.com/post1', + author: 'Author Name', + description: 'Description of the post', + pubDate: '2024-01-01T00:00:00Z', + category: 'Category Name', + title: 'Post Title', + updated: '2024-01-02T00:00:00Z', + }, + { + link: 'https://example.com/post2', + author: 'Anaother Author', + description: 'Another description', + pubDate: '2024-01-03T00:00:00Z', + category: 'Another Category', + title: 'Another Post', + updated: '2024-01-02T00:00:00Z', + }, + ], + }; + + const result = rss3(data); + + const expected = { + data: [ + { + owner: 'example.com', + id: 'https://example.com/post1', + network: NETWORK, + from: 'example.com', + to: 'example.com', + tag: TAG, + type: TYPE, + direction: 'out', + feeValue: '0', + actions: [ + { + tag: TAG, + type: TYPE, + platform: PLATFORM, + from: 'example.com', + to: 'example.com', + metadata: { + authors: [{ name: 'Author Name' }], + description: 'Description of the post', + pub_date: '2024-01-01T00:00:00Z', + tags: ['Category Name'], + title: 'Post Title', + }, + related_urls: ['https://example.com/post1'], + }, + ], + timestamp: 1_704_153_600, + }, + { + owner: 'example.com', + id: 'https://example.com/post2', + network: NETWORK, + from: 'example.com', + to: 'example.com', + tag: TAG, + type: TYPE, + direction: 'out', + feeValue: '0', + actions: [ + { + tag: TAG, + type: TYPE, + platform: PLATFORM, + from: 'example.com', + to: 'example.com', + metadata: { + authors: [{ name: 'Anaother Author' }], + description: 'Another description', + pub_date: '2024-01-03T00:00:00Z', + tags: ['Another Category'], + title: 'Another Post', + }, + related_urls: ['https://example.com/post2'], + }, + ], + timestamp: 1_704_153_600, + }, + ], + }; + expect(result).toStrictEqual(expected); + }); +}); diff --git a/lib/views/rss3.ts b/lib/views/rss3.ts index eb5c27613b72c6..3b0a26840b4ec6 100644 --- a/lib/views/rss3.ts +++ b/lib/views/rss3.ts @@ -6,9 +6,10 @@ import dayjs from 'dayjs'; * @returns `JSON.stringify`-ed [UMS Result](https://docs.rss3.io/docs/unified-metadata-schemas) */ -const NETWORK = 'RSS'; +const NETWORK = 'rsshub'; const TAG = 'RSS'; const TYPE = 'feed'; +const PLATFORM = 'RSSHub'; const rss3 = (data) => { const currentUnixTsp = dayjs().unix(); @@ -29,7 +30,7 @@ const rss3 = (data) => { { tag: TAG, type: TYPE, - platform: owner, + platform: PLATFORM, from: owner, to: owner, metadata: { diff --git a/package.json b/package.json index 14e00da6001c05..8e42f5b3a7e13e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "scripts": { "build": "tsx scripts/workflow/build-routes.ts", "build:docs": "tsx scripts/workflow/build-docs.ts", - "dev": "cross-env NODE_ENV=dev tsx watch --clear-screen=false lib/index.ts", + "dev": "cross-env NODE_ENV=dev tsx watch --inspect --clear-screen=false lib/index.ts", "dev:cache": "cross-env NODE_ENV=production tsx watch --clear-screen=false lib/index.ts", "format": "prettier \"**/*.{ts,tsx,js,json}\" --write && eslint --cache --fix \"**/*.{ts,tsx,js,yml}\"", "format:check": "prettier \"**/*.{ts,tsx,js,json}\" --check && eslint --cache \"**/*.{ts,tsx,js,yml}\"", @@ -50,137 +50,178 @@ "*.yml": "eslint --cache --fix" }, "dependencies": { - "@hono/node-server": "1.11.1", - "@hono/swagger-ui": "0.2.2", - "@hono/zod-openapi": "0.11.0", - "@notionhq/client": "2.2.15", + "@bbob/html": "4.2.0", + "@bbob/preset-html5": "4.2.0", + "@hono/node-server": "1.14.0", + "@hono/zod-openapi": "0.19.2", + "@notionhq/client": "2.3.0", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/exporter-prometheus": "0.200.0", + "@opentelemetry/exporter-trace-otlp-http": "0.200.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-metrics": "2.0.0", + "@opentelemetry/sdk-trace-base": "2.0.0", + "@opentelemetry/semantic-conventions": "1.30.0", "@postlight/parser": "2.2.3", - "@sentry/node": "7.113.0", - "@tonyrl/rand-user-agent": "2.0.61", + "@rss3/sdk": "0.0.25", + "@scalar/hono-api-reference": "0.7.4", + "@sentry/node": "9.10.1", + "@tonyrl/rand-user-agent": "2.0.83", "aes-js": "3.1.2", "art-template": "4.13.2", - "bbcodejs": "0.0.4", - "cheerio": "1.0.0-rc.12", - "chrono-node": "2.7.5", - "city-timezones": "1.2.1", + "cheerio": "1.0.0", + "chrono-node": "2.7.9", + "city-timezones": "1.3.0", "cross-env": "7.0.3", "crypto-js": "4.2.0", "currency-symbol-map": "5.1.0", "dayjs": "1.11.8", "destr": "2.0.3", - "directory-import": "3.3.1", - "dotenv": "16.4.5", - "entities": "4.5.0", + "directory-import": "3.3.2", + "dotenv": "16.4.7", + "entities": "6.0.0", "etag": "1.8.1", "fanfou-sdk": "5.0.0", - "form-data": "4.0.0", - "googleapis": "136.0.0", - "hono": "4.2.9", + "form-data": "4.0.2", + "googleapis": "148.0.0", + "hono": "4.7.5", "html-to-text": "9.0.5", - "https-proxy-agent": "7.0.4", + "http-cookie-agent": "6.0.8", + "https-proxy-agent": "7.0.6", "iconv-lite": "0.6.3", - "imapflow": "1.0.160", + "imapflow": "1.0.184", "instagram-private-api": "1.46.1", - "ioredis": "5.4.1", + "ioredis": "5.6.0", "ip-regex": "5.0.0", - "jsdom": "24.0.0", + "jsdom": "26.0.0", "json-bigint": "1.0.0", + "jsonpath-plus": "10.3.0", "jsrsasign": "10.9.0", - "lru-cache": "10.2.2", + "lru-cache": "11.1.0", "lz-string": "1.5.0", - "mailparser": "3.7.1", + "mailparser": "3.7.2", "markdown-it": "14.1.0", "module-alias": "2.2.3", - "notion-to-md": "3.1.1", + "narou": "1.1.0", + "notion-to-md": "3.1.7", "oauth-1.0a": "2.2.6", - "ofetch": "1.3.4", + "ofetch": "1.4.1", "otplib": "12.0.1", - "pac-proxy-agent": "7.0.1", - "proxy-chain": "2.4.0", + "pac-proxy-agent": "7.2.0", + "proxy-chain": "2.5.8", "puppeteer": "22.6.2", "puppeteer-extra": "3.3.6", "puppeteer-extra-plugin-stealth": "2.11.2", "puppeteer-extra-plugin-user-data-dir": "2.4.1", "puppeteer-extra-plugin-user-preferences": "2.4.1", - "query-string": "9.0.0", - "re2js": "0.4.1", - "rfc4648": "1.5.3", + "query-string": "9.1.1", + "rate-limiter-flexible": "6.2.1", + "re2js": "1.1.0", + "rfc4648": "1.5.4", "rss-parser": "3.13.0", - "sanitize-html": "2.13.0", - "simplecc-wasm": "0.1.5", - "socks-proxy-agent": "8.0.3", + "sanitize-html": "2.15.0", + "simplecc-wasm": "1.1.0", + "socks-proxy-agent": "8.0.5", "source-map": "0.7.4", - "telegram": "2.20.15", + "telegram": "2.26.22", "tiny-async-pool": "2.1.0", - "title": "3.5.3", - "tldts": "6.1.18", + "title": "4.0.1", + "tldts": "6.1.85", "tosource": "2.0.0-alpha.3", - "tough-cookie": "4.1.4", - "tsx": "4.8.2", - "twitter-api-v2": "1.16.3", - "undici": "6.15.0", - "uuid": "9.0.1", - "winston": "3.13.0", - "xxhash-wasm": "1.0.2", - "zod": "3.23.5" + "tough-cookie": "5.1.2", + "tsx": "4.19.3", + "twitter-api-v2": "1.22.0", + "ufo": "1.5.4", + "undici": "6.21.2", + "uuid": "11.1.0", + "winston": "3.17.0", + "xxhash-wasm": "1.1.0", + "zod": "3.24.2" }, "devDependencies": { - "@babel/preset-env": "7.24.5", - "@babel/preset-typescript": "7.24.1", + "@babel/preset-env": "7.26.9", + "@babel/preset-typescript": "7.27.0", + "@bbob/types": "4.2.0", + "@eslint/eslintrc": "3.3.1", + "@eslint/js": "9.23.0", "@microsoft/eslint-formatter-sarif": "3.1.0", - "@stylistic/eslint-plugin": "1.8.0", + "@stylistic/eslint-plugin": "4.2.0", "@types/aes-js": "3.1.4", - "@types/babel__preset-env": "7.9.6", + "@types/babel__preset-env": "7.10.0", "@types/crypto-js": "4.2.2", - "@types/eslint": "8.56.10", - "@types/eslint-config-prettier": "6.11.3", + "@types/eslint": "9.6.1", "@types/etag": "1.8.3", "@types/fs-extra": "11.0.4", "@types/html-to-text": "9.0.4", - "@types/imapflow": "1.0.18", + "@types/imapflow": "1.0.20", "@types/js-beautify": "1.14.3", - "@types/jsdom": "21.1.6", + "@types/jsdom": "21.1.7", "@types/json-bigint": "1.0.4", "@types/jsrsasign": "10.5.13", "@types/lint-staged": "13.3.0", - "@types/mailparser": "3.4.4", - "@types/markdown-it": "14.1.1", + "@types/mailparser": "3.4.5", + "@types/markdown-it": "14.1.2", "@types/module-alias": "2.0.4", - "@types/node": "20.12.8", - "@types/sanitize-html": "2.11.0", - "@types/supertest": "6.0.2", + "@types/node": "22.13.15", + "@types/sanitize-html": "2.15.0", + "@types/supertest": "6.0.3", "@types/tiny-async-pool": "2.0.3", "@types/title": "3.4.3", - "@types/tough-cookie": "4.0.5", - "@types/uuid": "9.0.8", - "@typescript-eslint/eslint-plugin": "7.8.0", - "@typescript-eslint/parser": "7.8.0", - "@vercel/nft": "0.26.4", - "@vitest/coverage-v8": "1.5.3", - "eslint": "8.57.0", - "eslint-config-prettier": "9.1.0", + "@types/uuid": "10.0.0", + "@typescript-eslint/eslint-plugin": "8.29.0", + "@typescript-eslint/parser": "8.29.0", + "@vercel/nft": "0.29.2", + "@vitest/coverage-v8": "2.1.9", + "discord-api-types": "0.37.119", + "eslint": "9.23.0", + "eslint-config-prettier": "10.1.1", "eslint-nibble": "8.1.0", - "eslint-plugin-n": "17.4.0", - "eslint-plugin-prettier": "5.1.3", - "eslint-plugin-unicorn": "52.0.0", - "eslint-plugin-yml": "1.14.0", - "fs-extra": "11.2.0", - "got": "14.2.1", - "husky": "9.0.11", - "js-beautify": "1.15.1", - "lint-staged": "15.2.2", + "eslint-plugin-n": "17.17.0", + "eslint-plugin-prettier": "5.2.5", + "eslint-plugin-unicorn": "58.0.0", + "eslint-plugin-yml": "1.17.0", + "fs-extra": "11.3.0", + "globals": "16.0.0", + "got": "14.4.7", + "husky": "9.1.7", + "js-beautify": "1.15.4", + "lint-staged": "15.5.0", "mockdate": "3.0.5", - "msw": "2.2.14", - "prettier": "3.2.5", + "msw": "2.4.3", + "node-network-devtools": "1.0.25", + "prettier": "3.5.3", "remark-parse": "11.0.0", - "supertest": "7.0.0", - "typescript": "5.4.5", - "unified": "11.0.4", - "vite-tsconfig-paths": "4.3.2", - "vitest": "1.5.3" + "supertest": "7.1.0", + "typescript": "5.8.2", + "unified": "11.0.5", + "vite-tsconfig-paths": "5.1.4", + "vitest": "2.1.9", + "yaml-eslint-parser": "1.3.0" }, - "packageManager": "pnpm@8.15.7+sha256.50783dd0fa303852de2dd1557cd4b9f07cb5b018154a6e76d0f40635d6cee019", + "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531", "engines": { - "node": ">=20" + "node": ">=22" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "bufferutil", + "core-js", + "es5-ext", + "esbuild", + "msw", + "protobufjs", + "puppeteer", + "utf-8-validate", + "vue-demi" + ], + "overrides": { + "art-template@4.13.2>html-minifier": "~4.0.0", + "difflib": "https://codeload.github.com/postlight/difflib.js/tar.gz/32e8e38c7fcd935241b9baab71bb432fd9b166ed", + "es-set-tostringtag": "npm:@nolyfill/es-set-tostringtag@^1", + "is-core-module": "npm:@nolyfill/is-core-module@^1", + "safe-buffer": "npm:@nolyfill/safe-buffer@^1", + "safer-buffer": "npm:@nolyfill/safer-buffer@^1", + "side-channel": "npm:@nolyfill/side-channel@^1" + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22f52388eef2d4..33fb22f0f4af5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,2312 +1,7683 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - '@hono/node-server': - specifier: 1.11.1 - version: 1.11.1 - '@hono/swagger-ui': - specifier: 0.2.2 - version: 0.2.2(hono@4.2.9) - '@hono/zod-openapi': - specifier: 0.11.0 - version: 0.11.0(hono@4.2.9)(zod@3.23.5) - '@notionhq/client': - specifier: 2.2.15 - version: 2.2.15 - '@postlight/parser': - specifier: 2.2.3 - version: 2.2.3 - '@sentry/node': - specifier: 7.113.0 - version: 7.113.0 - '@tonyrl/rand-user-agent': - specifier: 2.0.61 - version: 2.0.61 - aes-js: - specifier: 3.1.2 - version: 3.1.2 - art-template: - specifier: 4.13.2 - version: 4.13.2 - bbcodejs: - specifier: 0.0.4 - version: 0.0.4 - cheerio: - specifier: 1.0.0-rc.12 - version: 1.0.0-rc.12 - chrono-node: - specifier: 2.7.5 - version: 2.7.5 - city-timezones: - specifier: 1.2.1 - version: 1.2.1 - cross-env: - specifier: 7.0.3 - version: 7.0.3 - crypto-js: - specifier: 4.2.0 - version: 4.2.0 - currency-symbol-map: - specifier: 5.1.0 - version: 5.1.0 - dayjs: - specifier: 1.11.8 - version: 1.11.8 - destr: - specifier: 2.0.3 - version: 2.0.3 - directory-import: - specifier: 3.3.1 - version: 3.3.1 - dotenv: - specifier: 16.4.5 - version: 16.4.5 - entities: - specifier: 4.5.0 - version: 4.5.0 - etag: - specifier: 1.8.1 - version: 1.8.1 - fanfou-sdk: - specifier: 5.0.0 - version: 5.0.0 - form-data: - specifier: 4.0.0 - version: 4.0.0 - googleapis: - specifier: 136.0.0 - version: 136.0.0 - hono: - specifier: 4.2.9 - version: 4.2.9 - html-to-text: - specifier: 9.0.5 - version: 9.0.5 - https-proxy-agent: - specifier: 7.0.4 - version: 7.0.4 - iconv-lite: - specifier: 0.6.3 - version: 0.6.3 - imapflow: - specifier: 1.0.160 - version: 1.0.160 - instagram-private-api: - specifier: 1.46.1 - version: 1.46.1 - ioredis: - specifier: 5.4.1 - version: 5.4.1 - ip-regex: - specifier: 5.0.0 - version: 5.0.0 - jsdom: - specifier: 24.0.0 - version: 24.0.0 - json-bigint: - specifier: 1.0.0 - version: 1.0.0 - jsrsasign: - specifier: 10.9.0 - version: 10.9.0 - lru-cache: - specifier: 10.2.2 - version: 10.2.2 - lz-string: - specifier: 1.5.0 - version: 1.5.0 - mailparser: - specifier: 3.7.1 - version: 3.7.1 - markdown-it: - specifier: 14.1.0 - version: 14.1.0 - module-alias: - specifier: 2.2.3 - version: 2.2.3 - notion-to-md: - specifier: 3.1.1 - version: 3.1.1 - oauth-1.0a: - specifier: 2.2.6 - version: 2.2.6 - ofetch: - specifier: 1.3.4 - version: 1.3.4 - otplib: - specifier: 12.0.1 - version: 12.0.1 - pac-proxy-agent: - specifier: 7.0.1 - version: 7.0.1 - proxy-chain: - specifier: 2.4.0 - version: 2.4.0 - puppeteer: - specifier: 22.6.2 - version: 22.6.2(typescript@5.4.5) - puppeteer-extra: - specifier: 3.3.6 - version: 3.3.6(puppeteer@22.6.2) - puppeteer-extra-plugin-stealth: - specifier: 2.11.2 - version: 2.11.2(puppeteer-extra@3.3.6) - puppeteer-extra-plugin-user-data-dir: - specifier: 2.4.1 - version: 2.4.1(puppeteer-extra@3.3.6) - puppeteer-extra-plugin-user-preferences: - specifier: 2.4.1 - version: 2.4.1(puppeteer-extra@3.3.6) - query-string: - specifier: 9.0.0 - version: 9.0.0 - re2js: - specifier: 0.4.1 - version: 0.4.1 - rfc4648: - specifier: 1.5.3 - version: 1.5.3 - rss-parser: - specifier: 3.13.0 - version: 3.13.0 - sanitize-html: - specifier: 2.13.0 - version: 2.13.0 - simplecc-wasm: - specifier: 0.1.5 - version: 0.1.5 - socks-proxy-agent: - specifier: 8.0.3 - version: 8.0.3 - source-map: - specifier: 0.7.4 - version: 0.7.4 - telegram: - specifier: 2.20.15 - version: 2.20.15 - tiny-async-pool: - specifier: 2.1.0 - version: 2.1.0 - title: - specifier: 3.5.3 - version: 3.5.3 - tldts: - specifier: 6.1.18 - version: 6.1.18 - tosource: - specifier: 2.0.0-alpha.3 - version: 2.0.0-alpha.3 - tough-cookie: - specifier: 4.1.4 - version: 4.1.4 - tsx: - specifier: 4.8.2 - version: 4.8.2 - twitter-api-v2: - specifier: 1.16.3 - version: 1.16.3 - undici: - specifier: 6.15.0 - version: 6.15.0 - uuid: - specifier: 9.0.1 - version: 9.0.1 - winston: - specifier: 3.13.0 - version: 3.13.0 - xxhash-wasm: - specifier: 1.0.2 - version: 1.0.2 - zod: - specifier: 3.23.5 - version: 3.23.5 - -devDependencies: - '@babel/preset-env': - specifier: 7.24.5 - version: 7.24.5(@babel/core@7.24.4) - '@babel/preset-typescript': - specifier: 7.24.1 - version: 7.24.1(@babel/core@7.24.4) - '@microsoft/eslint-formatter-sarif': - specifier: 3.1.0 - version: 3.1.0 - '@stylistic/eslint-plugin': - specifier: 1.8.0 - version: 1.8.0(eslint@8.57.0)(typescript@5.4.5) - '@types/aes-js': - specifier: 3.1.4 - version: 3.1.4 - '@types/babel__preset-env': - specifier: 7.9.6 - version: 7.9.6 - '@types/crypto-js': - specifier: 4.2.2 - version: 4.2.2 - '@types/eslint': - specifier: 8.56.10 - version: 8.56.10 - '@types/eslint-config-prettier': - specifier: 6.11.3 - version: 6.11.3 - '@types/etag': - specifier: 1.8.3 - version: 1.8.3 - '@types/fs-extra': - specifier: 11.0.4 - version: 11.0.4 - '@types/html-to-text': - specifier: 9.0.4 - version: 9.0.4 - '@types/imapflow': - specifier: 1.0.18 - version: 1.0.18 - '@types/js-beautify': - specifier: 1.14.3 - version: 1.14.3 - '@types/jsdom': - specifier: 21.1.6 - version: 21.1.6 - '@types/json-bigint': - specifier: 1.0.4 - version: 1.0.4 - '@types/jsrsasign': - specifier: 10.5.13 - version: 10.5.13 - '@types/lint-staged': - specifier: 13.3.0 - version: 13.3.0 - '@types/mailparser': - specifier: 3.4.4 - version: 3.4.4 - '@types/markdown-it': - specifier: 14.1.1 - version: 14.1.1 - '@types/module-alias': - specifier: 2.0.4 - version: 2.0.4 - '@types/node': - specifier: 20.12.8 - version: 20.12.8 - '@types/sanitize-html': - specifier: 2.11.0 - version: 2.11.0 - '@types/supertest': - specifier: 6.0.2 - version: 6.0.2 - '@types/tiny-async-pool': - specifier: 2.0.3 - version: 2.0.3 - '@types/title': - specifier: 3.4.3 - version: 3.4.3 - '@types/tough-cookie': - specifier: 4.0.5 - version: 4.0.5 - '@types/uuid': - specifier: 9.0.8 - version: 9.0.8 - '@typescript-eslint/eslint-plugin': - specifier: 7.8.0 - version: 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/parser': - specifier: 7.8.0 - version: 7.8.0(eslint@8.57.0)(typescript@5.4.5) - '@vercel/nft': - specifier: 0.26.4 - version: 0.26.4 - '@vitest/coverage-v8': - specifier: 1.5.3 - version: 1.5.3(vitest@1.5.3) - eslint: - specifier: 8.57.0 - version: 8.57.0 - eslint-config-prettier: - specifier: 9.1.0 - version: 9.1.0(eslint@8.57.0) - eslint-nibble: - specifier: 8.1.0 - version: 8.1.0(eslint@8.57.0) - eslint-plugin-n: - specifier: 17.4.0 - version: 17.4.0(eslint@8.57.0) - eslint-plugin-prettier: - specifier: 5.1.3 - version: 5.1.3(@types/eslint@8.56.10)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) - eslint-plugin-unicorn: - specifier: 52.0.0 - version: 52.0.0(eslint@8.57.0) - eslint-plugin-yml: - specifier: 1.14.0 - version: 1.14.0(eslint@8.57.0) - fs-extra: - specifier: 11.2.0 - version: 11.2.0 - got: - specifier: 14.2.1 - version: 14.2.1 - husky: - specifier: 9.0.11 - version: 9.0.11 - js-beautify: - specifier: 1.15.1 - version: 1.15.1 - lint-staged: - specifier: 15.2.2 - version: 15.2.2 - mockdate: - specifier: 3.0.5 - version: 3.0.5 - msw: - specifier: 2.2.14 - version: 2.2.14(typescript@5.4.5) - prettier: - specifier: 3.2.5 - version: 3.2.5 - remark-parse: - specifier: 11.0.0 - version: 11.0.0 - supertest: - specifier: 7.0.0 - version: 7.0.0 - typescript: - specifier: 5.4.5 - version: 5.4.5 - unified: - specifier: 11.0.4 - version: 11.0.4 - vite-tsconfig-paths: - specifier: 4.3.2 - version: 4.3.2(typescript@5.4.5) - vitest: - specifier: 1.5.3 - version: 1.5.3(@types/node@20.12.8)(jsdom@24.0.0) +overrides: + art-template@4.13.2>html-minifier: ~4.0.0 + difflib: https://codeload.github.com/postlight/difflib.js/tar.gz/32e8e38c7fcd935241b9baab71bb432fd9b166ed + es-set-tostringtag: npm:@nolyfill/es-set-tostringtag@^1 + is-core-module: npm:@nolyfill/is-core-module@^1 + safe-buffer: npm:@nolyfill/safe-buffer@^1 + safer-buffer: npm:@nolyfill/safer-buffer@^1 + side-channel: npm:@nolyfill/side-channel@^1 + +importers: + + .: + dependencies: + '@bbob/html': + specifier: 4.2.0 + version: 4.2.0 + '@bbob/preset-html5': + specifier: 4.2.0 + version: 4.2.0 + '@hono/node-server': + specifier: 1.14.0 + version: 1.14.0(hono@4.7.5) + '@hono/zod-openapi': + specifier: 0.19.2 + version: 0.19.2(hono@4.7.5)(zod@3.24.2) + '@notionhq/client': + specifier: 2.3.0 + version: 2.3.0 + '@opentelemetry/api': + specifier: 1.9.0 + version: 1.9.0 + '@opentelemetry/exporter-prometheus': + specifier: 0.200.0 + version: 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': + specifier: 0.200.0 + version: 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: 2.0.0 + version: 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: 2.0.0 + version: 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: 2.0.0 + version: 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: 1.30.0 + version: 1.30.0 + '@postlight/parser': + specifier: 2.2.3 + version: 2.2.3 + '@rss3/sdk': + specifier: 0.0.25 + version: 0.0.25 + '@scalar/hono-api-reference': + specifier: 0.7.4 + version: 0.7.4(hono@4.7.5) + '@sentry/node': + specifier: 9.10.1 + version: 9.10.1 + '@tonyrl/rand-user-agent': + specifier: 2.0.83 + version: 2.0.83 + aes-js: + specifier: 3.1.2 + version: 3.1.2 + art-template: + specifier: 4.13.2 + version: 4.13.2 + cheerio: + specifier: 1.0.0 + version: 1.0.0 + chrono-node: + specifier: 2.7.9 + version: 2.7.9 + city-timezones: + specifier: 1.3.0 + version: 1.3.0 + cross-env: + specifier: 7.0.3 + version: 7.0.3 + crypto-js: + specifier: 4.2.0 + version: 4.2.0 + currency-symbol-map: + specifier: 5.1.0 + version: 5.1.0 + dayjs: + specifier: 1.11.8 + version: 1.11.8 + destr: + specifier: 2.0.3 + version: 2.0.3 + directory-import: + specifier: 3.3.2 + version: 3.3.2 + dotenv: + specifier: 16.4.7 + version: 16.4.7 + entities: + specifier: 6.0.0 + version: 6.0.0 + etag: + specifier: 1.8.1 + version: 1.8.1 + fanfou-sdk: + specifier: 5.0.0 + version: 5.0.0 + form-data: + specifier: 4.0.2 + version: 4.0.2 + googleapis: + specifier: 148.0.0 + version: 148.0.0 + hono: + specifier: 4.7.5 + version: 4.7.5 + html-to-text: + specifier: 9.0.5 + version: 9.0.5 + http-cookie-agent: + specifier: 6.0.8 + version: 6.0.8(tough-cookie@5.1.2)(undici@6.21.2) + https-proxy-agent: + specifier: 7.0.6 + version: 7.0.6 + iconv-lite: + specifier: 0.6.3 + version: 0.6.3 + imapflow: + specifier: 1.0.184 + version: 1.0.184 + instagram-private-api: + specifier: 1.46.1 + version: 1.46.1 + ioredis: + specifier: 5.6.0 + version: 5.6.0 + ip-regex: + specifier: 5.0.0 + version: 5.0.0 + jsdom: + specifier: 26.0.0 + version: 26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + json-bigint: + specifier: 1.0.0 + version: 1.0.0 + jsonpath-plus: + specifier: 10.3.0 + version: 10.3.0 + jsrsasign: + specifier: 10.9.0 + version: 10.9.0 + lru-cache: + specifier: 11.1.0 + version: 11.1.0 + lz-string: + specifier: 1.5.0 + version: 1.5.0 + mailparser: + specifier: 3.7.2 + version: 3.7.2 + markdown-it: + specifier: 14.1.0 + version: 14.1.0 + module-alias: + specifier: 2.2.3 + version: 2.2.3 + narou: + specifier: 1.1.0 + version: 1.1.0 + notion-to-md: + specifier: 3.1.7 + version: 3.1.7 + oauth-1.0a: + specifier: 2.2.6 + version: 2.2.6 + ofetch: + specifier: 1.4.1 + version: 1.4.1 + otplib: + specifier: 12.0.1 + version: 12.0.1 + pac-proxy-agent: + specifier: 7.2.0 + version: 7.2.0 + proxy-chain: + specifier: 2.5.8 + version: 2.5.8 + puppeteer: + specifier: 22.6.2 + version: 22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + puppeteer-extra: + specifier: 3.3.6 + version: 3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)) + puppeteer-extra-plugin-stealth: + specifier: 2.11.2 + version: 2.11.2(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))) + puppeteer-extra-plugin-user-data-dir: + specifier: 2.4.1 + version: 2.4.1(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))) + puppeteer-extra-plugin-user-preferences: + specifier: 2.4.1 + version: 2.4.1(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))) + query-string: + specifier: 9.1.1 + version: 9.1.1 + rate-limiter-flexible: + specifier: 6.2.1 + version: 6.2.1 + re2js: + specifier: 1.1.0 + version: 1.1.0 + rfc4648: + specifier: 1.5.4 + version: 1.5.4 + rss-parser: + specifier: 3.13.0 + version: 3.13.0 + sanitize-html: + specifier: 2.15.0 + version: 2.15.0 + simplecc-wasm: + specifier: 1.1.0 + version: 1.1.0 + socks-proxy-agent: + specifier: 8.0.5 + version: 8.0.5 + source-map: + specifier: 0.7.4 + version: 0.7.4 + telegram: + specifier: 2.26.22 + version: 2.26.22 + tiny-async-pool: + specifier: 2.1.0 + version: 2.1.0 + title: + specifier: 4.0.1 + version: 4.0.1 + tldts: + specifier: 6.1.85 + version: 6.1.85 + tosource: + specifier: 2.0.0-alpha.3 + version: 2.0.0-alpha.3 + tough-cookie: + specifier: 5.1.2 + version: 5.1.2 + tsx: + specifier: 4.19.3 + version: 4.19.3 + twitter-api-v2: + specifier: 1.22.0 + version: 1.22.0 + ufo: + specifier: 1.5.4 + version: 1.5.4 + undici: + specifier: 6.21.2 + version: 6.21.2 + uuid: + specifier: 11.1.0 + version: 11.1.0 + winston: + specifier: 3.17.0 + version: 3.17.0 + xxhash-wasm: + specifier: 1.1.0 + version: 1.1.0 + zod: + specifier: 3.24.2 + version: 3.24.2 + devDependencies: + '@babel/preset-env': + specifier: 7.26.9 + version: 7.26.9(@babel/core@7.26.10) + '@babel/preset-typescript': + specifier: 7.27.0 + version: 7.27.0(@babel/core@7.26.10) + '@bbob/types': + specifier: 4.2.0 + version: 4.2.0 + '@eslint/eslintrc': + specifier: 3.3.1 + version: 3.3.1 + '@eslint/js': + specifier: 9.23.0 + version: 9.23.0 + '@microsoft/eslint-formatter-sarif': + specifier: 3.1.0 + version: 3.1.0 + '@stylistic/eslint-plugin': + specifier: 4.2.0 + version: 4.2.0(eslint@9.23.0)(typescript@5.8.2) + '@types/aes-js': + specifier: 3.1.4 + version: 3.1.4 + '@types/babel__preset-env': + specifier: 7.10.0 + version: 7.10.0 + '@types/crypto-js': + specifier: 4.2.2 + version: 4.2.2 + '@types/eslint': + specifier: 9.6.1 + version: 9.6.1 + '@types/etag': + specifier: 1.8.3 + version: 1.8.3 + '@types/fs-extra': + specifier: 11.0.4 + version: 11.0.4 + '@types/html-to-text': + specifier: 9.0.4 + version: 9.0.4 + '@types/imapflow': + specifier: 1.0.20 + version: 1.0.20 + '@types/js-beautify': + specifier: 1.14.3 + version: 1.14.3 + '@types/jsdom': + specifier: 21.1.7 + version: 21.1.7 + '@types/json-bigint': + specifier: 1.0.4 + version: 1.0.4 + '@types/jsrsasign': + specifier: 10.5.13 + version: 10.5.13 + '@types/lint-staged': + specifier: 13.3.0 + version: 13.3.0 + '@types/mailparser': + specifier: 3.4.5 + version: 3.4.5 + '@types/markdown-it': + specifier: 14.1.2 + version: 14.1.2 + '@types/module-alias': + specifier: 2.0.4 + version: 2.0.4 + '@types/node': + specifier: 22.13.15 + version: 22.13.15 + '@types/sanitize-html': + specifier: 2.15.0 + version: 2.15.0 + '@types/supertest': + specifier: 6.0.3 + version: 6.0.3 + '@types/tiny-async-pool': + specifier: 2.0.3 + version: 2.0.3 + '@types/title': + specifier: 3.4.3 + version: 3.4.3 + '@types/uuid': + specifier: 10.0.0 + version: 10.0.0 + '@typescript-eslint/eslint-plugin': + specifier: 8.29.0 + version: 8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/parser': + specifier: 8.29.0 + version: 8.29.0(eslint@9.23.0)(typescript@5.8.2) + '@vercel/nft': + specifier: 0.29.2 + version: 0.29.2(rollup@4.37.0) + '@vitest/coverage-v8': + specifier: 2.1.9 + version: 2.1.9(vitest@2.1.9(@types/node@22.13.15)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.4.3(typescript@5.8.2))) + discord-api-types: + specifier: 0.37.119 + version: 0.37.119 + eslint: + specifier: 9.23.0 + version: 9.23.0 + eslint-config-prettier: + specifier: 10.1.1 + version: 10.1.1(eslint@9.23.0) + eslint-nibble: + specifier: 8.1.0 + version: 8.1.0(eslint@9.23.0) + eslint-plugin-n: + specifier: 17.17.0 + version: 17.17.0(eslint@9.23.0) + eslint-plugin-prettier: + specifier: 5.2.5 + version: 5.2.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.1(eslint@9.23.0))(eslint@9.23.0)(prettier@3.5.3) + eslint-plugin-unicorn: + specifier: 58.0.0 + version: 58.0.0(eslint@9.23.0) + eslint-plugin-yml: + specifier: 1.17.0 + version: 1.17.0(eslint@9.23.0) + fs-extra: + specifier: 11.3.0 + version: 11.3.0 + globals: + specifier: 16.0.0 + version: 16.0.0 + got: + specifier: 14.4.7 + version: 14.4.7 + husky: + specifier: 9.1.7 + version: 9.1.7 + js-beautify: + specifier: 1.15.4 + version: 1.15.4 + lint-staged: + specifier: 15.5.0 + version: 15.5.0 + mockdate: + specifier: 3.0.5 + version: 3.0.5 + msw: + specifier: 2.4.3 + version: 2.4.3(typescript@5.8.2) + node-network-devtools: + specifier: 1.0.25 + version: 1.0.25(bufferutil@4.0.9)(undici@6.21.2)(utf-8-validate@5.0.10) + prettier: + specifier: 3.5.3 + version: 3.5.3 + remark-parse: + specifier: 11.0.0 + version: 11.0.0 + supertest: + specifier: 7.1.0 + version: 7.1.0 + typescript: + specifier: 5.8.2 + version: 5.8.2 + unified: + specifier: 11.0.5 + version: 11.0.5 + vite-tsconfig-paths: + specifier: 5.1.4 + version: 5.1.4(typescript@5.8.2)(vite@5.4.15(@types/node@22.13.15)) + vitest: + specifier: 2.1.9 + version: 2.1.9(@types/node@22.13.15)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.4.3(typescript@5.8.2)) + yaml-eslint-parser: + specifier: 1.3.0 + version: 1.3.0 packages: - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - dev: true - - /@ampproject/remapping@2.3.0: + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - dev: true - /@asteasolutions/zod-to-openapi@7.0.0(zod@3.23.5): - resolution: {integrity: sha512-rJRKHD2m6nUb/9ZheeN8nqOURX24WTzY8Sex1ZKT0Kpx+xfpRcD0fTD6vEeXNHGaDGxzu65Jj/jb2x6nLTjcMw==} + '@asamuzakjp/css-color@3.1.1': + resolution: {integrity: sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==} + + '@asteasolutions/zod-to-openapi@7.3.0': + resolution: {integrity: sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==} peerDependencies: zod: ^3.20.2 - dependencies: - openapi3-ts: 4.3.1 - zod: 3.23.5 - dev: false - /@babel/code-frame@7.0.0: + '@babel/code-frame@7.0.0': resolution: {integrity: sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==} - dependencies: - '@babel/highlight': 7.24.2 - dev: true - - /@babel/code-frame@7.24.2: - resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.24.2 - picocolors: 1.0.0 - - /@babel/compat-data@7.24.4: - resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/core@7.24.4: - resolution: {integrity: sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.4 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) - '@babel/helpers': 7.24.4 - '@babel/parser': 7.24.4 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.5 - convert-source-map: 2.0.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/generator@7.24.4: - resolution: {integrity: sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==} + '@babel/compat-data@7.26.8': + resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - dev: true - /@babel/helper-annotate-as-pure@7.22.5: - resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + '@babel/core@7.26.10': + resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: - resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} + '@babel/generator@7.27.0': + resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-compilation-targets@7.23.6: - resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/compat-data': 7.24.4 - '@babel/helper-validator-option': 7.23.5 - browserslist: 4.23.0 - lru-cache: 5.1.1 - semver: 6.3.1 - dev: true - /@babel/helper-create-class-features-plugin@7.24.4(@babel/core@7.24.4): - resolution: {integrity: sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==} + '@babel/helper-compilation-targets@7.27.0': + resolution: {integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.23.0 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.4) - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - semver: 6.3.1 - dev: true - /@babel/helper-create-class-features-plugin@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==} + '@babel/helper-create-class-features-plugin@7.27.0': + resolution: {integrity: sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.24.5 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.4) - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - semver: 6.3.1 - dev: true - /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.4): - resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} + '@babel/helper-create-regexp-features-plugin@7.27.0': + resolution: {integrity: sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-annotate-as-pure': 7.22.5 - regexpu-core: 5.3.2 - semver: 6.3.1 - dev: true - /@babel/helper-define-polyfill-provider@0.6.1(@babel/core@7.24.4): - resolution: {integrity: sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==} + '@babel/helper-define-polyfill-provider@0.6.4': + resolution: {integrity: sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.8 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/helper-environment-visitor@7.22.20: - resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + '@babel/helper-member-expression-to-functions@7.25.9': + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-function-name@7.23.0: - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.24.0 - '@babel/types': 7.24.5 - dev: true - - /@babel/helper-hoist-variables@7.22.5: - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - - /@babel/helper-member-expression-to-functions@7.23.0: - resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - - /@babel/helper-member-expression-to-functions@7.24.5: - resolution: {integrity: sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - - /@babel/helper-module-imports@7.24.3: - resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.4): - resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.24.5 - dev: true - - /@babel/helper-optimise-call-expression@7.22.5: - resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-plugin-utils@7.24.0: - resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==} + '@babel/helper-optimise-call-expression@7.25.9': + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-plugin-utils@7.24.5: - resolution: {integrity: sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==} + '@babel/helper-plugin-utils@7.26.5': + resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.4): - resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} + '@babel/helper-remap-async-to-generator@7.25.9': + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-wrap-function': 7.22.20 - dev: true - /@babel/helper-replace-supers@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==} + '@babel/helper-replace-supers@7.26.5': + resolution: {integrity: sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-member-expression-to-functions': 7.24.5 - '@babel/helper-optimise-call-expression': 7.22.5 - dev: true - - /@babel/helper-simple-access@7.22.5: - resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - - /@babel/helper-skip-transparent-expression-wrappers@7.22.5: - resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - - /@babel/helper-split-export-declaration@7.22.6: - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-split-export-declaration@7.24.5: - resolution: {integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==} + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - - /@babel/helper-string-parser@7.24.1: - resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-validator-identifier@7.22.20: - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-validator-identifier@7.24.5: - resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option@7.23.5: - resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-wrap-function@7.22.20: - resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} + '@babel/helper-wrap-function@7.25.9': + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-function-name': 7.23.0 - '@babel/template': 7.24.0 - '@babel/types': 7.24.5 - dev: true - /@babel/helpers@7.24.4: - resolution: {integrity: sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==} + '@babel/helpers@7.27.0': + resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/highlight@7.24.2: - resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} + '@babel/highlight@7.25.9': + resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.24.5 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.0.0 - /@babel/parser@7.24.4: - resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==} + '@babel/parser@7.27.0': + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} engines: {node: '>=6.0.0'} hasBin: true - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==} + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==} + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9': + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.4) - dev: true - /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==} + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9': + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.4): - resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - dev: true - - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.4): - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.4): - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.4): - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9': + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.4): - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.4): - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/core': ^7.13.0 - /@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==} + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9': + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/core': ^7.0.0 - /@babel/plugin-syntax-import-attributes@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==} + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.4): - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.4): - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==} + '@babel/plugin-syntax-import-assertions@7.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.0 - dev: true - - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.4): - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.4): - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.4): - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.4): - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.4): - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.4): - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.4): - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.4): - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-typescript@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==} + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.4): + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-arrow-functions@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==} + '@babel/plugin-transform-arrow-functions@7.25.9': + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-async-generator-functions@7.24.3(@babel/core@7.24.4): - resolution: {integrity: sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==} + '@babel/plugin-transform-async-generator-functions@7.26.8': + resolution: {integrity: sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.4) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-async-to-generator@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==} + '@babel/plugin-transform-async-to-generator@7.25.9': + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-block-scoped-functions@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==} + '@babel/plugin-transform-block-scoped-functions@7.26.5': + resolution: {integrity: sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-block-scoping@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==} + '@babel/plugin-transform-block-scoping@7.27.0': + resolution: {integrity: sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-class-properties@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==} + '@babel/plugin-transform-class-properties@7.25.9': + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-class-static-block@7.24.4(@babel/core@7.24.4): - resolution: {integrity: sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==} + '@babel/plugin-transform-class-static-block@7.26.0': + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-classes@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==} + '@babel/plugin-transform-classes@7.25.9': + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.4) - '@babel/helper-split-export-declaration': 7.24.5 - globals: 11.12.0 - dev: true - /@babel/plugin-transform-computed-properties@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==} + '@babel/plugin-transform-computed-properties@7.25.9': + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/template': 7.24.0 - dev: true - /@babel/plugin-transform-destructuring@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==} + '@babel/plugin-transform-destructuring@7.25.9': + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-dotall-regex@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==} + '@babel/plugin-transform-dotall-regex@7.25.9': + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-duplicate-keys@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==} + '@babel/plugin-transform-duplicate-keys@7.25.9': + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-dynamic-import@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==} + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.25.9': + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-exponentiation-operator@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==} + '@babel/plugin-transform-exponentiation-operator@7.26.3': + resolution: {integrity: sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-export-namespace-from@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==} + '@babel/plugin-transform-export-namespace-from@7.25.9': + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-for-of@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==} + '@babel/plugin-transform-for-of@7.26.9': + resolution: {integrity: sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - dev: true - /@babel/plugin-transform-function-name@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==} + '@babel/plugin-transform-function-name@7.25.9': + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-json-strings@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==} + '@babel/plugin-transform-json-strings@7.25.9': + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-literals@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==} + '@babel/plugin-transform-literals@7.25.9': + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-logical-assignment-operators@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==} + '@babel/plugin-transform-logical-assignment-operators@7.25.9': + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-member-expression-literals@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==} + '@babel/plugin-transform-member-expression-literals@7.25.9': + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-modules-amd@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==} + '@babel/plugin-transform-modules-amd@7.25.9': + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-modules-commonjs@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==} + '@babel/plugin-transform-modules-commonjs@7.26.3': + resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-simple-access': 7.22.5 - dev: true - /@babel/plugin-transform-modules-systemjs@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==} + '@babel/plugin-transform-modules-systemjs@7.25.9': + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-validator-identifier': 7.24.5 - dev: true - /@babel/plugin-transform-modules-umd@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==} + '@babel/plugin-transform-modules-umd@7.25.9': + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.4): - resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-new-target@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==} + '@babel/plugin-transform-new-target@7.25.9': + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-nullish-coalescing-operator@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==} + '@babel/plugin-transform-nullish-coalescing-operator@7.26.6': + resolution: {integrity: sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-numeric-separator@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==} + '@babel/plugin-transform-numeric-separator@7.25.9': + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-object-rest-spread@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==} + '@babel/plugin-transform-object-rest-spread@7.25.9': + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.4) - '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-object-super@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==} + '@babel/plugin-transform-object-super@7.25.9': + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-optional-catch-binding@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==} + '@babel/plugin-transform-optional-catch-binding@7.25.9': + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-optional-chaining@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==} + '@babel/plugin-transform-optional-chaining@7.25.9': + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-parameters@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==} + '@babel/plugin-transform-parameters@7.25.9': + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-private-methods@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==} + '@babel/plugin-transform-private-methods@7.25.9': + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-private-property-in-object@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==} + '@babel/plugin-transform-private-property-in-object@7.25.9': + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-property-literals@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==} + '@babel/plugin-transform-property-literals@7.25.9': + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-regenerator@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==} + '@babel/plugin-transform-regenerator@7.27.0': + resolution: {integrity: sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - regenerator-transform: 0.15.2 - dev: true - /@babel/plugin-transform-reserved-words@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==} + '@babel/plugin-transform-regexp-modifiers@7.26.0': + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.25.9': + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-shorthand-properties@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==} + '@babel/plugin-transform-shorthand-properties@7.25.9': + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-spread@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==} + '@babel/plugin-transform-spread@7.25.9': + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - dev: true - /@babel/plugin-transform-sticky-regex@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==} + '@babel/plugin-transform-sticky-regex@7.25.9': + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-template-literals@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==} + '@babel/plugin-transform-template-literals@7.26.8': + resolution: {integrity: sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-typeof-symbol@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==} + '@babel/plugin-transform-typeof-symbol@7.27.0': + resolution: {integrity: sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-typescript@7.24.4(@babel/core@7.24.4): - resolution: {integrity: sha512-79t3CQ8+oBGk/80SQ8MN3Bs3obf83zJ0YZjDmDaEZN8MqhMI760apl5z6a20kFeMXBwJX99VpKT8CKxEBp5H1g==} + '@babel/plugin-transform-typescript@7.27.0': + resolution: {integrity: sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.24.4(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.4) - dev: true - /@babel/plugin-transform-unicode-escapes@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==} + '@babel/plugin-transform-unicode-escapes@7.25.9': + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-unicode-property-regex@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==} + '@babel/plugin-transform-unicode-property-regex@7.25.9': + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-unicode-regex@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==} + '@babel/plugin-transform-unicode-regex@7.25.9': + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-transform-unicode-sets-regex@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==} + '@babel/plugin-transform-unicode-sets-regex@7.25.9': + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/preset-env@7.24.5(@babel/core@7.24.4): - resolution: {integrity: sha512-UGK2ifKtcC8i5AI4cH+sbLLuLc2ktYSFJgBAXorKAsHUZmrQ1q6aQ6i3BvU24wWs2AAKqQB6kq3N9V9Gw1HiMQ==} + '@babel/preset-env@7.26.9': + resolution: {integrity: sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.24.4 - '@babel/core': 7.24.4 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.5(@babel/core@7.24.4) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.4) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.4) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.4) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.4) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.4) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.4) - '@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-syntax-import-attributes': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.4) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.4) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.4) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.4) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.4) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.4) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.4) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.4) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.4) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.4) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.4) - '@babel/plugin-transform-arrow-functions': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-async-generator-functions': 7.24.3(@babel/core@7.24.4) - '@babel/plugin-transform-async-to-generator': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-block-scoped-functions': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-block-scoping': 7.24.5(@babel/core@7.24.4) - '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-class-static-block': 7.24.4(@babel/core@7.24.4) - '@babel/plugin-transform-classes': 7.24.5(@babel/core@7.24.4) - '@babel/plugin-transform-computed-properties': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-destructuring': 7.24.5(@babel/core@7.24.4) - '@babel/plugin-transform-dotall-regex': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-duplicate-keys': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-dynamic-import': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-exponentiation-operator': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-export-namespace-from': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-for-of': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-function-name': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-json-strings': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-literals': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-logical-assignment-operators': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-member-expression-literals': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-modules-amd': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-modules-systemjs': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-modules-umd': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.4) - '@babel/plugin-transform-new-target': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-numeric-separator': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-object-rest-spread': 7.24.5(@babel/core@7.24.4) - '@babel/plugin-transform-object-super': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-optional-catch-binding': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.4) - '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.24.4) - '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-private-property-in-object': 7.24.5(@babel/core@7.24.4) - '@babel/plugin-transform-property-literals': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-regenerator': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-reserved-words': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-shorthand-properties': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-spread': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-sticky-regex': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-template-literals': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-typeof-symbol': 7.24.5(@babel/core@7.24.4) - '@babel/plugin-transform-unicode-escapes': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-unicode-property-regex': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-unicode-regex': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-unicode-sets-regex': 7.24.1(@babel/core@7.24.4) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.4) - babel-plugin-polyfill-corejs2: 0.4.10(@babel/core@7.24.4) - babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.4) - babel-plugin-polyfill-regenerator: 0.6.1(@babel/core@7.24.4) - core-js-compat: 3.36.1 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.4): + '@babel/preset-modules@0.1.6-no-external-plugins': resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/types': 7.24.5 - esutils: 2.0.3 - dev: true - /@babel/preset-typescript@7.24.1(@babel/core@7.24.4): - resolution: {integrity: sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==} + '@babel/preset-typescript@7.27.0': + resolution: {integrity: sha512-vxaPFfJtHhgeOVXRKuHpHPAOgymmy8V8I65T1q53R7GCZlefKeCaTyDs3zOPHTTbmquvNlQYC5klEvWsBAtrBQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.4 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-typescript': 7.24.4(@babel/core@7.24.4) - dev: true - /@babel/regjsgen@0.8.0: - resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} - dev: true - - /@babel/runtime-corejs2@7.23.9: - resolution: {integrity: sha512-lwwDy5QfMkO2rmSz9AvLj6j2kWt5a4ulMi1t21vWQEO0kNCFslHoszat8reU/uigIQSUDF31zraZG/qMkcqAlw==} + '@babel/runtime-corejs2@7.27.0': + resolution: {integrity: sha512-89TgomkhiBKJ1QN/zPJbSW6M3T9caLoSDYsHFNlTI2Q+T12w8ehZeEnx54I79gB0kmM+etCC5Lfgv95rYUJPdQ==} engines: {node: '>=6.9.0'} - dependencies: - core-js: 2.6.12 - regenerator-runtime: 0.14.1 - dev: false - /@babel/runtime@7.24.4: - resolution: {integrity: sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==} + '@babel/runtime@7.27.0': + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.14.1 - dev: true - /@babel/template@7.24.0: - resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} + '@babel/template@7.27.0': + resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.24.2 - '@babel/parser': 7.24.4 - '@babel/types': 7.24.5 - dev: true - /@babel/traverse@7.24.1: - resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} + '@babel/traverse@7.27.0': + resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.4 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/parser': 7.24.4 - '@babel/types': 7.24.5 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/types@7.24.5: - resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==} + '@babel/types@7.27.0': + resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.24.1 - '@babel/helper-validator-identifier': 7.24.5 - to-fast-properties: 2.0.0 - dev: true - /@bcoe/v8-coverage@0.2.3: + '@bbob/core@4.2.0': + resolution: {integrity: sha512-i5VUIO6xx+TCrBE8plQF9KpW1z+0QcCh9GawCCWYG9KcZrEsyRy57my5/kem0a2lFxXjh0o+tgjQY9sCP5b3bw==} + + '@bbob/html@4.2.0': + resolution: {integrity: sha512-mZG7MmE/YluH1/FNCr2Jv/Vz3Mq5stZ5tFAjBmYpVVN6Ndus4+FA84vKoHFdUHDiuU1p/e2/o/VUrwsWwLdoIA==} + + '@bbob/parser@4.2.0': + resolution: {integrity: sha512-l8BppXjdQClrUiv+qIN0Oe6aS/vlH7CEduMreDaxYDadUPC0RMzxj9lXVjO0xTbFEVEIUJCGc53qnpiApLbx2g==} + + '@bbob/plugin-helper@4.2.0': + resolution: {integrity: sha512-Uxs/UJROnkpcq5EJfz/8NCEYAcme8l6oAgMeWLX1nxWeieUhRgpY8BWQ9eQwUOXEKa0tsKVPnlmbUAjFhppVPw==} + + '@bbob/preset-html5@4.2.0': + resolution: {integrity: sha512-X2EIeb2vqTz/n34KWQXEL5IC0SuB6i2/ltRpEaMTyK7IoDEs0XGIWcqVpenb7Z2d4eNA3gTWhkoUUkKlZmfQRQ==} + + '@bbob/preset@4.2.0': + resolution: {integrity: sha512-IA+kxlrRcYBXE634B8W6uU2DO7VvrxeOqWUzIDKV2CgzTMpmCG875ihGp+Q3T+QxGwzG5S0L4GdmCIULTbzC4A==} + + '@bbob/types@4.2.0': + resolution: {integrity: sha512-bSCZnNg0VrPosBBUVkjBDYVKNEqwT0jTNuzvAuNrZkuILgUKtLYI9GHuT8rY4lZecCb7UqGDsHsYt0YJO36eJg==} + + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - dev: true - /@bundled-es-modules/cookie@2.0.0: - resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} - dependencies: - cookie: 0.5.0 - dev: true + '@bundled-es-modules/cookie@2.0.1': + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} - /@bundled-es-modules/statuses@1.0.1: + '@bundled-es-modules/statuses@1.0.1': resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} - dependencies: - statuses: 2.0.1 - dev: true - /@colors/colors@1.6.0: + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} - dev: false - /@cryptography/aes@0.1.1: + '@cryptography/aes@0.1.1': resolution: {integrity: sha512-PcYz4FDGblO6tM2kSC+VzhhK62vml6k6/YAkiWtyPvrgJVfnDRoHGDtKn5UiaRRUrvUTTocBpvc2rRgTCqxjsg==} - dev: false - /@dabh/diagnostics@2.0.3: - resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} - dependencies: - colorspace: 1.1.4 - enabled: 2.0.0 - kuler: 2.0.0 - dev: false + '@csstools/color-helpers@5.0.2': + resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.2': + resolution: {integrity: sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-color-parser@3.0.8': + resolution: {integrity: sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 - /@esbuild/aix-ppc64@0.20.2: - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - requiresBuild: true - optional: true - /@esbuild/android-arm64@0.20.2: - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + '@esbuild/aix-ppc64@0.25.1': + resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] - requiresBuild: true - optional: true - /@esbuild/android-arm@0.20.2: - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + '@esbuild/android-arm64@0.25.1': + resolution: {integrity: sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] - requiresBuild: true - optional: true - /@esbuild/android-x64@0.20.2: - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + '@esbuild/android-arm@0.25.1': + resolution: {integrity: sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] - requiresBuild: true - optional: true - /@esbuild/darwin-arm64@0.20.2: - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + '@esbuild/android-x64@0.25.1': + resolution: {integrity: sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - requiresBuild: true - optional: true - /@esbuild/darwin-x64@0.20.2: - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + '@esbuild/darwin-arm64@0.25.1': + resolution: {integrity: sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] - requiresBuild: true - optional: true - /@esbuild/freebsd-arm64@0.20.2: - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + '@esbuild/darwin-x64@0.25.1': + resolution: {integrity: sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] - requiresBuild: true - optional: true - /@esbuild/freebsd-x64@0.20.2: - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + '@esbuild/freebsd-arm64@0.25.1': + resolution: {integrity: sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] - requiresBuild: true - optional: true - /@esbuild/linux-arm64@0.20.2: - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + '@esbuild/freebsd-x64@0.25.1': + resolution: {integrity: sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-arm@0.20.2: - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + '@esbuild/linux-arm64@0.25.1': + resolution: {integrity: sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-ia32@0.20.2: - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + '@esbuild/linux-arm@0.25.1': + resolution: {integrity: sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-loong64@0.20.2: - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + '@esbuild/linux-ia32@0.25.1': + resolution: {integrity: sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-mips64el@0.20.2: - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + '@esbuild/linux-loong64@0.25.1': + resolution: {integrity: sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-ppc64@0.20.2: - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + '@esbuild/linux-mips64el@0.25.1': + resolution: {integrity: sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-riscv64@0.20.2: - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + '@esbuild/linux-ppc64@0.25.1': + resolution: {integrity: sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-s390x@0.20.2: - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + '@esbuild/linux-riscv64@0.25.1': + resolution: {integrity: sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-x64@0.20.2: - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + '@esbuild/linux-s390x@0.25.1': + resolution: {integrity: sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] - requiresBuild: true - optional: true - /@esbuild/netbsd-x64@0.20.2: - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + '@esbuild/linux-x64@0.25.1': + resolution: {integrity: sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.1': + resolution: {integrity: sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - requiresBuild: true - optional: true - /@esbuild/openbsd-x64@0.20.2: - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + '@esbuild/netbsd-x64@0.25.1': + resolution: {integrity: sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.1': + resolution: {integrity: sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - requiresBuild: true - optional: true - /@esbuild/sunos-x64@0.20.2: - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + '@esbuild/openbsd-x64@0.25.1': + resolution: {integrity: sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - requiresBuild: true - optional: true - /@esbuild/win32-arm64@0.20.2: - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + '@esbuild/sunos-x64@0.25.1': + resolution: {integrity: sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] - requiresBuild: true - optional: true - /@esbuild/win32-ia32@0.20.2: - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + '@esbuild/win32-arm64@0.25.1': + resolution: {integrity: sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] - requiresBuild: true - optional: true - /@esbuild/win32-x64@0.20.2: - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + '@esbuild/win32-ia32@0.25.1': + resolution: {integrity: sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} cpu: [x64] os: [win32] - requiresBuild: true - optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + '@esbuild/win32-x64@0.25.1': + resolution: {integrity: sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.5.1': + resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 8.57.0 - eslint-visitor-keys: 3.4.3 - dev: true - /@eslint-community/regexpp@4.10.0: - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true - /@eslint/eslintrc@2.1.4: + '@eslint/config-array@0.19.2': + resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.2.0': + resolution: {integrity: sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.12.0': + resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@2.1.4': resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.1 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - /@eslint/js@8.57.0: - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /@hono/node-server@1.11.1: - resolution: {integrity: sha512-GW1Iomhmm1o4Z+X57xGby8A35Cu9UZLL7pSMdqDBkD99U5cywff8F+8hLk5aBTzNubnsFAvWQ/fZjNwPsEn9lA==} - engines: {node: '>=18.14.1'} - dev: false + '@eslint/js@9.23.0': + resolution: {integrity: sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - /@hono/swagger-ui@0.2.2(hono@4.2.9): - resolution: {integrity: sha512-Bco6XdKkTP7yIVWXpquLQayvjwzDmsmsESjF7VcrU/ZPTYafGvvpHf7Z6PHTWyp+JpdYORZXlyZ75T3At2KfAA==} + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.7': + resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@hono/node-server@1.14.0': + resolution: {integrity: sha512-YUCxJwgHRKSqjrdTk9e4VMGKN27MK5r4+MGPyZTgKH+IYbK+KtYbHeOcPGJ91KGGD6RIQiz2dAHxvjauNhOS8g==} + engines: {node: '>=18.14.1'} peerDependencies: - hono: '*' - dependencies: - hono: 4.2.9 - dev: false + hono: ^4 - /@hono/zod-openapi@0.11.0(hono@4.2.9)(zod@3.23.5): - resolution: {integrity: sha512-thbxV4lWJoDo1NjF8ZGnd0muD3UHUpRqpKvS3RI+kWCXU05nyuViymUbPvVpp+O6i5SjovITTF91NRMTraZm3Q==} + '@hono/zod-openapi@0.19.2': + resolution: {integrity: sha512-lkFa6wdQVgY7d7/m++Ixr3hvKCF5Y+zjTIPM37fex5ylCfX53A/W28gZRDuFZx3aR+noKob7lHfwdk9dURLzxw==} engines: {node: '>=16.0.0'} peerDependencies: - hono: '>=3.11.3' + hono: '>=4.3.6' zod: 3.* - dependencies: - '@asteasolutions/zod-to-openapi': 7.0.0(zod@3.23.5) - '@hono/zod-validator': 0.2.1(hono@4.2.9)(zod@3.23.5) - hono: 4.2.9 - zod: 3.23.5 - dev: false - /@hono/zod-validator@0.2.1(hono@4.2.9)(zod@3.23.5): - resolution: {integrity: sha512-HFoxln7Q6JsE64qz2WBS28SD33UB2alp3aRKmcWnNLDzEL1BLsWfbdX6e1HIiUprHYTIXf5y7ax8eYidKUwyaA==} + '@hono/zod-validator@0.4.3': + resolution: {integrity: sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ==} peerDependencies: hono: '>=3.9.0' zod: ^3.19.1 - dependencies: - hono: 4.2.9 - zod: 3.23.5 - dev: false - /@humanwhocodes/config-array@0.11.14: - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true + deprecated: Use @eslint/config-array instead - /@humanwhocodes/module-importer@1.0.1: + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - dev: true - /@humanwhocodes/object-schema@2.0.2: - resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} - dev: true + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} - /@ianvs/eslint-stats@2.0.0: + '@humanwhocodes/retry@0.4.2': + resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + engines: {node: '>=18.18'} + + '@ianvs/eslint-stats@2.0.0': resolution: {integrity: sha512-DnIVVAiXR4tfWERTiQxr1Prrs/uFEbC1C4gTGORMvbF4k7ENyVQeLcoUfNyhlAj2MB/OeorCrN3wSnYuDOUS6Q==} engines: {node: '>=8.0.0'} - dependencies: - chalk: 2.4.2 - lodash: 4.17.21 - dev: true - /@inquirer/confirm@3.1.1: - resolution: {integrity: sha512-epf2RVHJJxX5qF85U41PBq9qq2KTJW9sKNLx6+bb2/i2rjXgeoHVGUm8kJxZHavrESgXgBLKCABcfOJYIso8cQ==} + '@inquirer/confirm@3.2.0': + resolution: {integrity: sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==} engines: {node: '>=18'} - dependencies: - '@inquirer/core': 7.1.1 - '@inquirer/type': 1.2.1 - dev: true - /@inquirer/core@7.1.1: - resolution: {integrity: sha512-rD1UI3eARN9qJBcLRXPOaZu++Bg+xsk0Tuz1EUOXEW+UbYif1sGjr0Tw7lKejHzKD9IbXE1CEtZ+xR/DrNlQGQ==} + '@inquirer/core@9.2.1': + resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} + engines: {node: '>=18'} + + '@inquirer/figures@1.0.11': + resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} + engines: {node: '>=18'} + + '@inquirer/type@1.5.5': + resolution: {integrity: sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==} engines: {node: '>=18'} - dependencies: - '@inquirer/type': 1.2.1 - '@types/mute-stream': 0.0.4 - '@types/node': 20.12.8 - '@types/wrap-ansi': 3.0.0 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-spinners: 2.9.2 - cli-width: 4.1.0 - figures: 3.2.0 - mute-stream: 1.0.0 - signal-exit: 4.1.0 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - dev: true - /@inquirer/type@1.2.1: - resolution: {integrity: sha512-xwMfkPAxeo8Ji/IxfUSqzRi0/+F2GIqJmpc5/thelgMGsjNZcjDDRBO9TLXT1s/hdx/mK5QbVIvgoLIFgXhTMQ==} + '@inquirer/type@2.0.0': + resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} engines: {node: '>=18'} - dev: true - /@ioredis/commands@1.2.0: + '@ioredis/commands@1.2.0': resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} - dev: false - /@isaacs/cliui@8.0.2: + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - dependencies: - string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: /strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 - dev: true - /@istanbuljs/schema@0.1.3: + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - dev: true - - /@jest/schemas@29.6.3: - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@sinclair/typebox': 0.27.8 - dev: true - /@jridgewell/gen-mapping@0.3.5: - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.25 - dev: true - /@jridgewell/resolve-uri@3.1.2: + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - dev: true - /@jridgewell/set-array@1.2.1: + '@jridgewell/set-array@1.2.1': resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - dev: true - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@lifeomic/attempt@3.1.0': + resolution: {integrity: sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==} + + '@mapbox/node-pre-gyp@2.0.0': + resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==} + engines: {node: '>=18'} + hasBin: true + + '@microsoft/eslint-formatter-sarif@3.1.0': + resolution: {integrity: sha512-/mn4UXziHzGXnKCg+r8HGgPy+w4RzpgdoqFuqaKOqUVBT5x2CygGefIrO4SusaY7t0C4gyIWMNu6YQT6Jw64Cw==} + engines: {node: '>= 14'} + + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + + '@mswjs/interceptors@0.29.1': + resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} + engines: {node: '>=18'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/es-set-tostringtag@1.0.44': + resolution: {integrity: sha512-Qfiv/3wI+mKSPCgU8Fg/7Auu4os00St4GwyLqCCZ/oBD4W00itWUkl9Rq1MCGS+VXYDnTobvrc6AvPagwnT2pg==} + engines: {node: '>=12.4.0'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@nolyfill/safe-buffer@1.0.44': + resolution: {integrity: sha512-SqlKXtlhNTDMeZKey9jnnuPhi8YTl1lJuEcY9zbm5i4Pqe79UJJ8IJ9oiD6DhgI8KjYc+HtLzpQJNRdNYqb/hw==} + engines: {node: '>=12.4.0'} + + '@nolyfill/safer-buffer@1.0.44': + resolution: {integrity: sha512-Ouw1fMwjAy1V4MpnDASfu1DCPgkP0nNFteiiWbFoEGSqa7Vnmkb6if2c522N2WcMk+RuaaabQbC1F1D4/kTXcg==} + engines: {node: '>=12.4.0'} + + '@nolyfill/side-channel@1.0.44': + resolution: {integrity: sha512-y3SvzjuY1ygnzWA4Krwx/WaJAsTMP11DN+e21A8Fa8PW1oDtVB5NSRW7LWurAiS2oKRkuCgcjTYMkBuBkcPCRg==} + engines: {node: '>=12.4.0'} + + '@notionhq/client@2.3.0': + resolution: {integrity: sha512-l7WqTCpQqC+HibkB9chghONQTYcxNQT0/rOJemBfmuKQRTu2vuV8B3yA395iKaUdDo7HI+0KvQaz9687Xskzkw==} + engines: {node: '>=12'} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@opentelemetry/api-logs@0.200.0': + resolution: {integrity: sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.0.0': + resolution: {integrity: sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-prometheus@0.200.0': + resolution: {integrity: sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.200.0': + resolution: {integrity: sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-amqplib@0.46.1': + resolution: {integrity: sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.43.1': + resolution: {integrity: sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dataloader@0.16.1': + resolution: {integrity: sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.47.1': + resolution: {integrity: sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fastify@0.44.2': + resolution: {integrity: sha512-arSp97Y4D2NWogoXRb8CzFK3W2ooVdvqRRtQDljFt9uC3zI6OuShgey6CVFC0JxT1iGjkAr1r4PDz23mWrFULQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.19.1': + resolution: {integrity: sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.43.1': + resolution: {integrity: sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.47.1': + resolution: {integrity: sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.45.2': + resolution: {integrity: sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.57.2': + resolution: {integrity: sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.47.1': + resolution: {integrity: sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.7.1': + resolution: {integrity: sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.44.1': + resolution: {integrity: sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.47.1': + resolution: {integrity: sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1': + resolution: {integrity: sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.52.0': + resolution: {integrity: sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.46.1': + resolution: {integrity: sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.45.2': + resolution: {integrity: sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.45.1': + resolution: {integrity: sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.51.1': + resolution: {integrity: sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis-4@0.46.1': + resolution: {integrity: sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.18.1': + resolution: {integrity: sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.10.1': + resolution: {integrity: sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.200.0': + resolution: {integrity: sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.200.0': + resolution: {integrity: sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/redis-common@0.36.2': + resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} + engines: {node: '>=14'} + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@2.0.0': + resolution: {integrity: sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.200.0': + resolution: {integrity: sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.0.0': + resolution: {integrity: sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.0.0': + resolution: {integrity: sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.30.0': + resolution: {integrity: sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.40.1': + resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + + '@otplib/core@12.0.1': + resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} + + '@otplib/plugin-crypto@12.0.1': + resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==} + + '@otplib/plugin-thirty-two@12.0.1': + resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==} + + '@otplib/preset-default@12.0.1': + resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==} + + '@otplib/preset-v11@12.0.1': + resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.0': + resolution: {integrity: sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@postlight/ci-failed-test-reporter@1.0.26': + resolution: {integrity: sha512-xfXzxyOiKhco7Gx2OLTe9b66b0dFJw0elg94KGHoQXf5F8JqqFvdo35J8wayGOor64CSMvn+4Bjlu2NKV+yTGA==} + hasBin: true + + '@postlight/parser@2.2.3': + resolution: {integrity: sha512-4/syRvqJARgLN4yH8qtl634WO0+KINjkijU/SmhCJqqh8/aOfv5uQf+SquFpA+JwsAsbGzYQkIxSum29riOreg==} + engines: {node: '>=10'} + hasBin: true + bundledDependencies: + - jquery + - moment-timezone + - browser-request + + '@postman/form-data@3.1.1': + resolution: {integrity: sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==} + engines: {node: '>= 6'} + + '@postman/tough-cookie@4.1.3-postman.1': + resolution: {integrity: sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==} + engines: {node: '>=6'} + + '@postman/tunnel-agent@0.6.4': + resolution: {integrity: sha512-CJJlq8V7rNKhAw4sBfjixKpJW00SHqebqNUQKxMoepgeWZIbdPcD+rguRcivGhS4N12PymDcKgUgSD4rVC+RjQ==} + + '@prisma/instrumentation@6.5.0': + resolution: {integrity: sha512-morJDtFRoAp5d/KENEm+K6Y3PQcn5bCvpJ5a9y3V3DNMrNy/ZSn2zulPGj+ld+Xj2UYVoaMJ8DpBX/o6iF6OiA==} + peerDependencies: + '@opentelemetry/api': ^1.8 + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@puppeteer/browsers@2.2.0': + resolution: {integrity: sha512-MC7LxpcBtdfTbzwARXIkqGZ1Osn3nnZJlm+i0+VqHl72t//Xwl9wICrXT8BwtgC6s1xJNHsxOpvzISUqe92+sw==} + engines: {node: '>=18'} + hasBin: true + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.37.0': + resolution: {integrity: sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.37.0': + resolution: {integrity: sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.37.0': + resolution: {integrity: sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.37.0': + resolution: {integrity: sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.37.0': + resolution: {integrity: sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.37.0': + resolution: {integrity: sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.37.0': + resolution: {integrity: sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.37.0': + resolution: {integrity: sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.37.0': + resolution: {integrity: sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.37.0': + resolution: {integrity: sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.37.0': + resolution: {integrity: sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.37.0': + resolution: {integrity: sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.37.0': + resolution: {integrity: sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.37.0': + resolution: {integrity: sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.37.0': + resolution: {integrity: sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.37.0': + resolution: {integrity: sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.37.0': + resolution: {integrity: sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.37.0': + resolution: {integrity: sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.37.0': + resolution: {integrity: sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.37.0': + resolution: {integrity: sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==} + cpu: [x64] + os: [win32] + + '@rss3/api-core@0.0.25': + resolution: {integrity: sha512-YVH1QwF4P4r1JCYkkfK+UrKsa2PlJCOjViKhybRUanyR8TwwebpFPV+f5rcDDLRISlCdvhErpiFk9TntstYiCw==} + + '@rss3/api-utils@0.0.25': + resolution: {integrity: sha512-tQtyjbPOYRaxgT0QIDq0/QV/vVwDp0CHQsVF+w+wkHu9pMnGTNi46WcK4L8n21fIoiOV2DLUuV+YN1C00w5gaA==} + + '@rss3/sdk@0.0.25': + resolution: {integrity: sha512-jyXT4YTwefxxRZ0tt5xjbnw8e7zPg2OGdo/0xb+h/7qWnMNhLtWpc95DsYs/1C/I0rIyiDpZBhLI2DieQ9y+tw==} + + '@scalar/core@0.2.4': + resolution: {integrity: sha512-XGrg2P+FrvtzLsDm6TVl1oZv4DYmkFqJ3sI/SS2mjRlfOcblHv2CcrhYTz0+y1nG3cyHlE5esHAK0+BEI032dA==} + engines: {node: '>=18'} + + '@scalar/hono-api-reference@0.7.4': + resolution: {integrity: sha512-Uo2TpPdQbhnDkmTnVeTIXEXL30YY0GvBRgOo6mv1pIzZrYbTSJqxfg9um1/5wXZ6fh6w6GI3Y/tdu0GcMg5fJw==} + engines: {node: '>=18'} + peerDependencies: + hono: ^4.0.0 + + '@scalar/openapi-types@0.1.9': + resolution: {integrity: sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g==} + engines: {node: '>=18'} + + '@scalar/types@0.1.4': + resolution: {integrity: sha512-IAxpfrfdYfliLJR6WbuC8hxUwBUOeVsGuZxQE+zP8JDtdoHmgT6aNxCqMnGZ1ft6dvJ4jvzVR6qCWrq6Kg25oA==} + engines: {node: '>=18'} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + + '@sentry/core@9.10.1': + resolution: {integrity: sha512-TE2zZV3Od4131mZNgFo2Mv4aKU8FXxL0s96yqRvmV+8AU57mJoycMXBnmNSYfWuDICbPJTVAp+3bYMXwX7N5YA==} + engines: {node: '>=18'} + + '@sentry/node@9.10.1': + resolution: {integrity: sha512-salNc4R0GiZZNNScNpdAB3OI3kz+clmgXL1rl5O2Kh1IW5vftf5I69n+qqZLJ3kaUp0Sm6V+deCHyUOnw9GozA==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@9.10.1': + resolution: {integrity: sha512-qqcsbIyoOPI91Tm6w0oFzsx/mlu+lywRGSVbPRFhk4zCXBOhCCp4Mg7nwKK0wGJ7AZRl6qtELrRSGClAthC55g==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 + '@opentelemetry/core': ^1.30.1 + '@opentelemetry/instrumentation': ^0.57.1 + '@opentelemetry/sdk-trace-base': ^1.30.1 + '@opentelemetry/semantic-conventions': ^1.28.0 + + '@sindresorhus/is@5.6.0': + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + + '@sindresorhus/is@7.0.1': + resolution: {integrity: sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ==} + engines: {node: '>=18'} + + '@stylistic/eslint-plugin@4.2.0': + resolution: {integrity: sha512-8hXezgz7jexGHdo5WN6JBEIPHCSFyyU4vgbxevu4YLVS5vl+sxqAAGyXSzfNDyR6xMNSH5H1x67nsXcYMOHtZA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=9.0.0' + + '@szmarczak/http-timer@5.0.1': + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + + '@tonyrl/rand-user-agent@2.0.83': + resolution: {integrity: sha512-FqifQhfeJ1WojY8j6s7D3qcs20KhZwVggirT7btqi7g5bSTbbD4v0cuhxHaASyHlv3R757Jum2ro03dRVUXgVw==} + engines: {node: '>=14.16'} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@types/aes-js@3.1.4': + resolution: {integrity: sha512-v3D66IptpUqh+pHKVNRxY8yvp2ESSZXe0rTzsGdzUhEwag7ljVfgCllkWv2YgiYXDhWFBrEywll4A5JToyTNFA==} + + '@types/babel__preset-env@7.10.0': + resolution: {integrity: sha512-LS8hRb/8TQir2f8W9/s5enDtrRS2F/6fsdkVw5ePHp6Q8SrSJHOGtWnP93ryaYMmg2du03vOsiGrl5mllz4uDA==} + + '@types/bluebird@3.5.42': + resolution: {integrity: sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==} + + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + + '@types/chance@1.1.6': + resolution: {integrity: sha512-V+pm3stv1Mvz8fSKJJod6CglNGVqEQ6OyuqitoDkWywEODM/eJd1eSuIp9xt6DrX8BWZ2eDSIzbw1tPCUTvGbQ==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/etag@1.8.3': + resolution: {integrity: sha512-QYHv9Yeh1ZYSMPQOoxY4XC4F1r+xRUiAriB303F4G6uBsT3KKX60DjiogvVv+2VISVDuJhcIzMdbjT+Bm938QQ==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/html-to-text@9.0.4': + resolution: {integrity: sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/imapflow@1.0.20': + resolution: {integrity: sha512-kmBeiV815byuxYlu2lomAx3VY3SsyOYg4rDsK5vT6CGZ6Sow2AUEZwieql8uAizO6+p4sQchw/g3vQX76XBIpA==} + + '@types/js-beautify@1.14.3': + resolution: {integrity: sha512-FMbQHz+qd9DoGvgLHxeqqVPaNRffpIu5ZjozwV8hf9JAGpIOzuAf4wGbRSo8LNITHqGjmmVjaMggTT5P4v4IHg==} + + '@types/jsdom@21.1.7': + resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} + + '@types/json-bigint@1.0.4': + resolution: {integrity: sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/jsrsasign@10.5.13': + resolution: {integrity: sha512-vvVHLrXxoUZgBWTcJnTMSC4FAQcG2loK7N1Uy20I3nr/aUhetbGdfuwSzXkrMoll2RoYKW0IcMIN0I0bwMwVMQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/lint-staged@13.3.0': + resolution: {integrity: sha512-WxGjVP+rA4OJlEdbZdT9MS9PFKQ7kVPhLn26gC+2tnBWBEFEj/KW+IbFfz6sxdxY5U6V7BvyF+3BzCGsAMHhNg==} + + '@types/mailparser@3.4.5': + resolution: {integrity: sha512-EPERBp7fLeFZh7tS2X36MF7jawUx3Y6/0rXciZah3CTYgwLi3e0kpGUJ6FOmUabgzis/U1g+3/JzrVWbWIOGjg==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/module-alias@2.0.4': + resolution: {integrity: sha512-5+G/QXO/DvHZw60FjvbDzO4JmlD/nG5m2/vVGt25VN1eeP3w2bCoks1Wa7VuptMPM1TxJdx6RjO70N9Fw0nZPA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/mute-stream@0.0.4': + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + + '@types/mysql@2.15.26': + resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} + + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + + '@types/node@22.13.15': + resolution: {integrity: sha512-imAbQEEbVni6i6h6Bd5xkCRwLqFc8hihCsi2GbtDoAtUcAFQ6Zs4pFXTZUUbroTkXdImczWM9AI8eZUuybXE3w==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/pg-pool@2.0.6': + resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + + '@types/pg@8.6.1': + resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + + '@types/request-promise@4.1.51': + resolution: {integrity: sha512-qVcP9Fuzh9oaAh8oPxiSoWMFGnWKkJDknnij66vi09Yiy62bsSDqtd+fG5kIM9wLLgZsRP3Y6acqj9O/v2ZtRw==} + + '@types/request@2.48.12': + resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==} + + '@types/sanitize-html@2.15.0': + resolution: {integrity: sha512-71Z6PbYsVKfp4i6Jvr37s5ql6if1Q/iJQT80NbaSi7uGaG8CqBMXP0pk/EsURAOuGdk5IJCd/vnzKrR7S3Txsw==} + + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + + '@types/statuses@2.0.5': + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@6.0.3': + resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + + '@types/tiny-async-pool@2.0.3': + resolution: {integrity: sha512-n3l1s538tKo9RBoHs4I3DG/VmD3VYhF5mHcgu1sU4Lq7JCNBtxnpBy3OkWSbZsp5r5QOuplh2UkXXXwufoAuNQ==} + + '@types/title@3.4.3': + resolution: {integrity: sha512-mjupLOb4kwUuoUFokkacy/VMRVBH2qtqZ5AX7K7iha6+iKIkX80n/Y4EoNVEVRmer8dYJU/ry+fppUaDFVQh7Q==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/wrap-ansi@3.0.0': + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@typescript-eslint/eslint-plugin@8.29.0': + resolution: {integrity: sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@8.29.0': + resolution: {integrity: sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@8.28.0': + resolution: {integrity: sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.29.0': + resolution: {integrity: sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.29.0': + resolution: {integrity: sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@8.28.0': + resolution: {integrity: sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.29.0': + resolution: {integrity: sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.28.0': + resolution: {integrity: sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/typescript-estree@8.29.0': + resolution: {integrity: sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.28.0': + resolution: {integrity: sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.29.0': + resolution: {integrity: sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@8.28.0': + resolution: {integrity: sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.29.0': + resolution: {integrity: sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unhead/schema@1.11.20': + resolution: {integrity: sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==} + + '@vercel/nft@0.29.2': + resolution: {integrity: sha512-A/Si4mrTkQqJ6EXJKv5EYCDQ3NL6nJXxG8VGXePsaiQigsomHYQC9xSpX8qGk7AEZk4b1ssbYIqJ0ISQQ7bfcA==} + engines: {node: '>=18'} + hasBin: true + + '@vitest/coverage-v8@2.1.9': + resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} + peerDependencies: + '@vitest/browser': 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + abbrev@3.0.0: + resolution: {integrity: sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA==} + engines: {node: ^18.17.0 || >=20.5.0} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@5.7.4: + resolution: {integrity: sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + aes-js@3.1.2: + resolution: {integrity: sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + + art-template@4.13.2: + resolution: {integrity: sha512-04ws5k+ndA5DghfheY4c8F1304XJKeTcaXqZCLpxFkNMSkaR3ChW1pX2i9d3sEEOZuLy7de8lFriRaik1jEeOQ==} + engines: {node: '>= 1.0.0'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + async-mutex@0.3.2: + resolution: {integrity: sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==} + + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + + babel-plugin-polyfill-corejs2@0.4.13: + resolution: {integrity: sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.11.1: + resolution: {integrity: sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.4: + resolution: {integrity: sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-events@2.5.4: + resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} + + bare-fs@2.3.5: + resolution: {integrity: sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==} + + bare-os@2.4.4: + resolution: {integrity: sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==} + + bare-path@2.1.3: + resolution: {integrity: sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==} + + bare-stream@2.6.5: + resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird@2.11.0: + resolution: {integrity: sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bufferutil@4.0.9: + resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} + engines: {node: '>=6.14.2'} + + builtin-modules@5.0.0: + resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} + engines: {node: '>=18.20'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + + cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + + cacheable-request@12.0.1: + resolution: {integrity: sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==} + engines: {node: '>=18'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@3.0.0: + resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + + camelcase-keys@7.0.2: + resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} + engines: {node: '>=12'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001707: + resolution: {integrity: sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + + chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chance@1.1.12: + resolution: {integrity: sha512-vVBIGQVnwtUG+SYe0ge+3MvF78cvSpuCOEUJr7sVEk2vSBuMW6OXNJjSzdtzrlxNUEaoqH2GBd5Y/+18BEB01Q==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@0.22.0: + resolution: {integrity: sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==} + engines: {node: '>= 0.6'} + + cheerio@1.0.0: + resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} + engines: {node: '>=18.17'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chromium-bidi@0.5.16: + resolution: {integrity: sha512-IT5lnR44h/qZQ4GaCHvBxYIl4cQL2i9UvFyYeRyVdcpY04hx5H720HQfe/7Oz7ndxaYVLQFGpCO71J4X2Ye/Gw==} + peerDependencies: + devtools-protocol: '*' + + chrono-node@2.7.9: + resolution: {integrity: sha512-PW3tzuztH7OFbwdCCwv1k8F6ALFs5Yet1Neh5JJBL1GGj8zsLj3ZgZU6StUyM6gSsVRMv8EE6LqpTjM52Mshrw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + ci-info@4.2.0: + resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==} + engines: {node: '>=8'} + + city-timezones@1.3.0: + resolution: {integrity: sha512-S/FiU8F/1HgMvbd8POvb+8xorp0tp5VJwUfYC/ssnbxykLbwEZ9poZWFMPfBVuh1KlXxP63DGCkdr0D8aFEADQ==} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + class-transformer@0.3.1: + resolution: {integrity: sha512-cKFwohpJbuMovS8xVLmn8N2AUbAuc8pVo4zEfsUVo8qgECOogns1WVk/FkOZoxhOPTyTYFckuoH+13FO+MQ8GA==} + + clean-css@4.2.4: + resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} + engines: {node: '>= 4.0'} + + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + clipboardy@4.0.0: + resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} + engines: {node: '>=18'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-deep@0.2.4: + resolution: {integrity: sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==} + engines: {node: '>=0.10.0'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + core-js-compat@3.41.0: + resolution: {integrity: sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==} + + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + css-select@1.2.0: + resolution: {integrity: sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-what@2.1.3: + resolution: {integrity: sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssstyle@4.3.0: + resolution: {integrity: sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==} + engines: {node: '>=18'} + + currency-symbol-map@5.1.0: + resolution: {integrity: sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw==} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + dayjs@1.11.8: + resolution: {integrity: sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + + decode-named-character-reference@1.1.0: + resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + decode-uri-component@0.4.1: + resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} + engines: {node: '>=14.16'} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + devtools-protocol@0.0.1262051: + resolution: {integrity: sha512-YJe4CT5SA8on3Spa+UDtNhEqtuV6Epwz3OZ4HQVLhlRccpZ9/PAYk0/cy/oKxFKRrZPBUPyxympQci4yWNWZ9g==} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + difflib@https://codeload.github.com/postlight/difflib.js/tar.gz/32e8e38c7fcd935241b9baab71bb432fd9b166ed: + resolution: {tarball: https://codeload.github.com/postlight/difflib.js/tar.gz/32e8e38c7fcd935241b9baab71bb432fd9b166ed} + version: 0.2.6 + + directory-import@3.3.2: + resolution: {integrity: sha512-dSApZXgx29qO/6AVigdsoC6HSvaHWinJ4HTRPKrlMAxX71FgPzn/WEWbgM+aB1PlKD9IfSC3Ir2ouYYQR1uy+g==} + engines: {node: '>=18.17.0'} + + discord-api-types@0.37.119: + resolution: {integrity: sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-serializer@0.1.1: + resolution: {integrity: sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@1.3.1: + resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@2.4.2: + resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@1.5.1: + resolution: {integrity: sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==} + + domutils@1.7.0: + resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + dotenv@6.2.0: + resolution: {integrity: sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==} + engines: {node: '>=6'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + + electron-to-chromium@1.5.128: + resolution: {integrity: sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==} + + ellipsize@0.1.0: + resolution: {integrity: sha512-5gxbEjcb/Z2n6TTmXZx9wVi3N/DOzE7RXY3Xg9dakDuhX/izwumB9rGjeWUV6dTA0D0+juvo+JonZgNR9sgA5A==} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encoding-japanese@2.2.0: + resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==} + engines: {node: '>=8.10.0'} + + encoding-sniffer@0.2.0: + resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + + entities@1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.0: + resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-module-lexer@1.6.0: + resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.1: + resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@1.14.3: + resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} + engines: {node: '>=4.0'} + hasBin: true + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-compat-utils@0.6.4: + resolution: {integrity: sha512-/u+GQt8NMfXO8w17QendT4gvO5acfxQsAKirAt0LVxDnr2N8YLCVbregaNc/Yhp7NM128DwCaRvr8PLDfeNkQw==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-prettier@10.1.1: + resolution: {integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-filtered-fix@0.3.0: + resolution: {integrity: sha512-UMHOza9epEn9T+yVT8RiCFf0JdALpVzmoH62Ez/zvxM540IyUNAkr7aH2Frkv6zlm9a/gbmq/sc7C4SvzZQXcA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-formatter-friendly@7.0.0: + resolution: {integrity: sha512-WXg2D5kMHcRxIZA3ulxdevi8/BGTXu72pfOO5vXHqcAfClfIWDSlOljROjCSOCcKvilgmHz1jDWbvFCZHjMQ5w==} + engines: {node: '>=0.10.0'} + + eslint-nibble@8.1.0: + resolution: {integrity: sha512-x9H/1oeuKdC0HsaWeBarOryqNLC+7QZfAZIAP0HnGcmiiPktFIQq/D0e+iiCSyqYLSaui3UwvH56sXMrf5oQhw==} + engines: {node: '>=12.0.0'} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-n@17.17.0: + resolution: {integrity: sha512-2VvPK7Mo73z1rDFb6pTvkH6kFibAmnTubFq5l83vePxu0WiY1s0LOtj2WHb6Sa40R3w4mnh8GFYbHBQyMlotKw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + + eslint-plugin-prettier@5.2.5: + resolution: {integrity: sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-unicorn@58.0.0: + resolution: {integrity: sha512-fc3iaxCm9chBWOHPVjn+Czb/wHS0D2Mko7wkOdobqo9R2bbFObc4LyZaLTNy0mhZOP84nKkLhTUQxlLOZ7EjKw==} + engines: {node: ^18.20.0 || ^20.10.0 || >=21.0.0} + peerDependencies: + eslint: '>=9.22.0' + + eslint-plugin-yml@1.17.0: + resolution: {integrity: sha512-Q3LXFRnNpGYAK/PM0BY1Xs0IY1xTLfM0kC986nNQkx1l8tOGz+YS50N6wXkAJkrBpeUN9OxEMB7QJ+9MTDAqIQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.3.0: + resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-summary@1.0.0: + resolution: {integrity: sha512-cHr5WiNFhu2guLQykhQV8O7BQcnpFLR6GdLjbQfDDL0yGy9U7dXC6zMUtwoxYgJRC/Wk3yZMc+I6Q15Z7r4j9Q==} + engines: {node: '>=0.10.0'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + eslint@9.23.0: + resolution: {integrity: sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + expect-type@1.2.0: + resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==} + engines: {node: '>=12.0.0'} + + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + fanfou-sdk@5.0.0: + resolution: {integrity: sha512-i/I3Py9A/JloJ6qU5HUvzo+iywiloGWOJ8MDzIjtNIr6rGZB5SBAzBDh29h7ybywRJZhdO2C+ZslqCk66Rl6ig==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + for-in@0.1.8: + resolution: {integrity: sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==} + engines: {node: '>=0.10.0'} + + for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + + for-own@0.1.5: + resolution: {integrity: sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==} + engines: {node: '>=0.10.0'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + + form-data-encoder@4.0.2: + resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==} + engines: {node: '>= 18'} + + form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + + form-data@2.5.3: + resolution: {integrity: sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==} + engines: {node: '>= 0.12'} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + formidable@3.5.2: + resolution: {integrity: sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==} + + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.10.0: + resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + + get-uri@6.0.4: + resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==} + engines: {node: '>= 14'} + + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globals@16.0.0: + resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==} + engines: {node: '>=18'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + googleapis-common@7.2.0: + resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} + engines: {node: '>=14.0.0'} + + googleapis@148.0.0: + resolution: {integrity: sha512-8PDG5VItm6E1TdZWDqtRrUJSlBcNwz0/MwCa6AL81y/RxPGXJRUwKqGZfCoVX1ZBbfr3I4NkDxBmeTyOAZSWqw==} + engines: {node: '>=14.0.0'} + + got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + + got@14.4.7: + resolution: {integrity: sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==} + engines: {node: '>=20'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + graphql@16.10.0: + resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + + har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + + har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + + has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + heap@0.2.7: + resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} + + hexoid@2.0.0: + resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} + engines: {node: '>=8'} + + hmacsha1@1.0.0: + resolution: {integrity: sha512-4FP6J0oI8jqb6gLLl9tSwVdosWJ/AKSGJ+HwYf6Ixe4MUcEkst4uWzpVQrNOCin0fzTRQbXV8ePheU8WiiDYBw==} + + hono@4.7.5: + resolution: {integrity: sha512-fDOK5W2C1vZACsgLONigdZTRZxuBqFtcKh7bUQ5cVSbwI2RWjloJDcgFOVzbQrlI6pCmhlTsVYZ7zpLj4m4qMQ==} + engines: {node: '>=16.9.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-minifier@4.0.0: + resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==} + engines: {node: '>=6'} + hasBin: true + + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + htmlparser2@3.10.1: + resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http-cookie-agent@6.0.8: + resolution: {integrity: sha512-qnYh3yLSr2jBsTYkw11elq+T361uKAJaZ2dR4cfYZChw1dt9uL5t3zSUwehoqqVb4oldk1BpkXKm2oat8zV+oA==} + engines: {node: '>=18.0.0'} + peerDependencies: + tough-cookie: ^4.0.0 || ^5.0.0 + undici: ^5.11.0 || ^6.0.0 + peerDependenciesMeta: + undici: + optional: true + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + + http-signature@1.4.0: + resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} + engines: {node: '>=0.10'} + + http2-wrapper@2.2.1: + resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.5.0: + resolution: {integrity: sha512-NnEhI9hIEKHOzJ4f697DMz9IQEXr/MMJ5w64vN2/4Ai+wRnvV7SBrL0KLoRlwaKVghOc7LQ5YkPLuX146b6Ydw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + image-size@0.7.5: + resolution: {integrity: sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==} + engines: {node: '>=6.9.0'} + hasBin: true + + imapflow@1.0.184: + resolution: {integrity: sha512-GKGyNBe7JWF+vP7TZP5AcGe1XJzw+H2D1MGPqdJc5SZ2KQLvGBF/JIhx/TCp3962oraQwW8CTUDFRtRGcEzkMQ==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-in-the-middle@1.13.1: + resolution: {integrity: sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + index-to-position@1.0.0: + resolution: {integrity: sha512-sCO7uaLVhRJ25vz1o8s9IFM3nVS4DkuQnyjMwiQPKvQuBYBDmb8H7zx8ki7nVh4HJQOdVWebyvLE0qt+clruxA==} + engines: {node: '>=18'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + + instagram-private-api@1.46.1: + resolution: {integrity: sha512-fq0q6UfhpikKZ5Kw8HNwS6YpsNghE9I/uc8AM9Do9nsQ+3H1u0jLz+0t/FcGkGTjZz5VGvU8s2VbWj9wxchwYg==} + engines: {node: '>=8.0.0'} + peerDependencies: + re2: ^1.17.2 + peerDependenciesMeta: + re2: + optional: true + + ioredis@5.6.0: + resolution: {integrity: sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==} + engines: {node: '>=12.22.0'} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + + ip-regex@4.3.0: + resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} + engines: {node: '>=8'} + + ip-regex@5.0.0: + resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-builtin-module@5.0.0: + resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} + engines: {node: '>=18.20'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-keyword-js@1.0.3: + resolution: {integrity: sha512-EW8wNCNvomPa/jsH1g0DmLfPakkRCRTcTML1v1fZMLiVCvQ/1YB+tKsRzShBiWQhqrYCi5a+WsepA4Z8TA9iaA==} + engines: {node: '>=0.10.0'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + is64bit@2.0.0: + resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@3.0.2: + resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + jschardet@3.1.4: + resolution: {integrity: sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==} + engines: {node: '>=0.1.90'} + + jsdom@26.0.0: + resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonpath-plus@10.3.0: + resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==} + engines: {node: '>=18.0.0'} + hasBin: true + + jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + + jsprim@2.0.2: + resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} + engines: {'0': node >=0.6.0} + + jsrsasign@10.9.0: + resolution: {integrity: sha512-QWLUikj1SBJGuyGK8tjKSx3K7Y69KYJnrs/pQ1KZ6wvZIkHkWjZ1PJDpuvc1/28c1uP0KW9qn1eI1LzHQqDOwQ==} + + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@2.0.1: + resolution: {integrity: sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==} + engines: {node: '>=0.10.0'} + + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + lazy-cache@0.2.7: + resolution: {integrity: sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==} + engines: {node: '>=0.10.0'} + + lazy-cache@1.0.4: + resolution: {integrity: sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==} + engines: {node: '>=0.10.0'} + + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + + levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + libbase64@1.3.0: + resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} + + libmime@5.3.6: + resolution: {integrity: sha512-j9mBC7eiqi6fgBPAGvKCXJKJSIASanYF4EeA4iBzSG0HxQxmXnR3KbyWqTn4CwsKSebqCv2f5XZfAO6sKzgvwA==} + + libqp@2.1.1: + resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lint-staged@15.5.0: + resolution: {integrity: sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.2.5: + resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} + engines: {node: '>=18.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.assignin@4.2.0: + resolution: {integrity: sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==} + + lodash.bind@4.2.1: + resolution: {integrity: sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.filter@4.6.0: + resolution: {integrity: sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.foreach@4.5.0: + resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.map@4.6.0: + resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.pick@4.4.0: + resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} + deprecated: This package is deprecated. Use destructuring assignment syntax instead. + + lodash.reduce@4.6.0: + resolution: {integrity: sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==} + + lodash.reject@4.6.0: + resolution: {integrity: sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==} + + lodash.some@4.6.0: + resolution: {integrity: sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + long@5.3.1: + resolution: {integrity: sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==} + + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + + lower-case@1.1.4: + resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} + + lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + luxon@1.28.1: + resolution: {integrity: sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + mailparser@3.7.2: + resolution: {integrity: sha512-iI0p2TCcIodR1qGiRoDBBwboSSff50vQAWytM5JRggLfABa4hHYCf3YVujtuzV454xrOP352VsAPIzviqMTo4Q==} + + mailsplit@5.4.2: + resolution: {integrity: sha512-4cczG/3Iu3pyl8JgQ76dKkisurZTmxMrA4dj/e8d2jKYcFTZ7MxOzg1gTioTDMPuFXwTrVuN/gxhkrO7wLg7qA==} + + mailsplit@5.4.3: + resolution: {integrity: sha512-PFV0BBh4Tv7Omui5FtXXVtN4ExAxIi8Yvmb9JgBz+J6Hnnrv/YYXLlKKudLhXwd3/qWEATOslRsnzVCWDeCnmQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + merge-deep@3.0.3: + resolution: {integrity: sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==} + engines: {node: '>=0.10.0'} + + merge-source-map@1.1.0: + resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.1: + resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==} + engines: {node: '>= 18'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mixin-object@2.0.1: + resolution: {integrity: sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==} + engines: {node: '>=0.10.0'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mockdate@3.0.5: + resolution: {integrity: sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==} + + module-alias@2.2.3: + resolution: {integrity: sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==} + + module-details-from-path@1.0.3: + resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} + + moment-parseformat@3.0.0: + resolution: {integrity: sha512-dVgXe6b6DLnv4CHG7a1zUe5mSXaIZ3c6lSHm/EKeVeQI2/4pwe0VRde8OyoCE1Ro2lKT5P6uT9JElF7KDLV+jw==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.4.3: + resolution: {integrity: sha512-PXK3wOQHwDtz6JYVyAVlQtzrLr6bOAJxggw5UHm3CId79+W7238aNBD1zJVkFY53o/DMacuIfgesW2nv9yCO3Q==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + engines: {node: ^18 || >=20} + hasBin: true + + narou@1.1.0: + resolution: {integrity: sha512-UwYk9x+5cidHwqiKiklEdsQy4tID0pn6HYYweah+7bamze2hDE7gnFdjaqRCrp2INzptdp3mCiauKV49trTVFg==} + engines: {node: '>=16.0.0', pnpm: '>=8'} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + no-case@2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + + node-fetch-native@1.6.6: + resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-localstorage@2.2.1: + resolution: {integrity: sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==} + engines: {node: '>=0.12'} + + node-network-devtools@1.0.25: + resolution: {integrity: sha512-lPOdXdkh3I6haAUz+dTtfOCdt6/J6tJdQsKXxix9TmHV2wwecEVXLrZuawkkCMfbU4tb6/PJLqfiIO3R/VARiw==} + peerDependencies: + undici: ^6 + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + nodemailer@6.10.0: + resolution: {integrity: sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==} + engines: {node: '>=6.0.0'} + + nodemailer@6.9.16: + resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==} + engines: {node: '>=6.0.0'} + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + normalize-package-data@6.0.2: + resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} + engines: {node: ^16.14.0 || >=18.0.0} + + normalize-url@8.0.1: + resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} + engines: {node: '>=14.16'} + + notion-to-md@3.1.7: + resolution: {integrity: sha512-DXW4JzDTXmH8VY9v+3Kv2lEI2jMzN23bUSO9hqqkVkTuI4hgbAhtfkTx3L6vw7M/J7wMdkqdF88Vw5dGKwZBIw==} + engines: {node: '>=12'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nth-check@1.0.2: + resolution: {integrity: sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nwsapi@2.2.19: + resolution: {integrity: sha512-94bcyI3RsqiZufXjkr3ltkI86iEl+I7uiHVDtcq9wJUTwYQJ5odHDeSzkkrRzi80jJ8MaeZgqKjH1bAWAFw9bA==} + + oauth-1.0a@2.2.6: + resolution: {integrity: sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==} + + oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + openapi-fetch@0.11.3: + resolution: {integrity: sha512-r18fERgpxFrI4pv79ABD1dqFetWz7pTfwRd7jQmRm/lFdCDpWF43kvHUiOqOZu+tWsMydDJMpJN1hlZ9inRvfA==} + + openapi-typescript-helpers@0.0.13: + resolution: {integrity: sha512-z44WK2e7ygW3aUtAtiurfEACohf/Qt9g6BsejmIYgEoY4REHeRzgFJmO3ium0libsuzPc145I+8lE9aiiZrQvQ==} + + openapi3-ts@4.4.0: + resolution: {integrity: sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==} + + optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + otplib@12.0.1: + resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + + p-cancelable@4.0.1: + resolution: {integrity: sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==} + engines: {node: '>=14.16'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + param-case@2.1.1: + resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-json@8.2.0: + resolution: {integrity: sha512-eONBZy4hm2AgxjNFd8a4nyDJnzUAH0g34xSQAwWEVGCjdZ4ZL7dKZBfq267GWP/JaS9zW62Xs2FeAdDvpHHJGQ==} + engines: {node: '>=18'} + + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.8.0: + resolution: {integrity: sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.6.0: + resolution: {integrity: sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==} + hasBin: true + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postman-request@2.88.1-postman.42: + resolution: {integrity: sha512-lepCE8QU0izagxxA31O/MHj8IUguwLlpqeVK7A8vHK401FPvN/PTIzWHm29c/L3j3kTUE7dhZbq8vvbyQ7S2Bw==} + engines: {node: '>= 16'} + + prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + engines: {node: '>=14'} + hasBin: true + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + protobufjs@7.4.0: + resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} + engines: {node: '>=12.0.0'} + + proxy-agent@6.4.0: + resolution: {integrity: sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==} + engines: {node: '>= 14'} + + proxy-chain@2.5.8: + resolution: {integrity: sha512-TqKOYRD/1Gga/JhiwmdYHJoj0zMJkKGofQ9bHQuSm+vexczatt81fkUHTVMyci+2mWczXiTNv1Eom+2v3Da5og==} + engines: {node: '>=14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + puppeteer-core@22.6.2: + resolution: {integrity: sha512-Sws/9V2/7nFrn3MSsRPHn1pXJMIFn6FWHhoMFMUBXQwVvcBstRIa9yW8sFfxePzb56W1xNfSYzPRnyAd0+qRVQ==} + engines: {node: '>=18'} + + puppeteer-extra-plugin-stealth@2.11.2: + resolution: {integrity: sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ==} + engines: {node: '>=8'} + peerDependencies: + playwright-extra: '*' + puppeteer-extra: '*' + peerDependenciesMeta: + playwright-extra: + optional: true + puppeteer-extra: + optional: true + + puppeteer-extra-plugin-user-data-dir@2.4.1: + resolution: {integrity: sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g==} + engines: {node: '>=8'} + peerDependencies: + playwright-extra: '*' + puppeteer-extra: '*' + peerDependenciesMeta: + playwright-extra: + optional: true + puppeteer-extra: + optional: true + + puppeteer-extra-plugin-user-preferences@2.4.1: + resolution: {integrity: sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A==} + engines: {node: '>=8'} + peerDependencies: + playwright-extra: '*' + puppeteer-extra: '*' + peerDependenciesMeta: + playwright-extra: + optional: true + puppeteer-extra: + optional: true + + puppeteer-extra-plugin@3.2.3: + resolution: {integrity: sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q==} + engines: {node: '>=9.11.2'} + peerDependencies: + playwright-extra: '*' + puppeteer-extra: '*' + peerDependenciesMeta: + playwright-extra: + optional: true + puppeteer-extra: + optional: true + + puppeteer-extra@3.3.6: + resolution: {integrity: sha512-rsLBE/6mMxAjlLd06LuGacrukP2bqbzKCLzV1vrhHFavqQE/taQ2UXv3H5P0Ls7nsrASa+6x3bDbXHpqMwq+7A==} + engines: {node: '>=8'} + peerDependencies: + '@types/puppeteer': '*' + puppeteer: '*' + puppeteer-core: '*' + peerDependenciesMeta: + '@types/puppeteer': + optional: true + puppeteer: + optional: true + puppeteer-core: + optional: true + + puppeteer@22.6.2: + resolution: {integrity: sha512-3GMAJ9adPUSdIHGuYV1b1RqRB6D2UScjnq779uZsvpAP6HOWw2+9ezZiUZaAXVST+Ku7KWsxOjkctEvRasJClA==} + engines: {node: '>=18'} + deprecated: < 22.8.2 is no longer supported + hasBin: true + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + qs@6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + query-string@9.1.1: + resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==} + engines: {node: '>=18'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + rate-limiter-flexible@6.2.1: + resolution: {integrity: sha512-d9AN+d/wwKW3/yHAL0G3zKpWZQFe55VjRGIFK9VG1w3CSOkcRqRqh0NhCiIXvgKhihNZPjGfISuN3it07NjPbw==} + + re2js@1.1.0: + resolution: {integrity: sha512-ovDCIb2ZQR7Do3NzH2XEuXOzqd1q8srvkeaOVMf+EZNt1Z4JoUVvNKCR9qf8EbqoPhvLknkoyiiBzoQtmuzIbQ==} + + read-package-up@11.0.0: + resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} + engines: {node: '>=18'} + + read-pkg@9.0.1: + resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} + engines: {node: '>=18'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + real-cancellable-promise@1.2.1: + resolution: {integrity: sha512-JwhiWJTMMyzFYfpKsiSb8CyQktCi1MZ8ZBn3wXvq28qXDh8Y5dM7RYzgW3r6SV22JTEcof8pRsvDp4GxLmGIxg==} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + request-promise-core@1.1.4: + resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==} + engines: {node: '>=0.10.0'} + peerDependencies: + request: ^2.34 + + request-promise@4.2.6: + resolution: {integrity: sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==} + engines: {node: '>=0.10.0'} + deprecated: request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 + peerDependencies: + request: ^2.34 + + request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfc4648@1.5.4: + resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + rollup@4.37.0: + resolution: {integrity: sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + rss-parser@3.13.0: + resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + sanitize-html@2.15.0: + resolution: {integrity: sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + shallow-clone@0.1.2: + resolution: {integrity: sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + simplecc-wasm@1.1.0: + resolution: {integrity: sha512-0nmH9WrzxpI8GOZkltH0NKAlcPe42rXek01noMSdelCmj8RYSL06SDDG/YWhvVTbMcuN1fq5imOeXpUJmhbcPQ==} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + slide@1.1.6: + resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + snakecase-keys@3.2.1: + resolution: {integrity: sha512-CjU5pyRfwOtaOITYv5C8DzpZ8XA/ieRsDpr93HI2r6e3YInC6moZpSQbmUtg8cTk58tq2x3jcG2gv+p1IZGmMA==} + engines: {node: '>=8'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.4: + resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + split-on-first@3.0.0: + resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} + engines: {node: '>=12'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.8.1: + resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} + + stealthy-require@1.1.1: + resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} + engines: {node: '>=0.10.0'} + + store2@2.14.4: + resolution: {integrity: sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==} + + stream-length@1.0.2: + resolution: {integrity: sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==} + + streamx@2.22.0: + resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-direction@0.1.2: + resolution: {integrity: sha512-NJHQRg6GlOEMLA6jEAlSy21KaXvJDNoAid/v6fBAJbqdvOEIiPpCrIPTHnl4636wUF/IGyktX5A9eddmETb1Cw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-indent@4.0.0: + resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + superagent@9.0.2: + resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} + engines: {node: '>=14.18.0'} + + supertest@7.1.0: + resolution: {integrity: sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==} + engines: {node: '>=14.18.0'} + + supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + synckit@0.10.3: + resolution: {integrity: sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + system-architecture@0.1.0: + resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} + engines: {node: '>=18'} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar-fs@3.0.5: + resolution: {integrity: sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + telegram@2.26.22: + resolution: {integrity: sha512-EIj7Yrjiu0Yosa3FZ/7EyPg9s6UiTi/zDQrFmR/2Mg7pIUU+XjAit1n1u9OU9h2oRnRM5M+67/fxzQluZpaJJg==} + + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thirty-two@1.0.2: + resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} + engines: {node: '>=0.2.6'} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-async-pool@2.1.0: + resolution: {integrity: sha512-ltAHPh/9k0STRQqaoUX52NH4ZQYAJz24ZAEwf1Zm+HYg3l9OXTWeqWKyYsHu40wF/F0rxd2N2bk5sLvX2qlSvg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + title@4.0.1: + resolution: {integrity: sha512-xRnPkJx9nvE5MF6LkB5e8QJjE2FW8269wTu/LQdf7zZqBgPly0QJPf/CWAo7srj5so4yXfoLEdCFgurlpi47zg==} + hasBin: true + + tlds@1.255.0: + resolution: {integrity: sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==} + hasBin: true + + tlds@1.256.0: + resolution: {integrity: sha512-ZmyVB9DAw+FFTmLElGYJgdZFsKLYd/I59Bg9NHkCGPwAbVZNRilFWDMAdX8UG+bHuv7kfursd5XGqo/9wi26lA==} + hasBin: true + + tldts-core@6.1.85: + resolution: {integrity: sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==} + + tldts@6.1.85: + resolution: {integrity: sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==} + hasBin: true + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + to-snake-case@1.0.0: + resolution: {integrity: sha512-joRpzBAk1Bhi2eGEYBjukEWHOe/IvclOkiJl3DtA91jV6NwQ3MwXA4FHYeqk8BNp/D8bmi9tcNbRu/SozP0jbQ==} + + to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + + tosource@2.0.0-alpha.3: + resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} + engines: {node: '>=10'} + + tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@5.1.0: + resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==} + engines: {node: '>=18'} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-case-convert@2.1.0: + resolution: {integrity: sha512-Ye79el/pHYXfoew6kqhMwCoxp4NWjKNcm2kBzpmEMIU9dd9aBmHNNFtZ+WTm0rz1ngyDmfqDXDlyUnBXayiD0w==} + + ts-custom-error@2.2.2: + resolution: {integrity: sha512-I0FEdfdatDjeigRqh1JFj67bcIKyRNm12UVGheBjs2pXgyELg2xeiQLVaWu1pVmNGXZVnz/fvycSU41moBIpOg==} + engines: {node: '>=8.0.0'} + deprecated: npm package tarball contains useless codeclimate-reporter binary, please update to version 3.1.1. See https://github.com/adriengibrat/ts-custom-error/issues/32 + + ts-custom-error@3.3.1: + resolution: {integrity: sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==} + engines: {node: '>=14.0.0'} + + ts-xor@1.3.0: + resolution: {integrity: sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA==} + + tsconfck@3.1.5: + resolution: {integrity: sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.19.3: + resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + turndown@7.2.0: + resolution: {integrity: sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + twitter-api-v2@1.22.0: + resolution: {integrity: sha512-KlcRL9vcBzjeS/PwxX33NziP+SHp9n35DOclKtpOmnNes7nNVnK7WG4pKlHfBqGrY5kAz/8J5ERS8DWkYOaiWw==} + + type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-fest@4.38.0: + resolution: {integrity: sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==} + engines: {node: '>=16'} + + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + undici@6.21.2: + resolution: {integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==} + engines: {node: '>=18.17'} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + upper-case@1.1.3: + resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + url-regex-safe@3.0.0: + resolution: {integrity: sha512-+2U40NrcmtWFVjuxXVt9bGRw6c7/MgkGKN9xIfPrT/2RX0LTkkae6CCEDp93xqUN0UKm/rr821QnHd2dHQmN3A==} + engines: {node: '>= 10.12.0'} + peerDependencies: + re2: ^1.17.2 + peerDependenciesMeta: + re2: + optional: true + + url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + + urlpattern-polyfill@10.0.0: + resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + + utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + + utf8@3.0.0: + resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + valid-url@1.0.9: + resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@5.4.15: + resolution: {integrity: sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + websocket@1.0.35: + resolution: {integrity: sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==} + engines: {node: '>=4.0.0'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.17.0: + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@1.3.4: + resolution: {integrity: sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==} + + ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.1: + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wuzzy@0.1.8: + resolution: {integrity: sha512-FUzKQepFSTnANsDYwxpIzGJ/dIJaqxuMre6tzzbvWwFAiUHPsI1nVQVCLK4Xqr67KO7oYAK0kaCcI/+WYj/7JA==} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaeti@0.0.6: + resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==} + engines: {node: '>=0.10.32'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml-eslint-parser@1.3.0: + resolution: {integrity: sha512-E/+VitOorXSLiAqtTd7Yqax0/pAS3xaYMP+AUUJGOK1OZG3rhcj9fcJOM5HJ2VrP1FrStVCWr1muTfQCdj4tAA==} + engines: {node: ^14.17.0 || >=16.0.0} + + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@15.0.3: + resolution: {integrity: sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + + zhead@2.2.4: + resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==} + + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + + zod@3.24.2: + resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@asamuzakjp/css-color@3.1.1': + dependencies: + '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 10.4.3 + + '@asteasolutions/zod-to-openapi@7.3.0(zod@3.24.2)': + dependencies: + openapi3-ts: 4.4.0 + zod: 3.24.2 + + '@babel/code-frame@7.0.0': + dependencies: + '@babel/highlight': 7.25.9 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.8': {} + + '@babel/core@7.26.10': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helpers': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.0': + dependencies: + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.25.9': + dependencies: + '@babel/types': 7.27.0 + + '@babel/helper-compilation-targets@7.27.0': + dependencies: + '@babel/compat-data': 7.26.8 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.27.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-plugin-utils': 7.26.5 + debug: 4.4.0 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.25.9': + dependencies: + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.25.9': + dependencies: + '@babel/types': 7.27.0 + + '@babel/helper-plugin-utils@7.26.5': {} + + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.26.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + dependencies: + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helper-wrap-function@7.25.9': + dependencies: + '@babel/template': 7.27.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.27.0': + dependencies: + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + + '@babel/highlight@7.25.9': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/parser@7.27.0': + dependencies: + '@babel/types': 7.27.0 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-async-generator-functions@7.26.8(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.10) + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.26.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-block-scoping@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) + '@babel/traverse': 7.27.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/template': 7.27.0 + + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-exponentiation-operator@7.26.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-for-of@7.26.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-nullish-coalescing-operator@7.26.6(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.10) + + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-regenerator@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-template-literals@7.26.8(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-typeof-symbol@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-typescript@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/preset-env@7.26.9(@babel/core@7.26.10)': + dependencies: + '@babel/compat-data': 7.26.8 + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.10) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.10) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.10) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.10) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-async-generator-functions': 7.26.8(@babel/core@7.26.10) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-block-scoped-functions': 7.26.5(@babel/core@7.26.10) + '@babel/plugin-transform-block-scoping': 7.27.0(@babel/core@7.26.10) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.10) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-exponentiation-operator': 7.26.3(@babel/core@7.26.10) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-for-of': 7.26.9(@babel/core@7.26.10) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.10) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.26.10) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-regenerator': 7.27.0(@babel/core@7.26.10) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.10) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-template-literals': 7.26.8(@babel/core@7.26.10) + '@babel/plugin-transform-typeof-symbol': 7.27.0(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.10) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.10) + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.26.10) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.26.10) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.26.10) + core-js-compat: 3.41.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/types': 7.27.0 + esutils: 2.0.3 + + '@babel/preset-typescript@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.10) + '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/runtime-corejs2@7.27.0': + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.14.1 + + '@babel/runtime@7.27.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.27.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + + '@babel/traverse@7.27.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@bbob/core@4.2.0': + dependencies: + '@bbob/parser': 4.2.0 + '@bbob/plugin-helper': 4.2.0 + '@bbob/types': 4.2.0 + + '@bbob/html@4.2.0': + dependencies: + '@bbob/core': 4.2.0 + '@bbob/plugin-helper': 4.2.0 + '@bbob/types': 4.2.0 + + '@bbob/parser@4.2.0': + dependencies: + '@bbob/plugin-helper': 4.2.0 + '@bbob/types': 4.2.0 + + '@bbob/plugin-helper@4.2.0': + dependencies: + '@bbob/types': 4.2.0 + + '@bbob/preset-html5@4.2.0': + dependencies: + '@bbob/plugin-helper': 4.2.0 + '@bbob/preset': 4.2.0 + '@bbob/types': 4.2.0 + + '@bbob/preset@4.2.0': + dependencies: + '@bbob/plugin-helper': 4.2.0 + '@bbob/types': 4.2.0 + + '@bbob/types@4.2.0': {} + + '@bcoe/v8-coverage@0.2.3': {} + + '@bundled-es-modules/cookie@2.0.1': + dependencies: + cookie: 0.7.2 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.1 + + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + + '@colors/colors@1.6.0': {} + + '@cryptography/aes@0.1.1': {} + + '@csstools/color-helpers@5.0.2': {} + + '@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-color-parser@3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/color-helpers': 5.0.2 + '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-tokenizer@3.0.3': {} + + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.1': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.1': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.1': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.1': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.1': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.1': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.1': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.1': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.1': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.1': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.1': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.1': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.1': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.1': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.1': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.1': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.1': + optional: true + + '@esbuild/netbsd-arm64@0.25.1': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.1': + optional: true + + '@esbuild/openbsd-arm64@0.25.1': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.1': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.1': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.1': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.1': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.1': + optional: true + + '@eslint-community/eslint-utils@4.5.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.5.1(eslint@9.23.0)': + dependencies: + eslint: 9.23.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.19.2': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.2.0': {} + + '@eslint/core@0.12.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.0 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.0 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@eslint/js@9.23.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.2.7': + dependencies: + '@eslint/core': 0.12.0 + levn: 0.4.1 + + '@hono/node-server@1.14.0(hono@4.7.5)': + dependencies: + hono: 4.7.5 + + '@hono/zod-openapi@0.19.2(hono@4.7.5)(zod@3.24.2)': + dependencies: + '@asteasolutions/zod-to-openapi': 7.3.0(zod@3.24.2) + '@hono/zod-validator': 0.4.3(hono@4.7.5)(zod@3.24.2) + hono: 4.7.5 + zod: 3.24.2 + + '@hono/zod-validator@0.4.3(hono@4.7.5)(zod@3.24.2)': + dependencies: + hono: 4.7.5 + zod: 3.24.2 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.2': {} + + '@ianvs/eslint-stats@2.0.0': + dependencies: + chalk: 2.4.2 + lodash: 4.17.21 + + '@inquirer/confirm@3.2.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 1.5.5 + + '@inquirer/core@9.2.1': + dependencies: + '@inquirer/figures': 1.0.11 + '@inquirer/type': 2.0.0 + '@types/mute-stream': 0.0.4 + '@types/node': 22.13.15 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + + '@inquirer/figures@1.0.11': {} + + '@inquirer/type@1.5.5': + dependencies: + mute-stream: 1.0.0 + + '@inquirer/type@2.0.0': + dependencies: + mute-stream: 1.0.0 + + '@ioredis/commands@1.2.0': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@istanbuljs/schema@0.1.3': {} + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@lifeomic/attempt@3.1.0': {} + + '@mapbox/node-pre-gyp@2.0.0': + dependencies: + consola: 3.4.2 + detect-libc: 2.0.3 + https-proxy-agent: 7.0.6 + node-fetch: 2.7.0 + nopt: 8.1.0 + semver: 7.7.1 + tar: 7.4.3 + transitivePeerDependencies: + - encoding + - supports-color + + '@microsoft/eslint-formatter-sarif@3.1.0': + dependencies: + eslint: 8.57.1 + jschardet: 3.1.4 + lodash: 4.17.21 + utf8: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@mixmark-io/domino@2.2.0': {} + + '@mswjs/interceptors@0.29.1': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@nolyfill/es-set-tostringtag@1.0.44': {} + + '@nolyfill/is-core-module@1.0.39': {} + + '@nolyfill/safe-buffer@1.0.44': {} + + '@nolyfill/safer-buffer@1.0.44': {} + + '@nolyfill/side-channel@1.0.44': {} + + '@notionhq/client@2.3.0': + dependencies: + '@types/node-fetch': 2.6.12 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@one-ini/wasm@0.1.1': {} + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@opentelemetry/api-logs@0.200.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/core@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.30.0 + + '@opentelemetry/exporter-prometheus@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.16.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fastify@0.44.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.19.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.45.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + forwarded-parse: 2.1.2 + semver: 7.7.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.52.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.45.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + '@types/mysql': 2.15.26 + transitivePeerDependencies: + - supports-color - /@jridgewell/trace-mapping@0.3.25: - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@opentelemetry/instrumentation-pg@0.51.1(@opentelemetry/api@1.9.0)': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + '@types/pg': 8.6.1 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color - /@lifeomic/attempt@3.0.3: - resolution: {integrity: sha512-GlM2AbzrErd/TmLL3E8hAHmb5Q7VhDJp35vIbyPVA5Rz55LZuRr8pwL3qrwwkVNo05gMX1J44gURKb4MHQZo7w==} - dev: false + '@opentelemetry/instrumentation-redis-4@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.30.0 + transitivePeerDependencies: + - supports-color - /@mapbox/node-pre-gyp@1.0.11: - resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} - hasBin: true + '@opentelemetry/instrumentation-tedious@0.18.1(@opentelemetry/api@1.9.0)': dependencies: - detect-libc: 2.0.2 - https-proxy-agent: 5.0.1 - make-dir: 3.1.0 - node-fetch: 2.7.0 - nopt: 5.0.0 - npmlog: 5.0.1 - rimraf: 3.0.2 - semver: 7.6.0 - tar: 6.2.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + '@types/tedious': 4.0.14 transitivePeerDependencies: - - encoding - supports-color - dev: true - /@microsoft/eslint-formatter-sarif@3.1.0: - resolution: {integrity: sha512-/mn4UXziHzGXnKCg+r8HGgPy+w4RzpgdoqFuqaKOqUVBT5x2CygGefIrO4SusaY7t0C4gyIWMNu6YQT6Jw64Cw==} - engines: {node: '>= 14'} + '@opentelemetry/instrumentation-undici@0.10.1(@opentelemetry/api@1.9.0)': dependencies: - eslint: 8.57.0 - jschardet: 3.1.2 - lodash: 4.17.21 - utf8: 3.0.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - dev: true - /@mswjs/cookies@1.1.0: - resolution: {integrity: sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==} - engines: {node: '>=18'} - dev: true + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.13.1 + require-in-the-middle: 7.5.2 + semver: 7.7.1 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color - /@mswjs/interceptors@0.26.15: - resolution: {integrity: sha512-HM47Lu1YFmnYHKMBynFfjCp0U/yRskHj/8QEJW0CBEPOlw8Gkmjfll+S9b8M7V5CNDw2/ciRxjjnWeaCiblSIQ==} - engines: {node: '>=18'} + '@opentelemetry/otlp-exporter-base@0.200.0(@opentelemetry/api@1.9.0)': dependencies: - '@open-draft/deferred-promise': 2.2.0 - '@open-draft/logger': 0.3.0 - '@open-draft/until': 2.1.0 - is-node-process: 1.2.0 - outvariant: 1.4.2 - strict-event-emitter: 0.5.1 - dev: true + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@opentelemetry/otlp-transformer@0.200.0(@opentelemetry/api@1.9.0)': dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.200.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + protobufjs: 7.4.0 - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true + '@opentelemetry/redis-common@0.36.2': {} - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - dev: true + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 - /@notionhq/client@2.2.15: - resolution: {integrity: sha512-XhdSY/4B1D34tSco/GION+23GMjaS9S2zszcqYkMHo8RcWInymF6L1x+Gk7EmHdrSxNFva2WM8orhC4BwQCwgw==} - engines: {node: '>=12'} + '@opentelemetry/resources@2.0.0(@opentelemetry/api@1.9.0)': dependencies: - '@types/node-fetch': 2.6.11 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - dev: false + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 - /@one-ini/wasm@0.1.1: - resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} - dev: true + '@opentelemetry/sdk-logs@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.200.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) - /@open-draft/deferred-promise@2.2.0: - resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} - dev: true + '@opentelemetry/sdk-metrics@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) - /@open-draft/logger@0.3.0: - resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)': dependencies: - is-node-process: 1.2.0 - outvariant: 1.4.2 - dev: true + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 - /@open-draft/until@2.1.0: - resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - dev: true + '@opentelemetry/sdk-trace-base@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 - /@otplib/core@12.0.1: - resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} - dev: false + '@opentelemetry/semantic-conventions@1.28.0': {} - /@otplib/plugin-crypto@12.0.1: - resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==} + '@opentelemetry/semantic-conventions@1.30.0': {} + + '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@otplib/core@12.0.1': {} + + '@otplib/plugin-crypto@12.0.1': dependencies: '@otplib/core': 12.0.1 - dev: false - /@otplib/plugin-thirty-two@12.0.1: - resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==} + '@otplib/plugin-thirty-two@12.0.1': dependencies: '@otplib/core': 12.0.1 thirty-two: 1.0.2 - dev: false - /@otplib/preset-default@12.0.1: - resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==} + '@otplib/preset-default@12.0.1': dependencies: '@otplib/core': 12.0.1 '@otplib/plugin-crypto': 12.0.1 '@otplib/plugin-thirty-two': 12.0.1 - dev: false - /@otplib/preset-v11@12.0.1: - resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==} + '@otplib/preset-v11@12.0.1': dependencies: '@otplib/core': 12.0.1 '@otplib/plugin-crypto': 12.0.1 '@otplib/plugin-thirty-two': 12.0.1 - dev: false - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true - dev: true + '@pkgjs/parseargs@0.11.0': optional: true - /@pkgr/core@0.1.1: - resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - dev: true + '@pkgr/core@0.2.0': {} - /@postlight/ci-failed-test-reporter@1.0.26: - resolution: {integrity: sha512-xfXzxyOiKhco7Gx2OLTe9b66b0dFJw0elg94KGHoQXf5F8JqqFvdo35J8wayGOor64CSMvn+4Bjlu2NKV+yTGA==} - hasBin: true + '@postlight/ci-failed-test-reporter@1.0.26': dependencies: dotenv: 6.2.0 node-fetch: 2.7.0 transitivePeerDependencies: - encoding - dev: false - /@postlight/parser@2.2.3: - resolution: {integrity: sha512-4/syRvqJARgLN4yH8qtl634WO0+KINjkijU/SmhCJqqh8/aOfv5uQf+SquFpA+JwsAsbGzYQkIxSum29riOreg==} - engines: {node: '>=10'} - hasBin: true + '@postlight/parser@2.2.3': dependencies: - '@babel/runtime-corejs2': 7.23.9 + '@babel/runtime-corejs2': 7.27.0 '@postlight/ci-failed-test-reporter': 1.0.26 cheerio: 0.22.0 - difflib: github.com/postlight/difflib.js/32e8e38c7fcd935241b9baab71bb432fd9b166ed + difflib: https://codeload.github.com/postlight/difflib.js/tar.gz/32e8e38c7fcd935241b9baab71bb432fd9b166ed ellipsize: 0.1.0 iconv-lite: 0.5.0 moment: 2.30.1 moment-parseformat: 3.0.0 - postman-request: 2.88.1-postman.33 + postman-request: 2.88.1-postman.42 string-direction: 0.1.2 - turndown: 7.1.2 + turndown: 7.2.0 valid-url: 1.0.9 wuzzy: 0.1.8 yargs-parser: 15.0.3 transitivePeerDependencies: - encoding - dev: false - bundledDependencies: - - jquery - - moment-timezone - - browser-request - /@postman/form-data@3.1.1: - resolution: {integrity: sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==} - engines: {node: '>= 6'} + '@postman/form-data@3.1.1': dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: false - /@postman/tough-cookie@4.1.3-postman.1: - resolution: {integrity: sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==} - engines: {node: '>=6'} + '@postman/tough-cookie@4.1.3-postman.1': dependencies: - psl: 1.9.0 + psl: 1.15.0 punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10 - dev: false - /@postman/tunnel-agent@0.6.3: - resolution: {integrity: sha512-k57fzmAZ2PJGxfOA4SGR05ejorHbVAa/84Hxh/2nAztjNXc4ZjOm9NUIk6/Z6LCrBvJZqjRZbN8e/nROVUPVdg==} + '@postman/tunnel-agent@0.6.4': dependencies: - safe-buffer: 5.2.1 - dev: false + safe-buffer: '@nolyfill/safe-buffer@1.0.44' - /@puppeteer/browsers@2.2.0: - resolution: {integrity: sha512-MC7LxpcBtdfTbzwARXIkqGZ1Osn3nnZJlm+i0+VqHl72t//Xwl9wICrXT8BwtgC6s1xJNHsxOpvzISUqe92+sw==} - engines: {node: '>=18'} - hasBin: true + '@prisma/instrumentation@6.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@puppeteer/browsers@2.2.0': dependencies: debug: 4.3.4 extract-zip: 2.0.1 @@ -2317,1443 +7688,890 @@ packages: unbzip2-stream: 1.4.3 yargs: 17.7.2 transitivePeerDependencies: + - bare-buffer - supports-color - dev: false - /@rollup/pluginutils@4.2.1: - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} + '@rollup/pluginutils@5.1.4(rollup@4.37.0)': dependencies: + '@types/estree': 1.0.7 estree-walker: 2.0.2 - picomatch: 2.3.1 - dev: true + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.37.0 - /@rollup/rollup-android-arm-eabi@4.14.3: - resolution: {integrity: sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true + '@rollup/rollup-android-arm-eabi@4.37.0': optional: true - /@rollup/rollup-android-arm64@4.14.3: - resolution: {integrity: sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true + '@rollup/rollup-android-arm64@4.37.0': optional: true - /@rollup/rollup-darwin-arm64@4.14.3: - resolution: {integrity: sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true + '@rollup/rollup-darwin-arm64@4.37.0': optional: true - /@rollup/rollup-darwin-x64@4.14.3: - resolution: {integrity: sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true + '@rollup/rollup-darwin-x64@4.37.0': optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.14.3: - resolution: {integrity: sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-freebsd-arm64@4.37.0': optional: true - /@rollup/rollup-linux-arm-musleabihf@4.14.3: - resolution: {integrity: sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-freebsd-x64@4.37.0': optional: true - /@rollup/rollup-linux-arm64-gnu@4.14.3: - resolution: {integrity: sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-arm-gnueabihf@4.37.0': optional: true - /@rollup/rollup-linux-arm64-musl@4.14.3: - resolution: {integrity: sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-arm-musleabihf@4.37.0': optional: true - /@rollup/rollup-linux-powerpc64le-gnu@4.14.3: - resolution: {integrity: sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-arm64-gnu@4.37.0': optional: true - /@rollup/rollup-linux-riscv64-gnu@4.14.3: - resolution: {integrity: sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-arm64-musl@4.37.0': optional: true - /@rollup/rollup-linux-s390x-gnu@4.14.3: - resolution: {integrity: sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-loongarch64-gnu@4.37.0': optional: true - /@rollup/rollup-linux-x64-gnu@4.14.3: - resolution: {integrity: sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-powerpc64le-gnu@4.37.0': optional: true - /@rollup/rollup-linux-x64-musl@4.14.3: - resolution: {integrity: sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-riscv64-gnu@4.37.0': optional: true - /@rollup/rollup-win32-arm64-msvc@4.14.3: - resolution: {integrity: sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true + '@rollup/rollup-linux-riscv64-musl@4.37.0': optional: true - /@rollup/rollup-win32-ia32-msvc@4.14.3: - resolution: {integrity: sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true + '@rollup/rollup-linux-s390x-gnu@4.37.0': optional: true - /@rollup/rollup-win32-x64-msvc@4.14.3: - resolution: {integrity: sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true + '@rollup/rollup-linux-x64-gnu@4.37.0': optional: true - /@selderee/plugin-htmlparser2@0.11.0: - resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - dependencies: - domhandler: 5.0.3 - selderee: 0.11.0 - dev: false + '@rollup/rollup-linux-x64-musl@4.37.0': + optional: true - /@sentry-internal/tracing@7.113.0: - resolution: {integrity: sha512-8MDnYENRMnEfQjvN4gkFYFaaBSiMFSU/6SQZfY9pLI3V105z6JQ4D0PGMAUVowXilwNZVpKNYohE7XByuhEC7Q==} - engines: {node: '>=8'} - dependencies: - '@sentry/core': 7.113.0 - '@sentry/types': 7.113.0 - '@sentry/utils': 7.113.0 - dev: false + '@rollup/rollup-win32-arm64-msvc@4.37.0': + optional: true - /@sentry/core@7.113.0: - resolution: {integrity: sha512-pg75y3C5PG2+ur27A0Re37YTCEnX0liiEU7EOxWDGutH17x3ySwlYqLQmZsFZTSnvzv7t3MGsNZ8nT5O0746YA==} - engines: {node: '>=8'} - dependencies: - '@sentry/types': 7.113.0 - '@sentry/utils': 7.113.0 - dev: false + '@rollup/rollup-win32-ia32-msvc@4.37.0': + optional: true - /@sentry/integrations@7.113.0: - resolution: {integrity: sha512-w0sspGBQ+6+V/9bgCkpuM3CGwTYoQEVeTW6iNebFKbtN7MrM3XsGAM9I2cW1jVxFZROqCBPFtd2cs5n0j14aAg==} - engines: {node: '>=8'} - dependencies: - '@sentry/core': 7.113.0 - '@sentry/types': 7.113.0 - '@sentry/utils': 7.113.0 - localforage: 1.10.0 - dev: false + '@rollup/rollup-win32-x64-msvc@4.37.0': + optional: true - /@sentry/node@7.113.0: - resolution: {integrity: sha512-Vam4Ia0I9fhVw8GJOzcLP7MiiHJSKl8L9LzLMMLG3+2/dFnDQOyS7sOfk3GqgpwzqPiusP9vFu7CFSX7EMQbTg==} - engines: {node: '>=8'} + '@rss3/api-core@0.0.25': dependencies: - '@sentry-internal/tracing': 7.113.0 - '@sentry/core': 7.113.0 - '@sentry/integrations': 7.113.0 - '@sentry/types': 7.113.0 - '@sentry/utils': 7.113.0 - dev: false + openapi-fetch: 0.11.3 + ts-case-convert: 2.1.0 + type-fest: 4.38.0 - /@sentry/types@7.113.0: - resolution: {integrity: sha512-PJbTbvkcPu/LuRwwXB1He8m+GjDDLKBtu3lWg5xOZaF5IRdXQU2xwtdXXsjge4PZR00tF7MO7X8ZynTgWbYaew==} - engines: {node: '>=8'} - dev: false + '@rss3/api-utils@0.0.25': + dependencies: + '@rss3/api-core': 0.0.25 - /@sentry/utils@7.113.0: - resolution: {integrity: sha512-nzKsErwmze1mmEsbW2AwL2oB+I5v6cDEJY4sdfLekA4qZbYZ8pV5iWza6IRl4XfzGTE1qpkZmEjPU9eyo0yvYw==} - engines: {node: '>=8'} + '@rss3/sdk@0.0.25': dependencies: - '@sentry/types': 7.113.0 - dev: false + '@rss3/api-core': 0.0.25 + '@rss3/api-utils': 0.0.25 - /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true + '@scalar/core@0.2.4': + dependencies: + '@scalar/types': 0.1.4 - /@sindresorhus/is@5.6.0: - resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} - engines: {node: '>=14.16'} - dev: false + '@scalar/hono-api-reference@0.7.4(hono@4.7.5)': + dependencies: + '@scalar/core': 0.2.4 + hono: 4.7.5 - /@sindresorhus/is@6.2.0: - resolution: {integrity: sha512-yM/IGPkVnYGblhDosFBwq0ZGdnVSBkNV4onUtipGMOjZd4kB6GAu3ys91aftSbyMHh6A2GPdt+KDI5NoWP63MQ==} - engines: {node: '>=16'} - dev: true + '@scalar/openapi-types@0.1.9': {} - /@stylistic/eslint-plugin-js@1.8.0(eslint@8.57.0): - resolution: {integrity: sha512-jdvnzt+pZPg8TfclZlTZPiUbbima93ylvQ+wNgHLNmup3obY6heQvgewSu9i2CfS61BnRByv+F9fxQLPoNeHag==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: '>=8.40.0' + '@scalar/types@0.1.4': dependencies: - '@types/eslint': 8.56.10 - acorn: 8.11.3 - escape-string-regexp: 4.0.0 - eslint: 8.57.0 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - dev: true + '@scalar/openapi-types': 0.1.9 + '@unhead/schema': 1.11.20 + nanoid: 5.1.5 + type-fest: 4.38.0 + zod: 3.24.2 - /@stylistic/eslint-plugin-jsx@1.8.0(eslint@8.57.0): - resolution: {integrity: sha512-PC7tYXipF03TTilGJva1amAham7qOAFXT5r5jLTY6iIxkFqyb6H7Ljx5pv8d7n98VyIVidOEKY/AP8vNzAFNKg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: '>=8.40.0' - dependencies: - '@stylistic/eslint-plugin-js': 1.8.0(eslint@8.57.0) - '@types/eslint': 8.56.10 - eslint: 8.57.0 - estraverse: 5.3.0 - picomatch: 4.0.2 - dev: true + '@sec-ant/readable-stream@0.4.1': {} - /@stylistic/eslint-plugin-plus@1.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-TkrjzzYmTuAaLvFwtxomsgMUD8g8PREOQOQzTfKmiJ6oc4XOyFW4q/L9ES1J3UFSLybNCwbhu36lhXJut1w2Sg==} - peerDependencies: - eslint: '*' + '@selderee/plugin-htmlparser2@0.11.0': dependencies: - '@types/eslint': 8.56.10 - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 + domhandler: 5.0.3 + selderee: 0.11.0 + + '@sentry/core@9.10.1': {} + + '@sentry/node@9.10.1': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.16.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.44.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.19.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.51.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.18.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.10.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + '@prisma/instrumentation': 6.5.0(@opentelemetry/api@1.9.0) + '@sentry/core': 9.10.1 + '@sentry/opentelemetry': 9.10.1(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0) + import-in-the-middle: 1.13.1 transitivePeerDependencies: - supports-color - - typescript - dev: true - /@stylistic/eslint-plugin-ts@1.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-WuCIhz4JEHxzhAWjrBASMGj6Or1wAjDqTsRIck3DRRrw/FJ8C/8AAuHPk8ECHNSDI5PZ0OT72nF2uSUn0aQq1w==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: '>=8.40.0' + '@sentry/opentelemetry@9.10.1(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)': dependencies: - '@stylistic/eslint-plugin-js': 1.8.0(eslint@8.57.0) - '@types/eslint': 8.56.10 - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.30.0 + '@sentry/core': 9.10.1 - /@stylistic/eslint-plugin@1.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-JRR0lCDU97AiE0X6qTc/uf8Hv0yETUdyJgoNzTLUIWdhVJVe/KGPnFmEsO1iXfNUIS6vhv3JJ5vaZ2qtXhZe1g==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: '>=8.40.0' + '@sindresorhus/is@5.6.0': {} + + '@sindresorhus/is@7.0.1': {} + + '@stylistic/eslint-plugin@4.2.0(eslint@9.23.0)(typescript@5.8.2)': dependencies: - '@stylistic/eslint-plugin-js': 1.8.0(eslint@8.57.0) - '@stylistic/eslint-plugin-jsx': 1.8.0(eslint@8.57.0) - '@stylistic/eslint-plugin-plus': 1.8.0(eslint@8.57.0)(typescript@5.4.5) - '@stylistic/eslint-plugin-ts': 1.8.0(eslint@8.57.0)(typescript@5.4.5) - '@types/eslint': 8.56.10 - eslint: 8.57.0 + '@typescript-eslint/utils': 8.28.0(eslint@9.23.0)(typescript@5.8.2) + eslint: 9.23.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + estraverse: 5.3.0 + picomatch: 4.0.2 transitivePeerDependencies: - supports-color - typescript - dev: true - /@szmarczak/http-timer@5.0.1: - resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} - engines: {node: '>=14.16'} + '@szmarczak/http-timer@5.0.1': dependencies: defer-to-connect: 2.0.1 - /@tonyrl/rand-user-agent@2.0.61: - resolution: {integrity: sha512-sa/xm6BzI8FsDRk/jBJY8H2dI6JZ/SSDTRA/JW7aPSn6kJnPiHwPeWpGF3hATK/bgKgfKd5I0a/TePibemicwA==} - engines: {node: '>=14.16'} - dev: false + '@tonyrl/rand-user-agent@2.0.83': {} - /@tootallnate/quickjs-emscripten@0.23.0: - resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} - dev: false + '@tootallnate/quickjs-emscripten@0.23.0': {} - /@types/aes-js@3.1.4: - resolution: {integrity: sha512-v3D66IptpUqh+pHKVNRxY8yvp2ESSZXe0rTzsGdzUhEwag7ljVfgCllkWv2YgiYXDhWFBrEywll4A5JToyTNFA==} - dev: true + '@types/aes-js@3.1.4': {} - /@types/babel__preset-env@7.9.6: - resolution: {integrity: sha512-PaOA2V4J3CZZopQaTGT1e8WEWCqHWc1k12zLlci4T9eR2lQIlA/GbnVbloFDqYVFr1BNiCXnotH32Up8WdgTxQ==} - dev: true + '@types/babel__preset-env@7.10.0': {} - /@types/bluebird@3.5.42: - resolution: {integrity: sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==} - dev: false + '@types/bluebird@3.5.42': {} - /@types/caseless@0.12.5: - resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} - dev: false + '@types/caseless@0.12.5': {} - /@types/chance@1.1.6: - resolution: {integrity: sha512-V+pm3stv1Mvz8fSKJJod6CglNGVqEQ6OyuqitoDkWywEODM/eJd1eSuIp9xt6DrX8BWZ2eDSIzbw1tPCUTvGbQ==} - dev: false + '@types/chance@1.1.6': {} - /@types/cookie@0.6.0: - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - dev: true + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.13.15 - /@types/cookiejar@2.1.5: - resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} - dev: true + '@types/cookie@0.6.0': {} - /@types/crypto-js@4.2.2: - resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} - dev: true + '@types/cookiejar@2.1.5': {} - /@types/debug@4.1.12: - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - dependencies: - '@types/ms': 0.7.34 + '@types/crypto-js@4.2.2': {} - /@types/eslint-config-prettier@6.11.3: - resolution: {integrity: sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==} - dev: true + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 - /@types/eslint@8.56.10: - resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} + '@types/eslint@9.6.1': dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.7 '@types/json-schema': 7.0.15 - dev: true - /@types/estree@1.0.5: - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - dev: true + '@types/estree@1.0.6': {} - /@types/etag@1.8.3: - resolution: {integrity: sha512-QYHv9Yeh1ZYSMPQOoxY4XC4F1r+xRUiAriB303F4G6uBsT3KKX60DjiogvVv+2VISVDuJhcIzMdbjT+Bm938QQ==} + '@types/estree@1.0.7': {} + + '@types/etag@1.8.3': dependencies: - '@types/node': 20.12.8 - dev: true + '@types/node': 22.13.15 - /@types/fs-extra@11.0.4: - resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 20.12.8 - dev: true + '@types/node': 22.13.15 - /@types/html-to-text@9.0.4: - resolution: {integrity: sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==} - dev: true + '@types/html-to-text@9.0.4': {} - /@types/http-cache-semantics@4.0.4: - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/http-cache-semantics@4.0.4': {} - /@types/imapflow@1.0.18: - resolution: {integrity: sha512-BoWZUoMktji2YJmkRY8z0KsjvyDNpBzeC/rLVMFKcHkPxaKp+SHBFfx/kj7ltKh3l010Lc9RZqnJs8KUMNhf6Q==} + '@types/imapflow@1.0.20': dependencies: - '@types/node': 20.12.8 - dev: true + '@types/node': 22.13.15 - /@types/js-beautify@1.14.3: - resolution: {integrity: sha512-FMbQHz+qd9DoGvgLHxeqqVPaNRffpIu5ZjozwV8hf9JAGpIOzuAf4wGbRSo8LNITHqGjmmVjaMggTT5P4v4IHg==} - dev: true + '@types/js-beautify@1.14.3': {} - /@types/jsdom@21.1.6: - resolution: {integrity: sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==} + '@types/jsdom@21.1.7': dependencies: - '@types/node': 20.12.8 + '@types/node': 22.13.15 '@types/tough-cookie': 4.0.5 - parse5: 7.1.2 - dev: true + parse5: 7.2.1 - /@types/json-bigint@1.0.4: - resolution: {integrity: sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==} - dev: true + '@types/json-bigint@1.0.4': {} - /@types/json-schema@7.0.15: - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - dev: true + '@types/json-schema@7.0.15': {} - /@types/jsonfile@6.1.4: - resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/jsonfile@6.1.4': dependencies: - '@types/node': 20.12.8 - dev: true + '@types/node': 22.13.15 - /@types/jsrsasign@10.5.13: - resolution: {integrity: sha512-vvVHLrXxoUZgBWTcJnTMSC4FAQcG2loK7N1Uy20I3nr/aUhetbGdfuwSzXkrMoll2RoYKW0IcMIN0I0bwMwVMQ==} - dev: true + '@types/jsrsasign@10.5.13': {} - /@types/linkify-it@5.0.0: - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - dev: true + '@types/linkify-it@5.0.0': {} - /@types/lint-staged@13.3.0: - resolution: {integrity: sha512-WxGjVP+rA4OJlEdbZdT9MS9PFKQ7kVPhLn26gC+2tnBWBEFEj/KW+IbFfz6sxdxY5U6V7BvyF+3BzCGsAMHhNg==} - dev: true + '@types/lint-staged@13.3.0': {} - /@types/mailparser@3.4.4: - resolution: {integrity: sha512-C6Znp2QVS25JqtuPyxj38Qh+QoFcLycdxsvcc6IZCGekhaMBzbdTXzwGzhGoYb3TfKu8IRCNV0sV1o3Od97cEQ==} + '@types/mailparser@3.4.5': dependencies: - '@types/node': 20.12.8 + '@types/node': 22.13.15 iconv-lite: 0.6.3 - dev: true - /@types/markdown-it@14.1.1: - resolution: {integrity: sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==} + '@types/markdown-it@14.1.2': dependencies: '@types/linkify-it': 5.0.0 '@types/mdurl': 2.0.0 - dev: true - /@types/mdast@4.0.3: - resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} + '@types/mdast@4.0.4': dependencies: - '@types/unist': 3.0.2 - dev: true + '@types/unist': 3.0.3 - /@types/mdurl@2.0.0: - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - dev: true + '@types/mdurl@2.0.0': {} - /@types/methods@1.1.4: - resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} - dev: true + '@types/methods@1.1.4': {} - /@types/module-alias@2.0.4: - resolution: {integrity: sha512-5+G/QXO/DvHZw60FjvbDzO4JmlD/nG5m2/vVGt25VN1eeP3w2bCoks1Wa7VuptMPM1TxJdx6RjO70N9Fw0nZPA==} - dev: true + '@types/module-alias@2.0.4': {} - /@types/ms@0.7.34: - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + '@types/ms@2.1.0': {} - /@types/mute-stream@0.0.4: - resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + '@types/mute-stream@0.0.4': dependencies: - '@types/node': 20.12.8 - dev: true + '@types/node': 22.13.15 - /@types/node-fetch@2.6.11: - resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} + '@types/mysql@2.15.26': dependencies: - '@types/node': 20.12.8 - form-data: 4.0.0 - dev: false + '@types/node': 22.13.15 - /@types/node@20.12.8: - resolution: {integrity: sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==} + '@types/node-fetch@2.6.12': dependencies: - undici-types: 5.26.5 + '@types/node': 22.13.15 + form-data: 4.0.2 - /@types/normalize-package-data@2.4.4: - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - dev: true + '@types/node@22.13.15': + dependencies: + undici-types: 6.20.0 - /@types/request-promise@4.1.51: - resolution: {integrity: sha512-qVcP9Fuzh9oaAh8oPxiSoWMFGnWKkJDknnij66vi09Yiy62bsSDqtd+fG5kIM9wLLgZsRP3Y6acqj9O/v2ZtRw==} + '@types/normalize-package-data@2.4.4': {} + + '@types/pg-pool@2.0.6': + dependencies: + '@types/pg': 8.6.1 + + '@types/pg@8.6.1': + dependencies: + '@types/node': 22.13.15 + pg-protocol: 1.8.0 + pg-types: 2.2.0 + + '@types/request-promise@4.1.51': dependencies: '@types/bluebird': 3.5.42 '@types/request': 2.48.12 - dev: false - /@types/request@2.48.12: - resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==} + '@types/request@2.48.12': dependencies: '@types/caseless': 0.12.5 - '@types/node': 20.12.8 + '@types/node': 22.13.15 '@types/tough-cookie': 4.0.5 - form-data: 2.5.1 - dev: false + form-data: 2.5.3 - /@types/sanitize-html@2.11.0: - resolution: {integrity: sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==} + '@types/sanitize-html@2.15.0': dependencies: htmlparser2: 8.0.2 - dev: true - /@types/semver@7.5.8: - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - dev: true + '@types/shimmer@1.2.0': {} - /@types/statuses@2.0.5: - resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} - dev: true + '@types/statuses@2.0.5': {} - /@types/superagent@8.1.3: - resolution: {integrity: sha512-R/CfN6w2XsixLb1Ii8INfn+BT9sGPvw74OavfkW4SwY+jeUcAwLZv2+bXLJkndnimxjEBm0RPHgcjW9pLCa8cw==} + '@types/superagent@8.1.9': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 20.12.8 - dev: true + '@types/node': 22.13.15 + form-data: 4.0.2 - /@types/supertest@6.0.2: - resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==} + '@types/supertest@6.0.3': dependencies: '@types/methods': 1.1.4 - '@types/superagent': 8.1.3 - dev: true + '@types/superagent': 8.1.9 - /@types/tiny-async-pool@2.0.3: - resolution: {integrity: sha512-n3l1s538tKo9RBoHs4I3DG/VmD3VYhF5mHcgu1sU4Lq7JCNBtxnpBy3OkWSbZsp5r5QOuplh2UkXXXwufoAuNQ==} - dev: true + '@types/tedious@4.0.14': + dependencies: + '@types/node': 22.13.15 - /@types/title@3.4.3: - resolution: {integrity: sha512-mjupLOb4kwUuoUFokkacy/VMRVBH2qtqZ5AX7K7iha6+iKIkX80n/Y4EoNVEVRmer8dYJU/ry+fppUaDFVQh7Q==} - dev: true + '@types/tiny-async-pool@2.0.3': {} - /@types/tough-cookie@4.0.5: - resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/title@3.4.3': {} - /@types/triple-beam@1.3.5: - resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - dev: false + '@types/tough-cookie@4.0.5': {} - /@types/unist@3.0.2: - resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} - dev: true + '@types/triple-beam@1.3.5': {} - /@types/uuid@9.0.8: - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - dev: true + '@types/unist@3.0.3': {} - /@types/wrap-ansi@3.0.0: - resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} - dev: true + '@types/uuid@10.0.0': {} - /@types/yauzl@2.10.3: - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - requiresBuild: true + '@types/wrap-ansi@3.0.0': {} + + '@types/yauzl@2.10.3': dependencies: - '@types/node': 20.12.8 - dev: false + '@types/node': 22.13.15 optional: true - /@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2)': dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/scope-manager': 7.8.0 - '@typescript-eslint/type-utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.8.0 - debug: 4.3.4 - eslint: 8.57.0 + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.29.0(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/scope-manager': 8.29.0 + '@typescript-eslint/type-utils': 8.29.0(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.29.0(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.29.0 + eslint: 9.23.0 graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.3.2 natural-compare: 1.4.0 - semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 + ts-api-utils: 2.1.0(typescript@5.8.2) + typescript: 5.8.2 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/scope-manager': 7.8.0 - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.8.0 - debug: 4.3.4 - eslint: 8.57.0 - typescript: 5.4.5 + '@typescript-eslint/scope-manager': 8.29.0 + '@typescript-eslint/types': 8.29.0 + '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.29.0 + debug: 4.4.0 + eslint: 9.23.0 + typescript: 5.8.2 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/scope-manager@6.21.0: - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/scope-manager@8.28.0': dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - dev: true + '@typescript-eslint/types': 8.28.0 + '@typescript-eslint/visitor-keys': 8.28.0 - /@typescript-eslint/scope-manager@7.8.0: - resolution: {integrity: sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.29.0': dependencies: - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/visitor-keys': 7.8.0 - dev: true + '@typescript-eslint/types': 8.29.0 + '@typescript-eslint/visitor-keys': 8.29.0 - /@typescript-eslint/type-utils@7.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/type-utils@8.29.0(eslint@9.23.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - '@typescript-eslint/utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - debug: 4.3.4 - eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 + '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.8.2) + '@typescript-eslint/utils': 8.29.0(eslint@9.23.0)(typescript@5.8.2) + debug: 4.4.0 + eslint: 9.23.0 + ts-api-utils: 2.1.0(typescript@5.8.2) + typescript: 5.8.2 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/types@6.21.0: - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} - dev: true + '@typescript-eslint/types@8.28.0': {} - /@typescript-eslint/types@7.8.0: - resolution: {integrity: sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==} - engines: {node: ^18.18.0 || >=20.0.0} - dev: true + '@typescript-eslint/types@8.29.0': {} - /@typescript-eslint/typescript-estree@6.21.0(typescript@5.4.5): - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/typescript-estree@8.28.0(typescript@5.8.2)': dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4 - globby: 11.1.0 + '@typescript-eslint/types': 8.28.0 + '@typescript-eslint/visitor-keys': 8.28.0 + debug: 4.4.0 + fast-glob: 3.3.3 is-glob: 4.0.3 - minimatch: 9.0.3 - semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 + minimatch: 9.0.5 + semver: 7.7.1 + ts-api-utils: 2.1.0(typescript@5.8.2) + typescript: 5.8.2 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/typescript-estree@7.8.0(typescript@5.4.5): - resolution: {integrity: sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/typescript-estree@8.29.0(typescript@5.8.2)': dependencies: - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/visitor-keys': 7.8.0 - debug: 4.3.4 - globby: 11.1.0 + '@typescript-eslint/types': 8.29.0 + '@typescript-eslint/visitor-keys': 8.29.0 + debug: 4.4.0 + fast-glob: 3.3.3 is-glob: 4.0.3 - minimatch: 9.0.4 - semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 + minimatch: 9.0.5 + semver: 7.7.1 + ts-api-utils: 2.1.0(typescript@5.8.2) + typescript: 5.8.2 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) - eslint: 8.57.0 - semver: 7.6.0 + '@typescript-eslint/utils@8.28.0(eslint@9.23.0)(typescript@5.8.2)': + dependencies: + '@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0) + '@typescript-eslint/scope-manager': 8.28.0 + '@typescript-eslint/types': 8.28.0 + '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2) + eslint: 9.23.0 + typescript: 5.8.2 transitivePeerDependencies: - supports-color - - typescript - dev: true - /@typescript-eslint/utils@7.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 + '@typescript-eslint/utils@8.29.0(eslint@9.23.0)(typescript@5.8.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 7.8.0 - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - eslint: 8.57.0 - semver: 7.6.0 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0) + '@typescript-eslint/scope-manager': 8.29.0 + '@typescript-eslint/types': 8.29.0 + '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.8.2) + eslint: 9.23.0 + typescript: 5.8.2 transitivePeerDependencies: - supports-color - - typescript - dev: true - /@typescript-eslint/visitor-keys@6.21.0: - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/visitor-keys@8.28.0': dependencies: - '@typescript-eslint/types': 6.21.0 - eslint-visitor-keys: 3.4.3 - dev: true + '@typescript-eslint/types': 8.28.0 + eslint-visitor-keys: 4.2.0 - /@typescript-eslint/visitor-keys@7.8.0: - resolution: {integrity: sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/visitor-keys@8.29.0': dependencies: - '@typescript-eslint/types': 7.8.0 - eslint-visitor-keys: 3.4.3 - dev: true + '@typescript-eslint/types': 8.29.0 + eslint-visitor-keys: 4.2.0 - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: true + '@ungap/structured-clone@1.3.0': {} - /@vercel/nft@0.26.4: - resolution: {integrity: sha512-j4jCOOXke2t8cHZCIxu1dzKLHLcFmYzC3yqAK6MfZznOL1QIJKd0xcFsXK3zcqzU7ScsE2zWkiMMNHGMHgp+FA==} - engines: {node: '>=16'} - hasBin: true + '@unhead/schema@1.11.20': + dependencies: + hookable: 5.5.3 + zhead: 2.2.4 + + '@vercel/nft@0.29.2(rollup@4.37.0)': dependencies: - '@mapbox/node-pre-gyp': 1.0.11 - '@rollup/pluginutils': 4.2.1 - acorn: 8.11.3 - acorn-import-attributes: 1.9.2(acorn@8.11.3) + '@mapbox/node-pre-gyp': 2.0.0 + '@rollup/pluginutils': 5.1.4(rollup@4.37.0) + acorn: 8.14.1 + acorn-import-attributes: 1.9.5(acorn@8.14.1) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 - glob: 7.2.3 + glob: 10.4.5 graceful-fs: 4.2.11 - micromatch: 4.0.5 - node-gyp-build: 4.8.0 + node-gyp-build: 4.8.4 + picomatch: 4.0.2 resolve-from: 5.0.0 transitivePeerDependencies: - encoding + - rollup - supports-color - dev: true - /@vitest/coverage-v8@1.5.3(vitest@1.5.3): - resolution: {integrity: sha512-DPyGSu/fPHOJuPxzFSQoT4N/Fu/2aJfZRtEpEp8GI7NHsXBGE94CQ+pbEGBUMFjatsHPDJw/+TAF9r4ens2CNw==} - peerDependencies: - vitest: 1.5.3 + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.13.15)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.4.3(typescript@5.8.2)))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.4 + debug: 4.4.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.4 - istanbul-reports: 3.1.6 - magic-string: 0.30.8 - magicast: 0.3.3 - picocolors: 1.0.0 - std-env: 3.7.0 - strip-literal: 2.0.0 - test-exclude: 6.0.0 - vitest: 1.5.3(@types/node@20.12.8)(jsdom@24.0.0) + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.17 + magicast: 0.3.5 + std-env: 3.8.1 + test-exclude: 7.0.1 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@22.13.15)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.4.3(typescript@5.8.2)) transitivePeerDependencies: - supports-color - dev: true - /@vitest/expect@1.5.3: - resolution: {integrity: sha512-y+waPz31pOFr3rD7vWTbwiLe5+MgsMm40jTZbQE8p8/qXyBX3CQsIXRx9XK12IbY7q/t5a5aM/ckt33b4PxK2g==} + '@vitest/expect@2.1.9': dependencies: - '@vitest/spy': 1.5.3 - '@vitest/utils': 1.5.3 - chai: 4.4.1 - dev: true + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + tinyrainbow: 1.2.0 - /@vitest/runner@1.5.3: - resolution: {integrity: sha512-7PlfuReN8692IKQIdCxwir1AOaP5THfNkp0Uc4BKr2na+9lALNit7ub9l3/R7MP8aV61+mHKRGiqEKRIwu6iiQ==} + '@vitest/mocker@2.1.9(msw@2.4.3(typescript@5.8.2))(vite@5.4.15(@types/node@22.13.15))': dependencies: - '@vitest/utils': 1.5.3 - p-limit: 5.0.0 - pathe: 1.1.2 - dev: true + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + msw: 2.4.3(typescript@5.8.2) + vite: 5.4.15(@types/node@22.13.15) - /@vitest/snapshot@1.5.3: - resolution: {integrity: sha512-K3mvIsjyKYBhNIDujMD2gfQEzddLe51nNOAf45yKRt/QFJcUIeTQd2trRvv6M6oCBHNVnZwFWbQ4yj96ibiDsA==} + '@vitest/pretty-format@2.1.9': dependencies: - magic-string: 0.30.8 - pathe: 1.1.2 - pretty-format: 29.7.0 - dev: true + tinyrainbow: 1.2.0 - /@vitest/spy@1.5.3: - resolution: {integrity: sha512-Llj7Jgs6lbnL55WoshJUUacdJfjU2honvGcAJBxhra5TPEzTJH8ZuhI3p/JwqqfnTr4PmP7nDmOXP53MS7GJlg==} + '@vitest/runner@2.1.9': dependencies: - tinyspy: 2.2.1 - dev: true + '@vitest/utils': 2.1.9 + pathe: 1.1.2 - /@vitest/utils@1.5.3: - resolution: {integrity: sha512-rE9DTN1BRhzkzqNQO+kw8ZgfeEBCLXiHJwetk668shmNBpSagQxneT5eSqEBLP+cqSiAeecvQmbpFfdMyLcIQA==} + '@vitest/snapshot@2.1.9': dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - dev: true - - /abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: true - - /abbrev@2.0.0: - resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.17 + pathe: 1.1.2 - /abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} + '@vitest/spy@2.1.9': dependencies: - event-target-shim: 5.0.1 - dev: false + tinyspy: 3.0.2 - /acorn-import-attributes@1.9.2(acorn@8.11.3): - resolution: {integrity: sha512-O+nfJwNolEA771IYJaiLWK1UAwjNsQmZbTRqqwBYxCgVQTmpFEMvBw6LOIQV0Me339L5UMVYFyRohGnGlQDdIQ==} - peerDependencies: - acorn: ^8 + '@vitest/utils@2.1.9': dependencies: - acorn: 8.11.3 - dev: true + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.3 + tinyrainbow: 1.2.0 - /acorn-jsx@5.3.2(acorn@8.11.3): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.11.3 - dev: true + abbrev@2.0.0: {} - /acorn-walk@8.3.2: - resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} - engines: {node: '>=0.4.0'} - dev: true + abbrev@3.0.0: {} - /acorn@5.7.4: - resolution: {integrity: sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: false + acorn-import-attributes@1.9.5(acorn@8.14.1): + dependencies: + acorn: 8.14.1 - /acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true + acorn-jsx@5.3.2(acorn@8.14.1): + dependencies: + acorn: 8.14.1 - /aes-js@3.1.2: - resolution: {integrity: sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==} - dev: false + acorn@5.7.4: {} - /agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - dependencies: - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true + acorn@8.14.1: {} - /agent-base@7.1.0: - resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} - engines: {node: '>= 14'} - dependencies: - debug: 4.3.4 - transitivePeerDependencies: - - supports-color + aes-js@3.1.2: {} - /agent-base@7.1.1: - resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} - engines: {node: '>= 14'} - dependencies: - debug: 4.3.4 - transitivePeerDependencies: - - supports-color + agent-base@7.1.3: {} - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - /ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 - dev: true - /ansi-escapes@6.2.0: - resolution: {integrity: sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==} - engines: {node: '>=14.16'} + ansi-escapes@7.0.0: dependencies: - type-fest: 3.13.1 - dev: true + environment: 1.1.0 - /ansi-regex@2.1.1: - resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} - engines: {node: '>=0.10.0'} - dev: true + ansi-regex@2.1.1: {} - /ansi-regex@4.1.1: - resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} - engines: {node: '>=6'} - dev: true + ansi-regex@4.1.1: {} - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + ansi-regex@5.0.1: {} - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - dev: true + ansi-regex@6.1.0: {} - /ansi-styles@2.2.1: - resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} - engines: {node: '>=0.10.0'} - dev: true + ansi-styles@2.2.1: {} - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} + ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - dev: true - - /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - dev: true - - /aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} - dev: true - - /arch@2.2.0: - resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} - dev: false - - /are-we-there-yet@2.0.0: - resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} - engines: {node: '>=10'} - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 - dev: true - - /arg@1.0.0: - resolution: {integrity: sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==} - dev: false + ansi-styles@6.2.1: {} - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + arg@5.0.2: {} - /arr-union@3.1.0: - resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} - engines: {node: '>=0.10.0'} - dev: false + argparse@2.0.1: {} - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - dev: true + arr-union@3.1.0: {} - /art-template@4.13.2: - resolution: {integrity: sha512-04ws5k+ndA5DghfheY4c8F1304XJKeTcaXqZCLpxFkNMSkaR3ChW1pX2i9d3sEEOZuLy7de8lFriRaik1jEeOQ==} - engines: {node: '>= 1.0.0'} + art-template@4.13.2: dependencies: acorn: 5.7.4 escodegen: 1.14.3 estraverse: 4.3.0 - html-minifier: 3.5.21 + html-minifier: 4.0.0 is-keyword-js: 1.0.3 js-tokens: 3.0.2 merge-source-map: 1.1.0 source-map: 0.5.7 - dev: false - /asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - dev: true + asap@2.0.6: {} - /asn1@0.2.6: - resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + asn1@0.2.6: dependencies: - safer-buffer: 2.1.2 - dev: false + safer-buffer: '@nolyfill/safer-buffer@1.0.44' - /assert-plus@1.0.0: - resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} - engines: {node: '>=0.8'} - dev: false + assert-plus@1.0.0: {} - /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - dev: true + assertion-error@2.0.1: {} - /ast-types@0.13.4: - resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} - engines: {node: '>=4'} + ast-types@0.13.4: dependencies: - tslib: 2.6.2 - dev: false + tslib: 2.8.1 - /async-mutex@0.3.2: - resolution: {integrity: sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==} + async-mutex@0.3.2: dependencies: - tslib: 2.6.2 - dev: false + tslib: 2.8.1 - /async-sema@3.1.1: - resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} - dev: true + async-sema@3.1.1: {} - /async@3.2.5: - resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} - dev: false + async@3.2.6: {} - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + asynckit@0.4.0: {} - /atomic-sleep@1.0.0: - resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} - engines: {node: '>=8.0.0'} - dev: false + atomic-sleep@1.0.0: {} - /aws-sign2@0.7.0: - resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} - dev: false + aws-sign2@0.7.0: {} - /aws4@1.12.0: - resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} - dev: false + aws4@1.13.2: {} - /b4a@1.6.6: - resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} - dev: false + b4a@1.6.7: {} - /babel-plugin-polyfill-corejs2@0.4.10(@babel/core@7.24.4): - resolution: {integrity: sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.26.10): dependencies: - '@babel/compat-data': 7.24.4 - '@babel/core': 7.24.4 - '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.24.4) + '@babel/compat-data': 7.26.8 + '@babel/core': 7.26.10 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.10) semver: 6.3.1 transitivePeerDependencies: - supports-color - dev: true - /babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.4): - resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.26.10): dependencies: - '@babel/core': 7.24.4 - '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.24.4) - core-js-compat: 3.36.1 + '@babel/core': 7.26.10 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.10) + core-js-compat: 3.41.0 transitivePeerDependencies: - supports-color - dev: true - /babel-plugin-polyfill-regenerator@0.6.1(@babel/core@7.24.4): - resolution: {integrity: sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.26.10): dependencies: - '@babel/core': 7.24.4 - '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.24.4) + '@babel/core': 7.26.10 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.10) transitivePeerDependencies: - supports-color - dev: true - /bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - dev: true + bail@2.0.2: {} - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@1.0.2: {} - /bare-events@2.2.0: - resolution: {integrity: sha512-Yyyqff4PIFfSuthCZqLlPISTWHmnQxoPuAvkmgzsJEmG3CesdIv6Xweayl0JkCZJSB2yYIdJyEz97tpxNhgjbg==} - requiresBuild: true - dev: false + bare-events@2.5.4: optional: true - /bare-fs@2.2.3: - resolution: {integrity: sha512-amG72llr9pstfXOBOHve1WjiuKKAMnebcmMbPWDZ7BCevAoJLpugjuAPRsDINEyjT0a6tbaVx3DctkXIRbLuJw==} - requiresBuild: true + bare-fs@2.3.5: dependencies: - bare-events: 2.2.0 - bare-path: 2.1.1 - streamx: 2.15.8 - dev: false + bare-events: 2.5.4 + bare-path: 2.1.3 + bare-stream: 2.6.5(bare-events@2.5.4) + transitivePeerDependencies: + - bare-buffer optional: true - /bare-os@2.2.0: - resolution: {integrity: sha512-hD0rOPfYWOMpVirTACt4/nK8mC55La12K5fY1ij8HAdfQakD62M+H4o4tpfKzVGLgRDTuk3vjA4GqGXXCeFbag==} - requiresBuild: true - dev: false + bare-os@2.4.4: optional: true - /bare-path@2.1.1: - resolution: {integrity: sha512-OHM+iwRDRMDBsSW7kl3dO62JyHdBKO3B25FB9vNQBPcGHMo4+eA8Yj41Lfbk3pS/seDY+siNge0LdRTulAau/A==} - requiresBuild: true + bare-path@2.1.3: dependencies: - bare-os: 2.2.0 - dev: false + bare-os: 2.4.4 optional: true - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bare-stream@2.6.5(bare-events@2.5.4): + dependencies: + streamx: 2.22.0 + optionalDependencies: + bare-events: 2.5.4 + optional: true - /basic-ftp@5.0.4: - resolution: {integrity: sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==} - engines: {node: '>=10.0.0'} - dev: false + base64-js@1.5.1: {} - /bbcodejs@0.0.4: - resolution: {integrity: sha512-y7dPUdqXMMUeR0bBv/9hcLTMwkBRuVCK+9aJgYGYvdkIdXBfrEiEiVw5Cg8iluRCG7YzLFWoX6VEmNe1HeP0wA==} - dev: false + basic-ftp@5.0.5: {} - /bcrypt-pbkdf@1.0.2: - resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + bcrypt-pbkdf@1.0.2: dependencies: tweetnacl: 0.14.5 - dev: false - /big-integer@1.6.52: - resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} - engines: {node: '>=0.6'} - dev: false + big-integer@1.6.52: {} - /bignumber.js@9.1.2: - resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} - dev: false + bignumber.js@9.1.2: {} - /bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 - dev: true - /bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - dev: true - /bluebird@2.11.0: - resolution: {integrity: sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==} - dev: false + bluebird@2.11.0: {} - /bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - dev: false + bluebird@3.7.2: {} - /boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - dev: false + boolbase@1.0.0: {} - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 - dev: true - - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - /brotli@1.3.3: - resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + braces@3.0.3: dependencies: - base64-js: 1.5.1 - dev: false + fill-range: 7.1.1 - /browserslist@4.23.0: - resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true + browserslist@4.24.4: dependencies: - caniuse-lite: 1.0.30001587 - electron-to-chromium: 1.4.669 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.23.0) - dev: true + caniuse-lite: 1.0.30001707 + electron-to-chromium: 1.5.128 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.4) - /buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - dev: false + buffer-crc32@0.2.13: {} - /buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - dev: false + buffer-equal-constant-time@1.0.1: {} - /buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - /buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + buffer@6.0.3: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: false - /bufferutil@4.0.8: - resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} - engines: {node: '>=6.14.2'} - requiresBuild: true + bufferutil@4.0.9: dependencies: - node-gyp-build: 4.8.0 - dev: false + node-gyp-build: 4.8.4 - /builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - dev: true + builtin-modules@5.0.0: {} - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: true + cac@6.7.14: {} - /cacheable-lookup@7.0.0: - resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} - engines: {node: '>=14.16'} + cacheable-lookup@7.0.0: {} - /cacheable-request@10.2.14: - resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} - engines: {node: '>=14.16'} + cacheable-request@10.2.14: dependencies: '@types/http-cache-semantics': 4.0.4 get-stream: 6.0.1 http-cache-semantics: 4.1.1 keyv: 4.5.4 mimic-response: 4.0.0 - normalize-url: 8.0.0 + normalize-url: 8.0.1 responselike: 3.0.0 - /call-bind@1.0.7: - resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} - engines: {node: '>= 0.4'} + cacheable-request@12.0.1: dependencies: - es-define-property: 1.0.0 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.2.4 - set-function-length: 1.2.1 + '@types/http-cache-semantics': 4.0.4 + get-stream: 9.0.1 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.0.1 + responselike: 3.0.0 - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} + callsites@3.1.0: {} - /camel-case@3.0.0: - resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + camel-case@3.0.0: dependencies: no-case: 2.3.2 upper-case: 1.1.3 - dev: false - /camelcase-keys@7.0.2: - resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} - engines: {node: '>=12'} + camelcase-keys@7.0.2: dependencies: camelcase: 6.3.0 map-obj: 4.3.0 quick-lru: 5.1.1 type-fest: 1.4.0 - dev: false - /camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - dev: false + camelcase@5.3.1: {} - /camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - dev: false + camelcase@6.3.0: {} - /caniuse-lite@1.0.30001587: - resolution: {integrity: sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==} - dev: true + caniuse-lite@1.0.30001707: {} - /caseless@0.12.0: - resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} - dev: false + caseless@0.12.0: {} - /chai@4.4.1: - resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} - engines: {node: '>=4'} + chai@5.2.0: dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.3 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.0.8 - dev: true + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.3 + pathval: 2.0.0 - /chalk@1.1.3: - resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} - engines: {node: '>=0.10.0'} + chalk@1.1.3: dependencies: ansi-styles: 2.2.1 escape-string-regexp: 1.0.5 has-ansi: 2.0.0 strip-ansi: 3.0.1 supports-color: 2.0.0 - dev: true - - /chalk@2.3.0: - resolution: {integrity: sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 4.5.0 - dev: false - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true - /chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: true + chalk@5.4.1: {} - /chance@1.1.11: - resolution: {integrity: sha512-kqTg3WWywappJPqtgrdvbA380VoXO2eu9VCV895JgbyHsaErXdyHK9LOZ911OvAk6L0obK7kDk9CGs8+oBawVA==} - dev: false + chance@1.1.12: {} - /character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - dev: true + character-entities@2.0.2: {} - /chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: true + chardet@0.7.0: {} - /check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - dependencies: - get-func-name: 2.0.2 - dev: true + check-error@2.1.1: {} - /cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 css-select: 5.1.0 css-what: 6.1.0 domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.1.0 - dev: false + domutils: 3.2.2 - /cheerio@0.22.0: - resolution: {integrity: sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==} - engines: {node: '>= 0.6'} + cheerio@0.22.0: dependencies: css-select: 1.2.0 dom-serializer: 0.1.1 @@ -3771,817 +8589,514 @@ packages: lodash.reduce: 4.6.0 lodash.reject: 4.6.0 lodash.some: 4.6.0 - dev: false - /cheerio@1.0.0-rc.12: - resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} - engines: {node: '>= 6'} + cheerio@1.0.0: dependencies: cheerio-select: 2.1.0 dom-serializer: 2.0.0 domhandler: 5.0.3 - domutils: 3.1.0 - htmlparser2: 8.0.2 - parse5: 7.1.2 - parse5-htmlparser2-tree-adapter: 7.0.0 - dev: false + domutils: 3.2.2 + encoding-sniffer: 0.2.0 + htmlparser2: 9.1.0 + parse5: 7.2.1 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 6.21.2 + whatwg-mimetype: 4.0.0 - /chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - dev: true + chownr@3.0.0: {} - /chromium-bidi@0.5.16(devtools-protocol@0.0.1262051): - resolution: {integrity: sha512-IT5lnR44h/qZQ4GaCHvBxYIl4cQL2i9UvFyYeRyVdcpY04hx5H720HQfe/7Oz7ndxaYVLQFGpCO71J4X2Ye/Gw==} - peerDependencies: - devtools-protocol: '*' + chromium-bidi@0.5.16(devtools-protocol@0.0.1262051): dependencies: devtools-protocol: 0.0.1262051 mitt: 3.0.1 urlpattern-polyfill: 10.0.0 zod: 3.22.4 - dev: false - /chrono-node@2.7.5: - resolution: {integrity: sha512-VJWqFN5rWmXVvXAxOD4i0jX8Tb4cLswaslyaAFhxM45zNXPsZleygPbgiaYBD7ORb9fj07zBgJb0Q6eKL+0iJg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + chrono-node@2.7.9: dependencies: dayjs: 1.11.8 - dev: false - /ci-info@4.0.0: - resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} - engines: {node: '>=8'} - dev: true + ci-info@4.2.0: {} - /city-timezones@1.2.1: - resolution: {integrity: sha512-hruuB611QFoUFMsan7xd9B2VPMrA8XC716O/999WW34kmaJUT1hxKF2W8TSXAWkhSqgvbu70DjcDv7/wpM6vow==} + city-timezones@1.3.0: dependencies: lodash: 4.17.21 - dev: false - /class-transformer@0.3.1: - resolution: {integrity: sha512-cKFwohpJbuMovS8xVLmn8N2AUbAuc8pVo4zEfsUVo8qgECOogns1WVk/FkOZoxhOPTyTYFckuoH+13FO+MQ8GA==} - dev: false + cjs-module-lexer@1.4.3: {} - /clean-css@4.2.4: - resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} - engines: {node: '>= 4.0'} + class-transformer@0.3.1: {} + + clean-css@4.2.4: dependencies: source-map: 0.6.1 - dev: false - /clean-regexp@1.0.0: - resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} - engines: {node: '>=4'} + clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 - dev: true - /cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 - dev: true - /cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cli-cursor@5.0.0: dependencies: - restore-cursor: 4.0.0 - dev: true + restore-cursor: 5.1.0 - /cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - dev: true + cli-spinners@2.9.2: {} - /cli-truncate@4.0.0: - resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} - engines: {node: '>=18'} + cli-truncate@4.0.0: dependencies: slice-ansi: 5.0.0 - string-width: 7.1.0 - dev: true + string-width: 7.2.0 - /cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - dev: true + cli-width@3.0.0: {} - /cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} - dev: true + cli-width@4.1.0: {} - /clipboardy@1.2.2: - resolution: {integrity: sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw==} - engines: {node: '>=4'} + clipboardy@4.0.0: dependencies: - arch: 2.2.0 - execa: 0.8.0 - dev: false + execa: 8.0.1 + is-wsl: 3.1.0 + is64bit: 2.0.0 - /cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - /clone-deep@0.2.4: - resolution: {integrity: sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==} - engines: {node: '>=0.10.0'} + clone-deep@0.2.4: dependencies: for-own: 0.1.5 is-plain-object: 2.0.4 kind-of: 3.2.2 lazy-cache: 1.0.4 shallow-clone: 0.1.2 - dev: false - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - dev: true + clone@1.0.4: {} - /cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} - dev: false + cluster-key-slot@1.1.2: {} - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@1.9.3: dependencies: color-name: 1.1.3 - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + color-convert@2.0.1: dependencies: color-name: 1.1.4 - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.3: {} - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-name@1.1.4: {} - /color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-string@1.9.1: dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 - dev: false - - /color-support@1.1.3: - resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - dev: true - /color@3.2.1: - resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + color@3.2.1: dependencies: color-convert: 1.9.3 color-string: 1.9.1 - dev: false - /colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - dev: true + colorette@2.0.20: {} - /colorspace@1.1.4: - resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + colorspace@1.1.4: dependencies: color: 3.2.1 text-hex: 1.0.0 - dev: false - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - /commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} - engines: {node: '>=14'} - dev: true - - /commander@11.1.0: - resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} - engines: {node: '>=16'} - dev: true + commander@10.0.1: {} - /commander@2.17.1: - resolution: {integrity: sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==} - dev: false + commander@13.1.0: {} - /commander@2.19.0: - resolution: {integrity: sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==} - dev: false + commander@2.20.3: {} - /component-emitter@1.3.1: - resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} - dev: true + component-emitter@1.3.1: {} - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-map@0.0.1: {} - /config-chain@1.1.13: - resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + config-chain@1.1.13: dependencies: ini: 1.3.8 proto-list: 1.2.4 - dev: true - /console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - dev: true + consola@3.4.2: {} - /convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - dev: true + convert-source-map@2.0.0: {} - /cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} - dev: true + cookie@0.7.2: {} - /cookiejar@2.1.4: - resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - dev: true + cookiejar@2.1.4: {} - /core-js-compat@3.36.1: - resolution: {integrity: sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==} + core-js-compat@3.41.0: dependencies: - browserslist: 4.23.0 - dev: true + browserslist: 4.24.4 - /core-js@2.6.12: - resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} - deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. - requiresBuild: true - dev: false + core-js@2.6.12: {} - /core-util-is@1.0.2: - resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - dev: false + core-util-is@1.0.2: {} - /cosmiconfig@9.0.0(typescript@5.4.5): - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true + cosmiconfig@9.0.0(typescript@5.8.2): dependencies: env-paths: 2.2.1 - import-fresh: 3.3.0 + import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 - typescript: 5.4.5 - dev: false - - /cross-env@7.0.3: - resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} - engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} - hasBin: true - dependencies: - cross-spawn: 7.0.3 - dev: false + optionalDependencies: + typescript: 5.8.2 - /cross-spawn@5.1.0: - resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + cross-env@7.0.3: dependencies: - lru-cache: 4.1.5 - shebang-command: 1.2.0 - which: 1.3.1 - dev: false + cross-spawn: 7.0.6 - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - /crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - dev: false + crypto-js@4.2.0: {} - /css-select@1.2.0: - resolution: {integrity: sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==} + css-select@1.2.0: dependencies: boolbase: 1.0.0 css-what: 2.1.3 domutils: 1.5.1 nth-check: 1.0.2 - dev: false - /css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + css-select@5.1.0: dependencies: boolbase: 1.0.0 css-what: 6.1.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 nth-check: 2.1.1 - dev: false - /css-what@2.1.3: - resolution: {integrity: sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==} - dev: false + css-what@2.1.3: {} - /css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - dev: false + css-what@6.1.0: {} - /cssstyle@4.0.1: - resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} - engines: {node: '>=18'} + cssstyle@4.3.0: dependencies: - rrweb-cssom: 0.6.0 + '@asamuzakjp/css-color': 3.1.1 + rrweb-cssom: 0.8.0 - /currency-symbol-map@5.1.0: - resolution: {integrity: sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw==} - dev: false + currency-symbol-map@5.1.0: {} - /d@1.0.1: - resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} + d@1.0.2: dependencies: es5-ext: 0.10.64 - type: 1.2.0 - dev: false + type: 2.7.3 - /dashdash@1.14.1: - resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} - engines: {node: '>=0.10'} + dashdash@1.14.1: dependencies: assert-plus: 1.0.0 - dev: false - /data-uri-to-buffer@6.0.2: - resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} - engines: {node: '>= 14'} - dev: false + data-uri-to-buffer@6.0.2: {} - /data-urls@5.0.0: - resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} - engines: {node: '>=18'} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 - whatwg-url: 14.0.0 + whatwg-url: 14.2.0 - /dayjs@1.11.8: - resolution: {integrity: sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==} - dev: false + date-fns@4.1.0: {} - /debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + dayjs@1.11.8: {} + + debug@2.6.9: dependencies: ms: 2.0.0 - dev: false - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + debug@4.3.4: dependencies: ms: 2.1.2 - /decamelize-keys@1.1.1: - resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} - engines: {node: '>=0.10.0'} + debug@4.4.0: + dependencies: + ms: 2.1.3 + + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 map-obj: 1.0.1 - dev: false - /decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - dev: false + decamelize@1.2.0: {} - /decimal.js@10.4.3: - resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + decimal.js@10.5.0: {} - /decode-named-character-reference@1.0.2: - resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + decode-named-character-reference@1.1.0: dependencies: character-entities: 2.0.2 - dev: true - /decode-uri-component@0.2.2: - resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} - engines: {node: '>=0.10'} - dev: false + decode-uri-component@0.2.2: {} - /decode-uri-component@0.4.1: - resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} - engines: {node: '>=14.16'} - dev: false + decode-uri-component@0.4.1: {} - /decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 - /deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} - engines: {node: '>=6'} - dependencies: - type-detect: 4.0.8 - dev: true + deep-eql@5.0.2: {} - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deep-is@0.1.4: {} - /deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - dev: false + deepmerge@4.3.1: {} - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + defaults@1.0.4: dependencies: clone: 1.0.4 - dev: true - /defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} + defer-to-connect@2.0.1: {} - /define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - dependencies: - es-define-property: 1.0.0 - es-errors: 1.3.0 - gopd: 1.0.1 + define-lazy-prop@2.0.0: {} - /degenerator@5.0.1: - resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} - engines: {node: '>= 14'} + degenerator@5.0.1: dependencies: ast-types: 0.13.4 escodegen: 2.1.0 esprima: 4.0.1 - dev: false - - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - /delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - dev: true + delayed-stream@1.0.0: {} - /denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - dev: false + denque@2.1.0: {} - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: true + dequal@2.0.3: {} - /destr@2.0.3: - resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} - dev: false + destr@2.0.3: {} - /detect-libc@2.0.2: - resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} - engines: {node: '>=8'} - dev: true + detect-libc@2.0.3: {} - /devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + devlop@1.1.0: dependencies: dequal: 2.0.3 - dev: true - /devtools-protocol@0.0.1262051: - resolution: {integrity: sha512-YJe4CT5SA8on3Spa+UDtNhEqtuV6Epwz3OZ4HQVLhlRccpZ9/PAYk0/cy/oKxFKRrZPBUPyxympQci4yWNWZ9g==} - dev: false + devtools-protocol@0.0.1262051: {} - /dezalgo@1.0.4: - resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dezalgo@1.0.4: dependencies: asap: 2.0.6 wrappy: 1.0.2 - dev: true - /diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true - - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + difflib@https://codeload.github.com/postlight/difflib.js/tar.gz/32e8e38c7fcd935241b9baab71bb432fd9b166ed: dependencies: - path-type: 4.0.0 - dev: true + heap: 0.2.7 - /directory-import@3.3.1: - resolution: {integrity: sha512-d9paCbverdqmuwR+B40phSqiHhgPKiP8dpsMz5WT9U6ug2VVQ3tqXNCedpa6iGHg6mgv9lHaoq5DJUu2IXMjsQ==} - engines: {node: '>=18.17.0'} - dev: false + directory-import@3.3.2: {} - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + discord-api-types@0.37.119: {} + + doctrine@3.0.0: dependencies: esutils: 2.0.3 - dev: true - /dom-serializer@0.1.1: - resolution: {integrity: sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==} + dom-serializer@0.1.1: dependencies: domelementtype: 1.3.1 entities: 1.1.2 - dev: false - /dom-serializer@1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dom-serializer@1.4.1: dependencies: domelementtype: 2.3.0 domhandler: 4.3.1 entities: 2.2.0 - dev: false - /dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.5.0 - /domelementtype@1.3.1: - resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} - dev: false + domelementtype@1.3.1: {} - /domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domelementtype@2.3.0: {} - /domhandler@2.4.2: - resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} + domhandler@2.4.2: dependencies: domelementtype: 1.3.1 - dev: false - /domhandler@4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} + domhandler@4.3.1: dependencies: domelementtype: 2.3.0 - dev: false - /domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} + domhandler@5.0.3: dependencies: domelementtype: 2.3.0 - /domino@2.1.6: - resolution: {integrity: sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==} - dev: false - - /domutils@1.5.1: - resolution: {integrity: sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==} + domutils@1.5.1: dependencies: dom-serializer: 0.1.1 domelementtype: 1.3.1 - dev: false - /domutils@1.7.0: - resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + domutils@1.7.0: dependencies: dom-serializer: 0.1.1 domelementtype: 1.3.1 - dev: false - /domutils@2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 domelementtype: 2.3.0 domhandler: 4.3.1 - dev: false - /domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 - /dotenv@16.4.5: - resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} - engines: {node: '>=12'} - dev: false + dotenv@16.4.7: {} - /dotenv@6.2.0: - resolution: {integrity: sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==} - engines: {node: '>=6'} - dev: false + dotenv@6.2.0: {} - /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true + eastasianwidth@0.2.0: {} - /ecc-jsbn@0.1.2: - resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + ecc-jsbn@0.1.2: dependencies: jsbn: 0.1.1 - safer-buffer: 2.1.2 - dev: false + safer-buffer: '@nolyfill/safer-buffer@1.0.44' - /ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ecdsa-sig-formatter@1.0.11: dependencies: - safe-buffer: 5.2.1 - dev: false + safe-buffer: '@nolyfill/safe-buffer@1.0.44' - /editorconfig@1.0.4: - resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} - engines: {node: '>=14'} - hasBin: true + editorconfig@1.0.4: dependencies: '@one-ini/wasm': 0.1.1 commander: 10.0.1 minimatch: 9.0.1 - semver: 7.6.0 - dev: true + semver: 7.7.1 - /electron-to-chromium@1.4.669: - resolution: {integrity: sha512-E2SmpffFPrZhBSgf8ibqanRS2mpuk3FIRDzLDwt7WFpfgJMKDHJs0hmacyP0PS1cWsq0dVkwIIzlscNaterkPg==} - dev: true + electron-to-chromium@1.5.128: {} - /ellipsize@0.1.0: - resolution: {integrity: sha512-5gxbEjcb/Z2n6TTmXZx9wVi3N/DOzE7RXY3Xg9dakDuhX/izwumB9rGjeWUV6dTA0D0+juvo+JonZgNR9sgA5A==} - dev: false + ellipsize@0.1.0: {} - /emoji-regex@10.3.0: - resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} - dev: true + emoji-regex@10.4.0: {} - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@8.0.0: {} - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: true + emoji-regex@9.2.2: {} - /enabled@2.0.0: - resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} - dev: false + enabled@2.0.0: {} - /encoding-japanese@2.0.0: - resolution: {integrity: sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==} - engines: {node: '>=8.10.0'} - dev: false + encoding-japanese@2.2.0: {} - /encoding-japanese@2.1.0: - resolution: {integrity: sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w==} - engines: {node: '>=8.10.0'} - dev: false + encoding-sniffer@0.2.0: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 - /end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + end-of-stream@1.4.4: dependencies: once: 1.4.0 - dev: false - /enhanced-resolve@5.16.0: - resolution: {integrity: sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==} - engines: {node: '>=10.13.0'} + enhanced-resolve@5.18.1: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 - dev: true - /entities@1.1.2: - resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} - dev: false + entities@1.1.2: {} - /entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - dev: false + entities@2.2.0: {} - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} + entities@4.5.0: {} - /env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - dev: false + entities@6.0.0: {} - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - dependencies: - is-arrayish: 0.2.1 + env-paths@2.2.1: {} - /es-define-property@1.0.0: - resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} - engines: {node: '>= 0.4'} + environment@1.1.0: {} + + error-ex@1.3.2: dependencies: - get-intrinsic: 1.2.4 + is-arrayish: 0.2.1 - /es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + es-module-lexer@1.6.0: {} - /es5-ext@0.10.64: - resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} - engines: {node: '>=0.10'} - requiresBuild: true + es5-ext@0.10.64: dependencies: es6-iterator: 2.0.3 - es6-symbol: 3.1.3 + es6-symbol: 3.1.4 esniff: 2.0.1 next-tick: 1.1.0 - dev: false - /es6-iterator@2.0.3: - resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + es6-iterator@2.0.3: dependencies: - d: 1.0.1 + d: 1.0.2 es5-ext: 0.10.64 - es6-symbol: 3.1.3 - dev: false + es6-symbol: 3.1.4 - /es6-symbol@3.1.3: - resolution: {integrity: sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==} + es6-symbol@3.1.4: dependencies: - d: 1.0.1 + d: 1.0.2 ext: 1.7.0 - dev: false - /esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true + esbuild@0.21.5: optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 - - /escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - /escodegen@1.14.3: - resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} - engines: {node: '>=4.0'} - hasBin: true + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.1 + '@esbuild/android-arm': 0.25.1 + '@esbuild/android-arm64': 0.25.1 + '@esbuild/android-x64': 0.25.1 + '@esbuild/darwin-arm64': 0.25.1 + '@esbuild/darwin-x64': 0.25.1 + '@esbuild/freebsd-arm64': 0.25.1 + '@esbuild/freebsd-x64': 0.25.1 + '@esbuild/linux-arm': 0.25.1 + '@esbuild/linux-arm64': 0.25.1 + '@esbuild/linux-ia32': 0.25.1 + '@esbuild/linux-loong64': 0.25.1 + '@esbuild/linux-mips64el': 0.25.1 + '@esbuild/linux-ppc64': 0.25.1 + '@esbuild/linux-riscv64': 0.25.1 + '@esbuild/linux-s390x': 0.25.1 + '@esbuild/linux-x64': 0.25.1 + '@esbuild/netbsd-arm64': 0.25.1 + '@esbuild/netbsd-x64': 0.25.1 + '@esbuild/openbsd-arm64': 0.25.1 + '@esbuild/openbsd-x64': 0.25.1 + '@esbuild/sunos-x64': 0.25.1 + '@esbuild/win32-arm64': 0.25.1 + '@esbuild/win32-ia32': 0.25.1 + '@esbuild/win32-x64': 0.25.1 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escodegen@1.14.3: dependencies: esprima: 4.0.1 estraverse: 4.3.0 @@ -4589,224 +9104,153 @@ packages: optionator: 0.8.3 optionalDependencies: source-map: 0.6.1 - dev: false - /escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true + escodegen@2.1.0: dependencies: esprima: 4.0.1 estraverse: 5.3.0 esutils: 2.0.3 optionalDependencies: source-map: 0.6.1 - dev: false - /eslint-compat-utils@0.1.2(eslint@8.57.0): - resolution: {integrity: sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==} - engines: {node: '>=12'} - peerDependencies: - eslint: '>=6.0.0' + eslint-compat-utils@0.5.1(eslint@9.23.0): dependencies: - eslint: 8.57.0 - dev: true + eslint: 9.23.0 + semver: 7.7.1 - /eslint-compat-utils@0.5.0(eslint@8.57.0): - resolution: {integrity: sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg==} - engines: {node: '>=12'} - peerDependencies: - eslint: '>=6.0.0' + eslint-compat-utils@0.6.4(eslint@9.23.0): dependencies: - eslint: 8.57.0 - semver: 7.6.0 - dev: true + eslint: 9.23.0 + semver: 7.7.1 - /eslint-config-prettier@9.1.0(eslint@8.57.0): - resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' + eslint-config-prettier@10.1.1(eslint@9.23.0): dependencies: - eslint: 8.57.0 - dev: true + eslint: 9.23.0 - /eslint-filtered-fix@0.3.0(eslint@8.57.0): - resolution: {integrity: sha512-UMHOza9epEn9T+yVT8RiCFf0JdALpVzmoH62Ez/zvxM540IyUNAkr7aH2Frkv6zlm9a/gbmq/sc7C4SvzZQXcA==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' + eslint-filtered-fix@0.3.0(eslint@9.23.0): dependencies: - eslint: 8.57.0 - optionator: 0.9.3 - dev: true + eslint: 9.23.0 + optionator: 0.9.4 - /eslint-formatter-friendly@7.0.0: - resolution: {integrity: sha512-WXg2D5kMHcRxIZA3ulxdevi8/BGTXu72pfOO5vXHqcAfClfIWDSlOljROjCSOCcKvilgmHz1jDWbvFCZHjMQ5w==} - engines: {node: '>=0.10.0'} + eslint-formatter-friendly@7.0.0: dependencies: '@babel/code-frame': 7.0.0 chalk: 2.4.2 extend: 3.0.2 strip-ansi: 5.2.0 text-table: 0.2.0 - dev: true - /eslint-nibble@8.1.0(eslint@8.57.0): - resolution: {integrity: sha512-x9H/1oeuKdC0HsaWeBarOryqNLC+7QZfAZIAP0HnGcmiiPktFIQq/D0e+iiCSyqYLSaui3UwvH56sXMrf5oQhw==} - engines: {node: '>=12.0.0'} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' + eslint-nibble@8.1.0(eslint@9.23.0): dependencies: '@ianvs/eslint-stats': 2.0.0 chalk: 4.1.2 - eslint: 8.57.0 - eslint-filtered-fix: 0.3.0(eslint@8.57.0) + eslint: 9.23.0 + eslint-filtered-fix: 0.3.0(eslint@9.23.0) eslint-formatter-friendly: 7.0.0 eslint-summary: 1.0.0 inquirer: 8.2.6 - optionator: 0.9.3 - dev: true + optionator: 0.9.4 - /eslint-plugin-es-x@7.5.0(eslint@8.57.0): - resolution: {integrity: sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '>=8' + eslint-plugin-es-x@7.8.0(eslint@9.23.0): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.10.0 - eslint: 8.57.0 - eslint-compat-utils: 0.1.2(eslint@8.57.0) - dev: true + '@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0) + '@eslint-community/regexpp': 4.12.1 + eslint: 9.23.0 + eslint-compat-utils: 0.5.1(eslint@9.23.0) - /eslint-plugin-n@17.4.0(eslint@8.57.0): - resolution: {integrity: sha512-RtgGgNpYxECwE9dFr+D66RtbN0B8r/fY6ZF8EVsmK2YnZxE8/n9LNQhgnkL9z37UFZjYVmvMuC32qu7fQBsLVQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: '>=8.23.0' + eslint-plugin-n@17.17.0(eslint@9.23.0): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - enhanced-resolve: 5.16.0 - eslint: 8.57.0 - eslint-plugin-es-x: 7.5.0(eslint@8.57.0) - get-tsconfig: 4.7.2 - globals: 15.0.0 - ignore: 5.3.1 - minimatch: 9.0.4 - semver: 7.6.0 - dev: true + '@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0) + enhanced-resolve: 5.18.1 + eslint: 9.23.0 + eslint-plugin-es-x: 7.8.0(eslint@9.23.0) + get-tsconfig: 4.10.0 + globals: 15.15.0 + ignore: 5.3.2 + minimatch: 9.0.5 + semver: 7.7.1 - /eslint-plugin-prettier@5.1.3(@types/eslint@8.56.10)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5): - resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '*' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true + eslint-plugin-prettier@5.2.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.1(eslint@9.23.0))(eslint@9.23.0)(prettier@3.5.3): dependencies: - '@types/eslint': 8.56.10 - eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) - prettier: 3.2.5 + eslint: 9.23.0 + prettier: 3.5.3 prettier-linter-helpers: 1.0.0 - synckit: 0.8.8 - dev: true + synckit: 0.10.3 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 10.1.1(eslint@9.23.0) - /eslint-plugin-unicorn@52.0.0(eslint@8.57.0): - resolution: {integrity: sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==} - engines: {node: '>=16'} - peerDependencies: - eslint: '>=8.56.0' + eslint-plugin-unicorn@58.0.0(eslint@9.23.0): dependencies: - '@babel/helper-validator-identifier': 7.22.20 - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint/eslintrc': 2.1.4 - ci-info: 4.0.0 + '@babel/helper-validator-identifier': 7.25.9 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0) + '@eslint/plugin-kit': 0.2.7 + ci-info: 4.2.0 clean-regexp: 1.0.0 - core-js-compat: 3.36.1 - eslint: 8.57.0 - esquery: 1.5.0 - indent-string: 4.0.0 - is-builtin-module: 3.2.1 - jsesc: 3.0.2 + core-js-compat: 3.41.0 + eslint: 9.23.0 + esquery: 1.6.0 + globals: 16.0.0 + indent-string: 5.0.0 + is-builtin-module: 5.0.0 + jsesc: 3.1.0 pluralize: 8.0.0 - read-pkg-up: 7.0.1 + read-package-up: 11.0.0 regexp-tree: 0.1.27 - regjsparser: 0.10.0 - semver: 7.6.0 - strip-indent: 3.0.0 - transitivePeerDependencies: - - supports-color - dev: true + regjsparser: 0.12.0 + semver: 7.7.1 + strip-indent: 4.0.0 - /eslint-plugin-yml@1.14.0(eslint@8.57.0): - resolution: {integrity: sha512-ESUpgYPOcAYQO9czugcX5OqRvn/ydDVwGCPXY4YjPqc09rHaUVUA6IE6HLQys4rXk/S+qx3EwTd1wHCwam/OWQ==} - engines: {node: ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '>=6.0.0' + eslint-plugin-yml@1.17.0(eslint@9.23.0): dependencies: - debug: 4.3.4 - eslint: 8.57.0 - eslint-compat-utils: 0.5.0(eslint@8.57.0) - lodash: 4.17.21 + debug: 4.4.0 + escape-string-regexp: 4.0.0 + eslint: 9.23.0 + eslint-compat-utils: 0.6.4(eslint@9.23.0) natural-compare: 1.4.0 - yaml-eslint-parser: 1.2.2 + yaml-eslint-parser: 1.3.0 transitivePeerDependencies: - supports-color - dev: true - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 - dev: true - /eslint-summary@1.0.0: - resolution: {integrity: sha512-cHr5WiNFhu2guLQykhQV8O7BQcnpFLR6GdLjbQfDDL0yGy9U7dXC6zMUtwoxYgJRC/Wk3yZMc+I6Q15Z7r4j9Q==} - engines: {node: '>=0.10.0'} + eslint-scope@8.3.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-summary@1.0.0: dependencies: chalk: 1.1.3 text-table: 0.2.0 - dev: true - /eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + eslint-visitor-keys@3.4.3: {} - /eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true + eslint-visitor-keys@4.2.0: {} + + eslint@8.57.1: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.10.0 + '@eslint-community/eslint-utils': 4.5.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 + '@ungap/structured-clone': 1.3.0 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 + cross-spawn: 7.0.6 + debug: 4.4.0 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - esquery: 1.5.0 + esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 @@ -4814,7 +9258,7 @@ packages: glob-parent: 6.0.2 globals: 13.24.0 graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 @@ -4824,151 +9268,129 @@ packages: lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.3 + optionator: 0.9.4 strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color - dev: true - /esniff@2.0.1: - resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} - engines: {node: '>=0.10'} + eslint@9.23.0: + dependencies: + '@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.2 + '@eslint/config-helpers': 0.2.0 + '@eslint/core': 0.12.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.23.0 + '@eslint/plugin-kit': 0.2.7 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.2 + '@types/estree': 1.0.7 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0 + escape-string-regexp: 4.0.0 + eslint-scope: 8.3.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + esniff@2.0.1: dependencies: - d: 1.0.1 + d: 1.0.2 es5-ext: 0.10.64 event-emitter: 0.3.5 - type: 2.7.2 - dev: false + type: 2.7.3 - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@10.3.0: + dependencies: + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + eslint-visitor-keys: 4.2.0 + + espree@9.6.1: dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 3.4.3 - dev: true - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: false + esprima@4.0.1: {} - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} + esquery@1.6.0: dependencies: estraverse: 5.3.0 - dev: true - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 - dev: true - /estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - dev: false + estraverse@4.3.0: {} - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} + estraverse@5.3.0: {} - /estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true + estree-walker@2.0.2: {} - /estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.5 - dev: true + '@types/estree': 1.0.7 - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + esutils@2.0.3: {} - /etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - dev: false + etag@1.8.1: {} - /event-emitter@0.3.5: - resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + event-emitter@0.3.5: dependencies: - d: 1.0.1 + d: 1.0.2 es5-ext: 0.10.64 - dev: false - /event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - dev: false - - /eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - dev: true - - /events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - dev: false - - /execa@0.8.0: - resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==} - engines: {node: '>=4'} - dependencies: - cross-spawn: 5.1.0 - get-stream: 3.0.0 - is-stream: 1.1.0 - npm-run-path: 2.0.2 - p-finally: 1.0.0 - signal-exit: 3.0.7 - strip-eof: 1.0.0 - dev: false + eventemitter3@5.0.1: {} - /execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} + execa@8.0.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 8.0.1 human-signals: 5.0.0 is-stream: 3.0.0 merge-stream: 2.0.0 - npm-run-path: 5.2.0 + npm-run-path: 5.3.0 onetime: 6.0.0 signal-exit: 4.1.0 strip-final-newline: 3.0.0 - dev: true - /ext@1.7.0: - resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + expect-type@1.2.0: {} + + ext@1.7.0: dependencies: - type: 2.7.2 - dev: false + type: 2.7.3 - /extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extend@3.0.2: {} - /external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} + external-editor@3.1.0: dependencies: chardet: 0.7.0 iconv-lite: 0.4.24 tmp: 0.0.33 - dev: true - /extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true + extract-zip@2.0.1: dependencies: debug: 4.3.4 get-stream: 5.2.0 @@ -4977,404 +9399,234 @@ packages: '@types/yauzl': 2.10.3 transitivePeerDependencies: - supports-color - dev: false - /extsprintf@1.3.0: - resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} - engines: {'0': node >=0.6.0} - dev: false + extsprintf@1.3.0: {} - /fanfou-sdk@5.0.0: - resolution: {integrity: sha512-i/I3Py9A/JloJ6qU5HUvzo+iywiloGWOJ8MDzIjtNIr6rGZB5SBAzBDh29h7ybywRJZhdO2C+ZslqCk66Rl6ig==} - engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + fanfou-sdk@5.0.0: dependencies: camelcase-keys: 7.0.2 decamelize-keys: 1.1.1 - form-data: 4.0.0 + form-data: 4.0.2 got: 12.6.1 he: 1.2.0 hmacsha1: 1.0.0 oauth-1.0a: 2.2.6 query-string: 7.1.3 - dev: false - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-deep-equal@3.1.3: {} - /fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - dev: true + fast-diff@1.3.0: {} - /fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - dev: false + fast-fifo@1.3.2: {} - /fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.5 - dev: true + micromatch: 4.0.8 - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stable-stringify@2.1.0: {} - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-levenshtein@2.0.6: {} - /fast-redact@3.3.0: - resolution: {integrity: sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==} - engines: {node: '>=6'} - dev: false + fast-redact@3.5.0: {} - /fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - dev: true + fast-safe-stringify@2.1.1: {} - /fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fastq@1.19.1: dependencies: - reusify: 1.0.4 - dev: true + reusify: 1.1.0 - /fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fd-slicer@1.1.0: dependencies: pend: 1.2.0 - dev: false - /fecha@4.2.3: - resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - dev: false + fecha@4.2.3: {} - /figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 - dev: true - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 - dev: true - /file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - dev: true + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - dev: true - /filter-obj@1.1.0: - resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} - engines: {node: '>=0.10.0'} - dev: false + filter-obj@1.1.0: {} - /filter-obj@5.1.0: - resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} - engines: {node: '>=14.16'} - dev: false + filter-obj@5.1.0: {} - /find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - dev: true + find-up-simple@1.0.1: {} - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + find-up@5.0.0: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - dev: true - /flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@3.2.0: dependencies: - flatted: 3.2.9 + flatted: 3.3.3 keyv: 4.5.4 rimraf: 3.0.2 - dev: true - /flatted@3.2.9: - resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} - dev: true + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 - /fn.name@1.1.0: - resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - dev: false + flatted@3.3.3: {} - /for-in@0.1.8: - resolution: {integrity: sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==} - engines: {node: '>=0.10.0'} - dev: false + fn.name@1.1.0: {} - /for-in@1.0.2: - resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} - engines: {node: '>=0.10.0'} - dev: false + for-in@0.1.8: {} - /for-own@0.1.5: - resolution: {integrity: sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==} - engines: {node: '>=0.10.0'} + for-in@1.0.2: {} + + for-own@0.1.5: dependencies: for-in: 1.0.2 - dev: false - /foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} + foreground-child@3.3.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 - dev: true - /forever-agent@0.6.1: - resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - dev: false + forever-agent@0.6.1: {} - /form-data-encoder@2.1.4: - resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} - engines: {node: '>= 14.17'} - dev: false + form-data-encoder@2.1.4: {} - /form-data-encoder@4.0.2: - resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==} - engines: {node: '>= 18'} - dev: true + form-data-encoder@4.0.2: {} - /form-data@2.3.3: - resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} - engines: {node: '>= 0.12'} + form-data@2.3.3: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: false - /form-data@2.5.1: - resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} - engines: {node: '>= 0.12'} + form-data@2.5.3: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 + es-set-tostringtag: '@nolyfill/es-set-tostringtag@1.0.44' mime-types: 2.1.35 - dev: false + safe-buffer: '@nolyfill/safe-buffer@1.0.44' - /form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} + form-data@4.0.2: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 + es-set-tostringtag: '@nolyfill/es-set-tostringtag@1.0.44' mime-types: 2.1.35 - /formidable@3.5.1: - resolution: {integrity: sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==} + formidable@3.5.2: dependencies: dezalgo: 1.0.4 - hexoid: 1.0.0 + hexoid: 2.0.0 once: 1.4.0 - dev: true - /fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} + forwarded-parse@2.1.2: {} + + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.1 - dev: false - /fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} - engines: {node: '>=14.14'} + fs-extra@11.3.0: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.1 - /fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - dev: true - - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fs.realpath@1.0.0: {} - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true + fsevents@2.3.3: optional: true - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - /gauge@3.0.2: - resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} - engines: {node: '>=10'} - dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - dev: true - - /gaxios@6.2.0: - resolution: {integrity: sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ==} - engines: {node: '>=14'} + gaxios@6.7.1: dependencies: extend: 3.0.2 - https-proxy-agent: 7.0.4 + https-proxy-agent: 7.0.6 is-stream: 2.0.1 node-fetch: 2.7.0 + uuid: 9.0.1 transitivePeerDependencies: - encoding - supports-color - dev: false - /gcp-metadata@6.1.0: - resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==} - engines: {node: '>=14'} + gcp-metadata@6.1.1: dependencies: - gaxios: 6.2.0 + gaxios: 6.7.1 + google-logging-utils: 0.0.2 json-bigint: 1.0.0 transitivePeerDependencies: - encoding - supports-color - dev: false - - /gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - dev: true - - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - /get-east-asian-width@1.2.0: - resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} - engines: {node: '>=18'} - dev: true - /get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - dev: true + gensync@1.0.0-beta.2: {} - /get-intrinsic@1.2.4: - resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - has-proto: 1.0.1 - has-symbols: 1.0.3 - hasown: 2.0.1 + get-caller-file@2.0.5: {} - /get-stream@3.0.0: - resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} - engines: {node: '>=4'} - dev: false + get-east-asian-width@1.3.0: {} - /get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} + get-stream@5.2.0: dependencies: - pump: 3.0.0 - dev: false + pump: 3.0.2 - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} + get-stream@6.0.1: {} - /get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - dev: true + get-stream@8.0.1: {} - /get-tsconfig@4.7.2: - resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + get-stream@9.0.1: dependencies: - resolve-pkg-maps: 1.0.0 - dev: true + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 - /get-tsconfig@4.7.3: - resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} + get-tsconfig@4.10.0: dependencies: resolve-pkg-maps: 1.0.0 - dev: false - /get-uri@6.0.3: - resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} - engines: {node: '>= 14'} + get-uri@6.0.4: dependencies: - basic-ftp: 5.0.4 + basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.3.4 - fs-extra: 11.2.0 + debug: 4.4.0 transitivePeerDependencies: - supports-color - dev: false - /getpass@0.1.7: - resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + getpass@0.1.7: dependencies: assert-plus: 1.0.0 - dev: false - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - dev: true - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 - dev: true - /glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true + glob@10.4.5: dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.4 - minipass: 5.0.0 - path-scurry: 1.10.1 - dev: true + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + glob@7.2.3: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -5383,88 +9635,55 @@ packages: once: 1.4.0 path-is-absolute: 1.0.1 - /globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - dev: true + globals@11.12.0: {} - /globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + globals@13.24.0: dependencies: type-fest: 0.20.2 - dev: true - /globals@15.0.0: - resolution: {integrity: sha512-m/C/yR4mjO6pXDTm9/R/SpYTAIyaUB4EOzcaaMEl7mds7Mshct9GfejiJNQGjHHbdMPey13Kpu4TMbYi9ex1pw==} - engines: {node: '>=18'} - dev: true + globals@14.0.0: {} - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.1 - merge2: 1.4.1 - slash: 3.0.0 - dev: true + globals@15.15.0: {} - /globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - dev: true + globals@16.0.0: {} - /google-auth-library@9.6.3: - resolution: {integrity: sha512-4CacM29MLC2eT9Cey5GDVK4Q8t+MMp8+OEdOaqD9MG6b0dOyLORaaeJMPQ7EESVgm/+z5EKYyFLxgzBJlJgyHQ==} - engines: {node: '>=14'} + globrex@0.1.2: {} + + google-auth-library@9.15.1: dependencies: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 - gaxios: 6.2.0 - gcp-metadata: 6.1.0 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 gtoken: 7.1.0 jws: 4.0.0 transitivePeerDependencies: - encoding - supports-color - dev: false - /googleapis-common@7.0.1: - resolution: {integrity: sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w==} - engines: {node: '>=14.0.0'} + google-logging-utils@0.0.2: {} + + googleapis-common@7.2.0: dependencies: extend: 3.0.2 - gaxios: 6.2.0 - google-auth-library: 9.6.3 - qs: 6.11.2 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + qs: 6.14.0 url-template: 2.0.8 uuid: 9.0.1 transitivePeerDependencies: - encoding - supports-color - dev: false - /googleapis@136.0.0: - resolution: {integrity: sha512-W2Z1NtFS8ie5SzN74+lnTzLS9d0tS01dybibqXjWKSfdvTkQr9DYsKYNMRpzMAcBva1ZWCAgTiX4fgGz2Cwbaw==} - engines: {node: '>=14.0.0'} + googleapis@148.0.0: dependencies: - google-auth-library: 9.6.3 - googleapis-common: 7.0.1 + google-auth-library: 9.15.1 + googleapis-common: 7.2.0 transitivePeerDependencies: - encoding - supports-color - dev: false - - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} - dependencies: - get-intrinsic: 1.2.4 - /got@12.6.1: - resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} - engines: {node: '>=14.16'} + got@12.6.1: dependencies: '@sindresorhus/is': 5.6.0 '@szmarczak/http-timer': 5.0.1 @@ -5477,174 +9696,93 @@ packages: lowercase-keys: 3.0.0 p-cancelable: 3.0.0 responselike: 3.0.0 - dev: false - /got@14.2.1: - resolution: {integrity: sha512-KOaPMremmsvx6l9BLC04LYE6ZFW4x7e4HkTe3LwBmtuYYQwpeS4XKqzhubTIkaQ1Nr+eXxeori0zuwupXMovBQ==} - engines: {node: '>=20'} + got@14.4.7: dependencies: - '@sindresorhus/is': 6.2.0 + '@sindresorhus/is': 7.0.1 '@szmarczak/http-timer': 5.0.1 cacheable-lookup: 7.0.0 - cacheable-request: 10.2.14 + cacheable-request: 12.0.1 decompress-response: 6.0.0 form-data-encoder: 4.0.2 - get-stream: 8.0.1 http2-wrapper: 2.2.1 lowercase-keys: 3.0.0 p-cancelable: 4.0.1 responselike: 3.0.0 - dev: true + type-fest: 4.38.0 - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graceful-fs@4.2.11: {} - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true + graphemer@1.4.0: {} - /graphql@16.8.1: - resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - dev: true + graphql@16.10.0: {} - /gtoken@7.1.0: - resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} - engines: {node: '>=14.0.0'} + gtoken@7.1.0: dependencies: - gaxios: 6.2.0 + gaxios: 6.7.1 jws: 4.0.0 transitivePeerDependencies: - encoding - supports-color - dev: false - /har-schema@2.0.0: - resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} - engines: {node: '>=4'} - dev: false + har-schema@2.0.0: {} - /har-validator@5.1.5: - resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} - engines: {node: '>=6'} - deprecated: this library is no longer supported + har-validator@5.1.5: dependencies: ajv: 6.12.6 har-schema: 2.0.0 - dev: false - /has-ansi@2.0.0: - resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} - engines: {node: '>=0.10.0'} + has-ansi@2.0.0: dependencies: ansi-regex: 2.1.1 - dev: true - - /has-flag@2.0.0: - resolution: {integrity: sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==} - engines: {node: '>=0.10.0'} - dev: false - - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true - - /has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - dependencies: - es-define-property: 1.0.0 - - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} + has-flag@3.0.0: {} - /has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - dev: true + has-flag@4.0.0: {} - /hasown@2.0.1: - resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} - engines: {node: '>= 0.4'} - dependencies: - function-bind: 1.1.2 + he@1.2.0: {} - /he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - dev: false + headers-polyfill@4.0.3: {} - /headers-polyfill@4.0.3: - resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} - dev: true + heap@0.2.7: {} - /heap@0.2.7: - resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} - dev: false + hexoid@2.0.0: {} - /hexoid@1.0.0: - resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} - engines: {node: '>=8'} - dev: true + hmacsha1@1.0.0: {} - /hmacsha1@1.0.0: - resolution: {integrity: sha512-4FP6J0oI8jqb6gLLl9tSwVdosWJ/AKSGJ+HwYf6Ixe4MUcEkst4uWzpVQrNOCin0fzTRQbXV8ePheU8WiiDYBw==} - dev: false + hono@4.7.5: {} - /hono@4.2.9: - resolution: {integrity: sha512-59FAv52UxDWUt/NlC0NzrRCjeVCThUnVlqlrKYm+k80XujBu6uJwBIa5gACKKZWobjA0MJ6Vds0I3URKf383Cw==} - engines: {node: '>=16.0.0'} - dev: false + hookable@5.5.3: {} - /hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - dev: true + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 - /html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 - /html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - dev: true + html-escaper@2.0.2: {} - /html-minifier@3.5.21: - resolution: {integrity: sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==} - engines: {node: '>=4'} - hasBin: true + html-minifier@4.0.0: dependencies: camel-case: 3.0.0 clean-css: 4.2.4 - commander: 2.17.1 + commander: 2.20.3 he: 1.2.0 param-case: 2.1.1 relateurl: 0.2.7 - uglify-js: 3.4.10 - dev: false + uglify-js: 3.19.3 - /html-to-text@9.0.5: - resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} - engines: {node: '>=14'} + html-to-text@9.0.5: dependencies: '@selderee/plugin-htmlparser2': 0.11.0 deepmerge: 4.3.1 dom-serializer: 2.0.0 htmlparser2: 8.0.2 selderee: 0.11.0 - dev: false - /htmlparser2@3.10.1: - resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} + htmlparser2@3.10.1: dependencies: domelementtype: 1.3.1 domhandler: 2.4.2 @@ -5652,176 +9790,130 @@ packages: entities: 1.1.2 inherits: 2.0.4 readable-stream: 3.6.2 - dev: false - /htmlparser2@6.1.0: - resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + htmlparser2@6.1.0: dependencies: domelementtype: 2.3.0 domhandler: 4.3.1 domutils: 2.8.0 entities: 2.2.0 - dev: false - /htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 entities: 4.5.0 - /http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 - /http-proxy-agent@7.0.1: - resolution: {integrity: sha512-My1KCEPs6A0hb4qCVzYp8iEvA8j8YqcvXLZZH8C9OFuTYpYjHE7N2dtG3mRl1HMD4+VGXpF3XcDVcxGBT7yDZQ==} - engines: {node: '>= 14'} + http-cache-semantics@4.1.1: {} + + http-cookie-agent@6.0.8(tough-cookie@5.1.2)(undici@6.21.2): dependencies: - agent-base: 7.1.1 - debug: 4.3.4 + agent-base: 7.1.3 + tough-cookie: 5.1.2 + optionalDependencies: + undici: 6.21.2 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0 transitivePeerDependencies: - supports-color - /http-signature@1.2.0: - resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} - engines: {node: '>=0.8', npm: '>=1.3.7'} + http-signature@1.2.0: dependencies: assert-plus: 1.0.0 jsprim: 1.4.2 sshpk: 1.18.0 - dev: false - /http-signature@1.3.6: - resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==} - engines: {node: '>=0.10'} + http-signature@1.4.0: dependencies: assert-plus: 1.0.0 jsprim: 2.0.2 sshpk: 1.18.0 - dev: false - /http2-wrapper@2.2.1: - resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} - engines: {node: '>=10.19.0'} + http2-wrapper@2.2.1: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - /https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - dependencies: - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true - - /https-proxy-agent@7.0.4: - resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} - engines: {node: '>= 14'} + https-proxy-agent@7.0.6: dependencies: - agent-base: 7.1.0 - debug: 4.3.4 + agent-base: 7.1.3 + debug: 4.4.0 transitivePeerDependencies: - supports-color - /human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - dev: true + human-signals@5.0.0: {} - /husky@9.0.11: - resolution: {integrity: sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==} - engines: {node: '>=18'} - hasBin: true - dev: true + husky@9.1.7: {} - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} + iconv-lite@0.4.24: dependencies: - safer-buffer: 2.1.2 - dev: true + safer-buffer: '@nolyfill/safer-buffer@1.0.44' - /iconv-lite@0.5.0: - resolution: {integrity: sha512-NnEhI9hIEKHOzJ4f697DMz9IQEXr/MMJ5w64vN2/4Ai+wRnvV7SBrL0KLoRlwaKVghOc7LQ5YkPLuX146b6Ydw==} - engines: {node: '>=0.10.0'} + iconv-lite@0.5.0: dependencies: - safer-buffer: 2.1.2 - dev: false + safer-buffer: '@nolyfill/safer-buffer@1.0.44' - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: dependencies: - safer-buffer: 2.1.2 + safer-buffer: '@nolyfill/safer-buffer@1.0.44' - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ieee754@1.2.1: {} - /ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} - dev: true + ignore@5.3.2: {} - /image-size@0.7.5: - resolution: {integrity: sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==} - engines: {node: '>=6.9.0'} - hasBin: true - dev: false + image-size@0.7.5: {} - /imapflow@1.0.160: - resolution: {integrity: sha512-gjoPn863lt2RYGRKV9i+CdcPycs04ufBwtDPAri7+pnI1Zr/JPcUcL1aOB6Z/rNCvpRGJ1IBaad+bLo5wbm7cg==} + imapflow@1.0.184: dependencies: - encoding-japanese: 2.1.0 + encoding-japanese: 2.2.0 iconv-lite: 0.6.3 libbase64: 1.3.0 - libmime: 5.3.5 - libqp: 2.1.0 - mailsplit: 5.4.0 - nodemailer: 6.9.13 - pino: 8.20.0 - socks: 2.8.3 - dev: false - - /immediate@3.0.6: - resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - dev: false - - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + libmime: 5.3.6 + libqp: 2.1.1 + mailsplit: 5.4.3 + nodemailer: 6.10.0 + pino: 9.6.0 + socks: 2.8.4 + + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} + import-in-the-middle@1.13.1: + dependencies: + acorn: 8.14.1 + acorn-import-attributes: 1.9.5(acorn@8.14.1) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.3 - /indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - dev: true + imurmurhash@0.1.4: {} - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + indent-string@5.0.0: {} + + index-to-position@1.0.0: {} + + inflight@1.0.6: dependencies: once: 1.4.0 wrappy: 1.0.2 - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inherits@2.0.4: {} - /ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: true + ini@1.3.8: {} - /inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} - engines: {node: '>=12.0.0'} + inquirer@8.2.6: dependencies: ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -5833,29 +9925,21 @@ packages: mute-stream: 0.0.8 ora: 5.4.1 run-async: 2.4.1 - rxjs: 7.8.1 + rxjs: 7.8.2 string-width: 4.2.3 strip-ansi: 6.0.1 through: 2.3.8 wrap-ansi: 6.2.0 - dev: true - /instagram-private-api@1.46.1: - resolution: {integrity: sha512-fq0q6UfhpikKZ5Kw8HNwS6YpsNghE9I/uc8AM9Do9nsQ+3H1u0jLz+0t/FcGkGTjZz5VGvU8s2VbWj9wxchwYg==} - engines: {node: '>=8.0.0'} - peerDependencies: - re2: ^1.17.2 - peerDependenciesMeta: - re2: - optional: true + instagram-private-api@1.46.1: dependencies: - '@lifeomic/attempt': 3.0.3 + '@lifeomic/attempt': 3.1.0 '@types/chance': 1.1.6 '@types/request-promise': 4.1.51 bluebird: 3.7.2 - chance: 1.1.11 + chance: 1.1.12 class-transformer: 0.3.1 - debug: 4.3.4 + debug: 4.4.0 image-size: 0.7.5 json-bigint: 1.0.0 lodash: 4.17.21 @@ -5872,15 +9956,12 @@ packages: utility-types: 3.11.0 transitivePeerDependencies: - supports-color - dev: false - /ioredis@5.4.1: - resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} - engines: {node: '>=12.22.0'} + ioredis@5.6.0: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.4 + debug: 4.4.0 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -5889,758 +9970,439 @@ packages: standard-as-callback: 2.1.0 transitivePeerDependencies: - supports-color - dev: false - /ip-address@9.0.5: - resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} - engines: {node: '>= 12'} + ip-address@9.0.5: dependencies: jsbn: 1.1.0 sprintf-js: 1.1.3 - dev: false - /ip-regex@4.3.0: - resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} - engines: {node: '>=8'} - dev: false + ip-regex@4.3.0: {} - /ip-regex@5.0.0: - resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false + ip-regex@5.0.0: {} - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.2.1: {} - /is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - dev: false + is-arrayish@0.3.2: {} - /is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - dev: false + is-buffer@1.1.6: {} - /is-builtin-module@3.2.1: - resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} - engines: {node: '>=6'} + is-builtin-module@5.0.0: dependencies: - builtin-modules: 3.3.0 - dev: true + builtin-modules: 5.0.0 - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - dependencies: - hasown: 2.0.1 - dev: true + is-docker@2.2.1: {} - /is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - dev: false + is-docker@3.0.0: {} - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} + is-fullwidth-code-point@3.0.0: {} - /is-fullwidth-code-point@4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - dev: true + is-fullwidth-code-point@4.0.0: {} - /is-fullwidth-code-point@5.0.0: - resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} - engines: {node: '>=18'} + is-fullwidth-code-point@5.0.0: dependencies: - get-east-asian-width: 1.2.0 - dev: true + get-east-asian-width: 1.3.0 - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - dev: true - /is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - dev: true + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 - /is-keyword-js@1.0.3: - resolution: {integrity: sha512-EW8wNCNvomPa/jsH1g0DmLfPakkRCRTcTML1v1fZMLiVCvQ/1YB+tKsRzShBiWQhqrYCi5a+WsepA4Z8TA9iaA==} - engines: {node: '>=0.10.0'} - dev: false + is-interactive@1.0.0: {} - /is-node-process@1.2.0: - resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} - dev: true + is-keyword-js@1.0.3: {} - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true + is-node-process@1.2.0: {} - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true + is-number@7.0.0: {} - /is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - dev: true + is-path-inside@3.0.3: {} - /is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} + is-plain-obj@4.1.0: {} + + is-plain-object@2.0.4: dependencies: isobject: 3.0.1 - dev: false - /is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - dev: false + is-plain-object@5.0.0: {} - /is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-potential-custom-element-name@1.0.1: {} - /is-stream@1.1.0: - resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} - engines: {node: '>=0.10.0'} - dev: false + is-stream@2.0.1: {} - /is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - dev: false + is-stream@3.0.0: {} - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + is-stream@4.0.1: {} - /is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - dev: false + is-typedarray@1.0.0: {} - /is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - dev: true + is-unicode-supported@0.1.0: {} - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 - /isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} - dev: false + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 - /isstream@0.1.2: - resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} - dev: false + is64bit@2.0.0: + dependencies: + system-architecture: 0.1.0 - /istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - dev: true + isexe@2.0.0: {} - /istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} + isobject@3.0.1: {} + + isstream@0.1.2: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 - dev: true - /istanbul-lib-source-maps@5.0.4: - resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} - engines: {node: '>=10'} + istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.4 + debug: 4.4.0 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color - dev: true - /istanbul-reports@3.1.6: - resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} - engines: {node: '>=8'} + istanbul-reports@3.1.7: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - dev: true - /jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 - dev: true - /js-beautify@1.15.1: - resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} - engines: {node: '>=14'} - hasBin: true + js-beautify@1.15.4: dependencies: config-chain: 1.1.13 editorconfig: 1.0.4 - glob: 10.3.10 + glob: 10.4.5 js-cookie: 3.0.5 - nopt: 7.2.0 - dev: true - - /js-cookie@3.0.5: - resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} - engines: {node: '>=14'} - dev: true + nopt: 7.2.1 - /js-tokens@3.0.2: - resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==} - dev: false + js-cookie@3.0.5: {} - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@3.0.2: {} - /js-tokens@8.0.3: - resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} - dev: true + js-tokens@4.0.0: {} - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true + js-yaml@4.1.0: dependencies: argparse: 2.0.1 - /jsbn@0.1.1: - resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} - dev: false + jsbn@0.1.1: {} - /jsbn@1.1.0: - resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - dev: false + jsbn@1.1.0: {} - /jschardet@3.1.2: - resolution: {integrity: sha512-mw3CBZGzW8nUBPYhFU2ztZ/kJ6NClQUQVpyzvFMfznZsoC///ZQ30J2RCUanNsr5yF22LqhgYr/lj807/ZleWA==} - engines: {node: '>=0.1.90'} - dev: true + jschardet@3.1.4: {} - /jsdom@24.0.0: - resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==} - engines: {node: '>=18'} - peerDependencies: - canvas: ^2.11.2 - peerDependenciesMeta: - canvas: - optional: true + jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: - cssstyle: 4.0.1 + cssstyle: 4.3.0 data-urls: 5.0.0 - decimal.js: 10.4.3 - form-data: 4.0.0 + decimal.js: 10.5.0 + form-data: 4.0.2 html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.1 - https-proxy-agent: 7.0.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.7 - parse5: 7.1.2 - rrweb-cssom: 0.6.0 + nwsapi: 2.2.19 + parse5: 7.2.1 + rrweb-cssom: 0.8.0 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 4.1.4 + tough-cookie: 5.1.2 w3c-xmlserializer: 5.0.0 webidl-conversions: 7.0.0 whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 - whatwg-url: 14.0.0 - ws: 8.16.0 + whatwg-url: 14.2.0 + ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - /jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - dev: true + jsep@1.4.0: {} - /jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - dev: true + jsesc@3.0.2: {} - /jsesc@3.0.2: - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} - engines: {node: '>=6'} - hasBin: true - dev: true + jsesc@3.1.0: {} - /json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-bigint@1.0.0: dependencies: bignumber.js: 9.1.2 - dev: false - - /json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-buffer@3.0.1: {} - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-parse-even-better-errors@2.3.1: {} - /json-schema@0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - dev: false + json-schema-traverse@0.4.1: {} - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true + json-schema@0.4.0: {} - /json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - dev: false + json-stable-stringify-without-jsonify@1.0.1: {} - /json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - dev: true + json-stringify-safe@5.0.1: {} - /jsonc-parser@3.2.1: - resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} - dev: true + json5@2.2.3: {} - /jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.1.0: dependencies: universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 - /jsprim@1.4.2: - resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} - engines: {node: '>=0.6.0'} + jsonpath-plus@10.3.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + + jsprim@1.4.2: dependencies: assert-plus: 1.0.0 extsprintf: 1.3.0 json-schema: 0.4.0 verror: 1.10.0 - dev: false - /jsprim@2.0.2: - resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} - engines: {'0': node >=0.6.0} + jsprim@2.0.2: dependencies: assert-plus: 1.0.0 extsprintf: 1.3.0 json-schema: 0.4.0 verror: 1.10.0 - dev: false - /jsrsasign@10.9.0: - resolution: {integrity: sha512-QWLUikj1SBJGuyGK8tjKSx3K7Y69KYJnrs/pQ1KZ6wvZIkHkWjZ1PJDpuvc1/28c1uP0KW9qn1eI1LzHQqDOwQ==} - dev: false + jsrsasign@10.9.0: {} - /jwa@2.0.0: - resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + jwa@2.0.0: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - dev: false + safe-buffer: '@nolyfill/safe-buffer@1.0.44' - /jws@4.0.0: - resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + jws@4.0.0: dependencies: jwa: 2.0.0 - safe-buffer: 5.2.1 - dev: false + safe-buffer: '@nolyfill/safe-buffer@1.0.44' - /keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 - /kind-of@2.0.1: - resolution: {integrity: sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==} - engines: {node: '>=0.10.0'} + kind-of@2.0.1: dependencies: is-buffer: 1.1.6 - dev: false - /kind-of@3.2.2: - resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} - engines: {node: '>=0.10.0'} + kind-of@3.2.2: dependencies: is-buffer: 1.1.6 - dev: false - /kuler@2.0.0: - resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - dev: false + kuler@2.0.0: {} - /lazy-cache@0.2.7: - resolution: {integrity: sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==} - engines: {node: '>=0.10.0'} - dev: false + lazy-cache@0.2.7: {} - /lazy-cache@1.0.4: - resolution: {integrity: sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==} - engines: {node: '>=0.10.0'} - dev: false + lazy-cache@1.0.4: {} - /leac@0.6.0: - resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - dev: false + leac@0.6.0: {} - /levn@0.3.0: - resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} - engines: {node: '>= 0.8.0'} + levn@0.3.0: dependencies: prelude-ls: 1.1.2 type-check: 0.3.2 - dev: false - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true - /libbase64@1.2.1: - resolution: {integrity: sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==} - dev: false + libbase64@1.3.0: {} - /libbase64@1.3.0: - resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} - dev: false - - /libmime@5.2.0: - resolution: {integrity: sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==} - dependencies: - encoding-japanese: 2.0.0 - iconv-lite: 0.6.3 - libbase64: 1.2.1 - libqp: 2.0.1 - dev: false - - /libmime@5.3.5: - resolution: {integrity: sha512-nSlR1yRZ43L3cZCiWEw7ali3jY29Hz9CQQ96Oy+sSspYnIP5N54ucOPHqooBsXzwrX1pwn13VUE05q4WmzfaLg==} + libmime@5.3.6: dependencies: - encoding-japanese: 2.1.0 + encoding-japanese: 2.2.0 iconv-lite: 0.6.3 libbase64: 1.3.0 - libqp: 2.1.0 - dev: false - - /libqp@2.0.1: - resolution: {integrity: sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==} - dev: false + libqp: 2.1.1 - /libqp@2.1.0: - resolution: {integrity: sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A==} - dev: false - - /lie@3.1.1: - resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} - dependencies: - immediate: 3.0.6 - dev: false + libqp@2.1.1: {} - /lilconfig@3.0.0: - resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} - engines: {node: '>=14'} - dev: true + lilconfig@3.1.3: {} - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lines-and-columns@1.2.4: {} - /linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 - dev: false - /lint-staged@15.2.2: - resolution: {integrity: sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==} - engines: {node: '>=18.12.0'} - hasBin: true + lint-staged@15.5.0: dependencies: - chalk: 5.3.0 - commander: 11.1.0 - debug: 4.3.4 + chalk: 5.4.1 + commander: 13.1.0 + debug: 4.4.0 execa: 8.0.1 - lilconfig: 3.0.0 - listr2: 8.0.1 - micromatch: 4.0.5 + lilconfig: 3.1.3 + listr2: 8.2.5 + micromatch: 4.0.8 pidtree: 0.6.0 string-argv: 0.3.2 - yaml: 2.3.4 + yaml: 2.7.0 transitivePeerDependencies: - supports-color - dev: true - /listr2@8.0.1: - resolution: {integrity: sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==} - engines: {node: '>=18.0.0'} + listr2@8.2.5: dependencies: cli-truncate: 4.0.0 colorette: 2.0.20 eventemitter3: 5.0.1 - log-update: 6.0.0 - rfdc: 1.3.1 + log-update: 6.1.0 + rfdc: 1.4.1 wrap-ansi: 9.0.0 - dev: true - - /local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} - dependencies: - mlly: 1.6.1 - pkg-types: 1.0.3 - dev: true - - /localforage@1.10.0: - resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} - dependencies: - lie: 3.1.1 - dev: false - /locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - dependencies: - p-locate: 4.1.0 - dev: true - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 - dev: true - /lodash.assignin@4.2.0: - resolution: {integrity: sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==} - dev: false + lodash.assignin@4.2.0: {} - /lodash.bind@4.2.1: - resolution: {integrity: sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==} - dev: false + lodash.bind@4.2.1: {} - /lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - dev: true + lodash.debounce@4.0.8: {} - /lodash.defaults@4.2.0: - resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - dev: false + lodash.defaults@4.2.0: {} - /lodash.filter@4.6.0: - resolution: {integrity: sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==} - dev: false + lodash.filter@4.6.0: {} - /lodash.flatten@4.4.0: - resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} - dev: false + lodash.flatten@4.4.0: {} - /lodash.foreach@4.5.0: - resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==} - dev: false + lodash.foreach@4.5.0: {} - /lodash.isarguments@3.1.0: - resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - dev: false + lodash.isarguments@3.1.0: {} - /lodash.map@4.6.0: - resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} - dev: false + lodash.map@4.6.0: {} - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.merge@4.6.2: {} - /lodash.pick@4.4.0: - resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} - dev: false + lodash.pick@4.4.0: {} - /lodash.reduce@4.6.0: - resolution: {integrity: sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==} - dev: false + lodash.reduce@4.6.0: {} - /lodash.reject@4.6.0: - resolution: {integrity: sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==} - dev: false + lodash.reject@4.6.0: {} - /lodash.some@4.6.0: - resolution: {integrity: sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==} - dev: false + lodash.some@4.6.0: {} - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.21: {} - /log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - dev: true - /log-update@6.0.0: - resolution: {integrity: sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==} - engines: {node: '>=18'} + log-update@6.1.0: dependencies: - ansi-escapes: 6.2.0 - cli-cursor: 4.0.0 + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 slice-ansi: 7.1.0 strip-ansi: 7.1.0 wrap-ansi: 9.0.0 - dev: true - /logform@2.6.0: - resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==} - engines: {node: '>= 12.0.0'} + logform@2.7.0: dependencies: '@colors/colors': 1.6.0 '@types/triple-beam': 1.3.5 fecha: 4.2.3 ms: 2.1.3 - safe-stable-stringify: 2.4.3 + safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 - dev: false - /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - dependencies: - get-func-name: 2.0.2 - dev: true + long@5.3.1: {} - /lower-case@1.1.4: - resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} - dev: false + loupe@3.1.3: {} - /lowercase-keys@3.0.0: - resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lower-case@1.1.4: {} - /lru-cache@10.2.2: - resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} - engines: {node: 14 || >=16.14} + lowercase-keys@3.0.0: {} - /lru-cache@4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} - dependencies: - pseudomap: 1.0.2 - yallist: 2.1.2 - dev: false + lru-cache@10.4.3: {} - /lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@11.1.0: {} + + lru-cache@5.1.1: dependencies: yallist: 3.1.1 - dev: true - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 - /lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - dev: false + lru-cache@7.18.3: {} - /luxon@1.28.1: - resolution: {integrity: sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==} - dev: false + luxon@1.28.1: {} - /lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - dev: false + lz-string@1.5.0: {} - /magic-string@0.30.8: - resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} - engines: {node: '>=12'} + magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true + '@jridgewell/sourcemap-codec': 1.5.0 - /magicast@0.3.3: - resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==} + magicast@0.3.5: dependencies: - '@babel/parser': 7.24.4 - '@babel/types': 7.24.5 - source-map-js: 1.2.0 - dev: true + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + source-map-js: 1.2.1 - /mailparser@3.7.1: - resolution: {integrity: sha512-RCnBhy5q8XtB3mXzxcAfT1huNqN93HTYYyL6XawlIKycfxM/rXPg9tXoZ7D46+SgCS1zxKzw+BayDQSvncSTTw==} + mailparser@3.7.2: dependencies: - encoding-japanese: 2.1.0 + encoding-japanese: 2.2.0 he: 1.2.0 html-to-text: 9.0.5 iconv-lite: 0.6.3 - libmime: 5.3.5 + libmime: 5.3.6 linkify-it: 5.0.0 - mailsplit: 5.4.0 - nodemailer: 6.9.13 + mailsplit: 5.4.2 + nodemailer: 6.9.16 punycode.js: 2.3.1 - tlds: 1.252.0 - dev: false + tlds: 1.255.0 - /mailsplit@5.4.0: - resolution: {integrity: sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==} + mailsplit@5.4.2: dependencies: - libbase64: 1.2.1 - libmime: 5.2.0 - libqp: 2.0.1 - dev: false + libbase64: 1.3.0 + libmime: 5.3.6 + libqp: 2.1.1 - /make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} + mailsplit@5.4.3: dependencies: - semver: 6.3.1 - dev: true + libbase64: 1.3.0 + libmime: 5.3.6 + libqp: 2.1.1 - /make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} + make-dir@4.0.0: dependencies: - semver: 7.6.0 - dev: true + semver: 7.7.1 - /map-obj@1.0.1: - resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} - engines: {node: '>=0.10.0'} - dev: false + map-obj@1.0.1: {} - /map-obj@4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} - dev: false + map-obj@4.3.0: {} - /markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true + markdown-it@14.1.0: dependencies: argparse: 2.0.1 entities: 4.5.0 @@ -6648,639 +10410,412 @@ packages: mdurl: 2.0.0 punycode.js: 2.3.1 uc.micro: 2.1.0 - dev: false - /markdown-table@2.0.0: - resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + markdown-table@2.0.0: dependencies: repeat-string: 1.6.1 - dev: false - /mdast-util-from-markdown@2.0.0: - resolution: {integrity: sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==} + mdast-util-from-markdown@2.0.2: dependencies: - '@types/mdast': 4.0.3 - '@types/unist': 3.0.2 - decode-named-character-reference: 1.0.2 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.1.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.0 - micromark-util-decode-numeric-character-reference: 2.0.1 - micromark-util-decode-string: 2.0.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 unist-util-stringify-position: 4.0.0 transitivePeerDependencies: - supports-color - dev: true - /mdast-util-to-string@4.0.0: - resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdast-util-to-string@4.0.0: dependencies: - '@types/mdast': 4.0.3 - dev: true + '@types/mdast': 4.0.4 - /mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - dev: false + mdurl@2.0.0: {} - /merge-deep@3.0.3: - resolution: {integrity: sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==} - engines: {node: '>=0.10.0'} + merge-deep@3.0.3: dependencies: arr-union: 3.1.0 clone-deep: 0.2.4 kind-of: 3.2.2 - dev: false - /merge-source-map@1.1.0: - resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} + merge-source-map@1.1.0: dependencies: source-map: 0.6.1 - dev: false - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true + merge-stream@2.0.0: {} - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true + merge2@1.4.1: {} - /methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - dev: true + methods@1.1.2: {} - /micromark-core-commonmark@2.0.0: - resolution: {integrity: sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==} + micromark-core-commonmark@2.0.3: dependencies: - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.1.0 devlop: 1.1.0 - micromark-factory-destination: 2.0.0 - micromark-factory-label: 2.0.0 - micromark-factory-space: 2.0.0 - micromark-factory-title: 2.0.0 - micromark-factory-whitespace: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-classify-character: 2.0.0 - micromark-util-html-tag-name: 2.0.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-resolve-all: 2.0.0 - micromark-util-subtokenize: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true - - /micromark-factory-destination@2.0.0: - resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} - dependencies: - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true - - /micromark-factory-label@2.0.0: - resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: dependencies: devlop: 1.1.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-factory-space@2.0.0: - resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==} + micromark-factory-space@2.0.1: dependencies: - micromark-util-character: 2.1.0 - micromark-util-types: 2.0.0 - dev: true + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 - /micromark-factory-title@2.0.0: - resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==} + micromark-factory-title@2.0.1: dependencies: - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-factory-whitespace@2.0.0: - resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==} + micromark-factory-whitespace@2.0.1: dependencies: - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-util-character@2.1.0: - resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + micromark-util-character@2.1.1: dependencies: - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-util-chunked@2.0.0: - resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} + micromark-util-chunked@2.0.1: dependencies: - micromark-util-symbol: 2.0.0 - dev: true + micromark-util-symbol: 2.0.1 - /micromark-util-classify-character@2.0.0: - resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==} + micromark-util-classify-character@2.0.1: dependencies: - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-util-combine-extensions@2.0.0: - resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==} + micromark-util-combine-extensions@2.0.1: dependencies: - micromark-util-chunked: 2.0.0 - micromark-util-types: 2.0.0 - dev: true + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-util-decode-numeric-character-reference@2.0.1: - resolution: {integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==} + micromark-util-decode-numeric-character-reference@2.0.2: dependencies: - micromark-util-symbol: 2.0.0 - dev: true + micromark-util-symbol: 2.0.1 - /micromark-util-decode-string@2.0.0: - resolution: {integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==} + micromark-util-decode-string@2.0.1: dependencies: - decode-named-character-reference: 1.0.2 - micromark-util-character: 2.1.0 - micromark-util-decode-numeric-character-reference: 2.0.1 - micromark-util-symbol: 2.0.0 - dev: true + decode-named-character-reference: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 - /micromark-util-encode@2.0.0: - resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} - dev: true + micromark-util-encode@2.0.1: {} - /micromark-util-html-tag-name@2.0.0: - resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} - dev: true + micromark-util-html-tag-name@2.0.1: {} - /micromark-util-normalize-identifier@2.0.0: - resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} + micromark-util-normalize-identifier@2.0.1: dependencies: - micromark-util-symbol: 2.0.0 - dev: true + micromark-util-symbol: 2.0.1 - /micromark-util-resolve-all@2.0.0: - resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} + micromark-util-resolve-all@2.0.1: dependencies: - micromark-util-types: 2.0.0 - dev: true + micromark-util-types: 2.0.2 - /micromark-util-sanitize-uri@2.0.0: - resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + micromark-util-sanitize-uri@2.0.1: dependencies: - micromark-util-character: 2.1.0 - micromark-util-encode: 2.0.0 - micromark-util-symbol: 2.0.0 - dev: true + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 - /micromark-util-subtokenize@2.0.0: - resolution: {integrity: sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==} + micromark-util-subtokenize@2.1.0: dependencies: devlop: 1.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-util-symbol@2.0.0: - resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} - dev: true + micromark-util-symbol@2.0.1: {} - /micromark-util-types@2.0.0: - resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} - dev: true + micromark-util-types@2.0.2: {} - /micromark@4.0.0: - resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} + micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.3.4 - decode-named-character-reference: 1.0.2 + debug: 4.4.0 + decode-named-character-reference: 1.1.0 devlop: 1.1.0 - micromark-core-commonmark: 2.0.0 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-combine-extensions: 2.0.0 - micromark-util-decode-numeric-character-reference: 2.0.1 - micromark-util-encode: 2.0.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-resolve-all: 2.0.0 - micromark-util-sanitize-uri: 2.0.0 - micromark-util-subtokenize: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 transitivePeerDependencies: - supports-color - dev: true - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} + micromatch@4.0.8: dependencies: - braces: 3.0.2 + braces: 3.0.3 picomatch: 2.3.1 - dev: true - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} + mime-db@1.52.0: {} - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 - /mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true - dev: true + mime@2.6.0: {} - /mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - dev: false + mime@3.0.0: {} - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: true + mimic-fn@2.1.0: {} - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true + mimic-fn@4.0.0: {} - /mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} + mimic-function@5.0.1: {} - /mimic-response@4.0.0: - resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + mimic-response@3.1.0: {} - /min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - dev: true + mimic-response@4.0.0: {} - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 + min-indent@1.0.1: {} - /minimatch@9.0.1: - resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} - engines: {node: '>=16 || 14 >=14.17'} + minimatch@3.1.2: dependencies: - brace-expansion: 2.0.1 - dev: true + brace-expansion: 1.1.11 - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.1: dependencies: brace-expansion: 2.0.1 - dev: true - /minimatch@9.0.4: - resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} - engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 - dev: true - - /minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - dependencies: - yallist: 4.0.0 - dev: true - /minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - dev: true + minipass@7.1.2: {} - /minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} + minizlib@3.0.1: dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - dev: true + minipass: 7.1.2 + rimraf: 5.0.10 - /mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - dev: false + mitt@3.0.1: {} - /mixin-object@2.0.1: - resolution: {integrity: sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==} - engines: {node: '>=0.10.0'} + mixin-object@2.0.1: dependencies: for-in: 0.1.8 is-extendable: 0.1.1 - dev: false - /mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - dev: true + mkdirp@3.0.1: {} - /mlly@1.6.1: - resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} - dependencies: - acorn: 8.11.3 - pathe: 1.1.2 - pkg-types: 1.0.3 - ufo: 1.5.3 - dev: true + mockdate@3.0.5: {} - /mockdate@3.0.5: - resolution: {integrity: sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==} - dev: true + module-alias@2.2.3: {} - /module-alias@2.2.3: - resolution: {integrity: sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==} - dev: false + module-details-from-path@1.0.3: {} - /moment-parseformat@3.0.0: - resolution: {integrity: sha512-dVgXe6b6DLnv4CHG7a1zUe5mSXaIZ3c6lSHm/EKeVeQI2/4pwe0VRde8OyoCE1Ro2lKT5P6uT9JElF7KDLV+jw==} - dev: false + moment-parseformat@3.0.0: {} - /moment@2.30.1: - resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - dev: false + moment@2.30.1: {} - /ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - dev: false + ms@2.0.0: {} - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.2: {} - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: false + ms@2.1.3: {} - /msw@2.2.14(typescript@5.4.5): - resolution: {integrity: sha512-64i8rNCa1xzDK8ZYsTrVMli05D687jty8+Th+PU5VTbJ2/4P7fkQFVyDQ6ZFT5FrNR8z2BHhbY47fKNvfHrumA==} - engines: {node: '>=18'} - hasBin: true - requiresBuild: true - peerDependencies: - typescript: '>= 4.7.x' - peerDependenciesMeta: - typescript: - optional: true + msw@2.4.3(typescript@5.8.2): dependencies: - '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 - '@inquirer/confirm': 3.1.1 - '@mswjs/cookies': 1.1.0 - '@mswjs/interceptors': 0.26.15 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 3.2.0 + '@mswjs/interceptors': 0.29.1 '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 '@types/statuses': 2.0.5 chalk: 4.1.2 - graphql: 16.8.1 + graphql: 16.10.0 headers-polyfill: 4.0.3 is-node-process: 1.2.0 - outvariant: 1.4.2 - path-to-regexp: 6.2.1 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 strict-event-emitter: 0.5.1 - type-fest: 4.14.0 - typescript: 5.4.5 + type-fest: 4.38.0 yargs: 17.7.2 - dev: true + optionalDependencies: + typescript: 5.8.2 + + mute-stream@0.0.8: {} + + mute-stream@1.0.0: {} - /mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - dev: true + nanoid@3.3.11: {} - /mute-stream@1.0.0: - resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true + nanoid@5.1.5: {} - /nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true + narou@1.1.0: + dependencies: + date-fns: 4.1.0 - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true + natural-compare@1.4.0: {} - /netmask@2.0.2: - resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} - engines: {node: '>= 0.4.0'} - dev: false + netmask@2.0.2: {} - /next-tick@1.1.0: - resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - dev: false + next-tick@1.1.0: {} - /no-case@2.3.2: - resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + no-case@2.3.2: dependencies: lower-case: 1.1.4 - dev: false - /node-fetch-native@1.6.4: - resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} - dev: false + node-fetch-native@1.6.6: {} - /node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - /node-gyp-build@4.8.0: - resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==} - hasBin: true + node-gyp-build@4.8.4: {} - /node-localstorage@2.2.1: - resolution: {integrity: sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==} - engines: {node: '>=0.12'} + node-localstorage@2.2.1: dependencies: write-file-atomic: 1.3.4 - dev: false - /node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - dev: true + node-network-devtools@1.0.25(bufferutil@4.0.9)(undici@6.21.2)(utf-8-validate@5.0.10): + dependencies: + iconv-lite: 0.6.3 + open: 8.4.2 + undici: 6.21.2 + ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate - /nodemailer@6.9.13: - resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==} - engines: {node: '>=6.0.0'} - dev: false + node-releases@2.0.19: {} - /nopt@5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} - hasBin: true - dependencies: - abbrev: 1.1.1 - dev: true + nodemailer@6.10.0: {} - /nopt@7.2.0: - resolution: {integrity: sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - hasBin: true + nodemailer@6.9.16: {} + + nopt@7.2.1: dependencies: abbrev: 2.0.0 - dev: true - /normalize-package-data@2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + nopt@8.1.0: + dependencies: + abbrev: 3.0.0 + + normalize-package-data@6.0.2: dependencies: - hosted-git-info: 2.8.9 - resolve: 1.22.8 - semver: 5.7.2 + hosted-git-info: 7.0.2 + semver: 7.7.1 validate-npm-package-license: 3.0.4 - dev: true - /normalize-url@8.0.0: - resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} - engines: {node: '>=14.16'} + normalize-url@8.0.1: {} - /notion-to-md@3.1.1: - resolution: {integrity: sha512-Zaa2P1B9Rx99bevFYTGuUMYbbfdHn2G1AZMsytYGDWIJjr6Ie1qp/8CorpwVUh1qrquES/V2PkEREqCuTu1zKA==} - engines: {node: '>=12'} + notion-to-md@3.1.7: dependencies: markdown-table: 2.0.0 node-fetch: 2.7.0 transitivePeerDependencies: - encoding - dev: false - - /npm-run-path@2.0.2: - resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} - engines: {node: '>=4'} - dependencies: - path-key: 2.0.1 - dev: false - /npm-run-path@5.2.0: - resolution: {integrity: sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 - dev: true - - /npmlog@5.0.1: - resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} - dependencies: - are-we-there-yet: 2.0.0 - console-control-strings: 1.1.0 - gauge: 3.0.2 - set-blocking: 2.0.0 - dev: true - /nth-check@1.0.2: - resolution: {integrity: sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==} + nth-check@1.0.2: dependencies: boolbase: 1.0.0 - dev: false - /nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 - dev: false - - /nwsapi@2.2.7: - resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} - - /oauth-1.0a@2.2.6: - resolution: {integrity: sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==} - dev: false - /oauth-sign@0.9.0: - resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} - dev: false + nwsapi@2.2.19: {} - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: true + oauth-1.0a@2.2.6: {} - /object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + oauth-sign@0.9.0: {} - /ofetch@1.3.4: - resolution: {integrity: sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==} + ofetch@1.4.1: dependencies: destr: 2.0.3 - node-fetch-native: 1.6.4 - ufo: 1.5.3 - dev: false + node-fetch-native: 1.6.6 + ufo: 1.5.4 - /on-exit-leak-free@2.1.2: - resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} - engines: {node: '>=14.0.0'} - dev: false + on-exit-leak-free@2.1.2: {} - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + once@1.4.0: dependencies: wrappy: 1.0.2 - /one-time@1.0.0: - resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + one-time@1.0.0: dependencies: fn.name: 1.1.0 - dev: false - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 - dev: true - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 - dev: true - /openapi3-ts@4.3.1: - resolution: {integrity: sha512-ha/kTOLhMQL7MvS9Abu/cpCXx5qwHQ++88YkUzn1CGfmM8JvCOG/4ZE6tRsexgXRFaoJrcwLyf81H2Y/CXALtA==} + onetime@7.0.0: dependencies: - yaml: 2.4.1 - dev: false + mimic-function: 5.0.1 - /optionator@0.8.3: - resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} - engines: {node: '>= 0.8.0'} + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + openapi-fetch@0.11.3: + dependencies: + openapi-typescript-helpers: 0.0.13 + + openapi-typescript-helpers@0.0.13: {} + + openapi3-ts@4.4.0: + dependencies: + yaml: 2.7.0 + + optionator@0.8.3: dependencies: deep-is: 0.1.4 fast-levenshtein: 2.0.6 @@ -7288,23 +10823,17 @@ packages: prelude-ls: 1.1.2 type-check: 0.3.2 word-wrap: 1.2.5 - dev: false - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} - engines: {node: '>= 0.8.0'} + optionator@0.9.4: dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 fast-levenshtein: 2.0.6 levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true + word-wrap: 1.2.5 - /ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} + ora@5.4.1: dependencies: bl: 4.1.0 chalk: 4.1.2 @@ -7315,787 +10844,469 @@ packages: log-symbols: 4.1.0 strip-ansi: 6.0.1 wcwidth: 1.0.1 - dev: true - /os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - dev: true + os-tmpdir@1.0.2: {} - /otplib@12.0.1: - resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} + otplib@12.0.1: dependencies: '@otplib/core': 12.0.1 '@otplib/preset-default': 12.0.1 '@otplib/preset-v11': 12.0.1 - dev: false - - /outvariant@1.4.2: - resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} - dev: true - - /p-cancelable@3.0.0: - resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} - engines: {node: '>=12.20'} - dev: false - /p-cancelable@4.0.1: - resolution: {integrity: sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==} - engines: {node: '>=14.16'} - dev: true + outvariant@1.4.3: {} - /p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - dev: false + p-cancelable@3.0.0: {} - /p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - dependencies: - p-try: 2.2.0 - dev: true + p-cancelable@4.0.1: {} - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - dev: true - - /p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - dependencies: - yocto-queue: 1.0.0 - dev: true - /p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - dependencies: - p-limit: 2.3.0 - dev: true - - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + p-locate@5.0.0: dependencies: p-limit: 3.1.0 - dev: true - - /p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - dev: true - /pac-proxy-agent@7.0.1: - resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==} - engines: {node: '>= 14'} + pac-proxy-agent@7.2.0: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 - agent-base: 7.1.0 - debug: 4.3.4 - get-uri: 6.0.3 - http-proxy-agent: 7.0.1 - https-proxy-agent: 7.0.4 + agent-base: 7.1.3 + debug: 4.4.0 + get-uri: 6.0.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.3 + socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color - dev: false - /pac-resolver@7.0.1: - resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} - engines: {node: '>= 14'} + pac-resolver@7.0.1: dependencies: degenerator: 5.0.1 netmask: 2.0.2 - dev: false - /pako@2.1.0: - resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - dev: false + package-json-from-dist@1.0.1: {} - /param-case@2.1.1: - resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + pako@2.1.0: {} + + param-case@2.1.1: dependencies: no-case: 2.3.2 - dev: false - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + parent-module@1.0.1: dependencies: callsites: 3.1.0 - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.24.2 + '@babel/code-frame': 7.26.2 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - /parse-srcset@1.0.2: - resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} - dev: false + parse-json@8.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + index-to-position: 1.0.0 + type-fest: 4.38.0 - /parse5-htmlparser2-tree-adapter@7.0.0: - resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + parse-srcset@1.0.2: {} + + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 - parse5: 7.1.2 - dev: false + parse5: 7.2.1 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.2.1 - /parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + parse5@7.2.1: dependencies: entities: 4.5.0 - /parseley@0.12.1: - resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + parseley@0.12.1: dependencies: leac: 0.6.0 peberminta: 0.9.0 - dev: false - /path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - dev: false - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true + path-browserify@1.0.1: {} - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} + path-exists@4.0.0: {} - /path-key@2.0.1: - resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} - engines: {node: '>=4'} - dev: false + path-is-absolute@1.0.1: {} - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + path-key@3.1.1: {} - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true + path-key@4.0.0: {} - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true + path-parse@1.0.7: {} - /path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} + path-scurry@1.11.1: dependencies: - lru-cache: 10.2.2 - minipass: 5.0.0 - dev: true + lru-cache: 10.4.3 + minipass: 7.1.2 - /path-to-regexp@6.2.1: - resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} - dev: true + path-to-regexp@6.3.0: {} - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true + pathe@1.1.2: {} - /pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - dev: true + pathval@2.0.0: {} - /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - dev: true + peberminta@0.9.0: {} - /peberminta@0.9.0: - resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} - dev: false + pend@1.2.0: {} - /pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - dev: false + performance-now@2.1.0: {} - /performance-now@2.1.0: - resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} - dev: false + pg-int8@1.0.1: {} - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + pg-protocol@1.8.0: {} - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 - /picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - dev: true + picocolors@1.1.1: {} - /pidtree@0.6.0: - resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} - engines: {node: '>=0.10'} - hasBin: true - dev: true + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pidtree@0.6.0: {} - /pino-abstract-transport@1.1.0: - resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==} + pino-abstract-transport@2.0.0: dependencies: - readable-stream: 4.5.2 split2: 4.2.0 - dev: false - /pino-std-serializers@6.2.2: - resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} - dev: false + pino-std-serializers@7.0.0: {} - /pino@8.20.0: - resolution: {integrity: sha512-uhIfMj5TVp+WynVASaVEJFTncTUe4dHBq6CWplu/vBgvGHhvBvQfxz+vcOrnnBQdORH3izaGEurLfNlq3YxdFQ==} - hasBin: true + pino@9.6.0: dependencies: atomic-sleep: 1.0.0 - fast-redact: 3.3.0 + fast-redact: 3.5.0 on-exit-leak-free: 2.1.2 - pino-abstract-transport: 1.1.0 - pino-std-serializers: 6.2.2 - process-warning: 3.0.0 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 4.0.1 quick-format-unescaped: 4.0.4 real-require: 0.2.0 - safe-stable-stringify: 2.4.3 - sonic-boom: 3.8.0 - thread-stream: 2.4.1 - dev: false + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + + pluralize@8.0.0: {} - /pkg-types@1.0.3: - resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + postcss@8.5.3: dependencies: - jsonc-parser: 3.2.1 - mlly: 1.6.1 - pathe: 1.1.2 - dev: true + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 - /pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - dev: true + postgres-array@2.0.0: {} - /postcss@8.4.35: - resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.2.0 - dev: false + postgres-bytea@1.0.0: {} - /postcss@8.4.38: - resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} - engines: {node: ^10 || ^12 || >=14} + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: dependencies: - nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.2.0 - dev: true + xtend: 4.0.2 - /postman-request@2.88.1-postman.33: - resolution: {integrity: sha512-uL9sCML4gPH6Z4hreDWbeinKU0p0Ke261nU7OvII95NU22HN6Dk7T/SaVPaj6T4TsQqGKIFw6/woLZnH7ugFNA==} - engines: {node: '>= 6'} + postman-request@2.88.1-postman.42: dependencies: '@postman/form-data': 3.1.1 '@postman/tough-cookie': 4.1.3-postman.1 - '@postman/tunnel-agent': 0.6.3 + '@postman/tunnel-agent': 0.6.4 aws-sign2: 0.7.0 - aws4: 1.12.0 - brotli: 1.3.3 + aws4: 1.13.2 caseless: 0.12.0 combined-stream: 1.0.8 extend: 3.0.2 forever-agent: 0.6.1 - har-validator: 5.1.5 - http-signature: 1.3.6 + http-signature: 1.4.0 is-typedarray: 1.0.0 isstream: 0.1.2 json-stringify-safe: 5.0.1 mime-types: 2.1.35 oauth-sign: 0.9.0 - performance-now: 2.1.0 qs: 6.5.3 - safe-buffer: 5.2.1 + safe-buffer: '@nolyfill/safe-buffer@1.0.44' stream-length: 1.0.2 uuid: 8.3.2 - dev: false - /prelude-ls@1.1.2: - resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} - engines: {node: '>= 0.8.0'} - dev: false + prelude-ls@1.1.2: {} - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true + prelude-ls@1.2.1: {} - /prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} + prettier-linter-helpers@1.0.0: dependencies: fast-diff: 1.3.0 - dev: true - - /prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} - engines: {node: '>=14'} - hasBin: true - dev: true - /pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.2.0 - dev: true + prettier@3.5.3: {} - /process-warning@3.0.0: - resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} - dev: false + process-warning@4.0.1: {} - /process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - dev: false + progress@2.0.3: {} - /progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - dev: false + proto-list@1.2.4: {} - /proto-list@1.2.4: - resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - dev: true + protobufjs@7.4.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.13.15 + long: 5.3.1 - /proxy-agent@6.4.0: - resolution: {integrity: sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==} - engines: {node: '>= 14'} + proxy-agent@6.4.0: dependencies: - agent-base: 7.1.1 + agent-base: 7.1.3 debug: 4.3.4 - http-proxy-agent: 7.0.1 - https-proxy-agent: 7.0.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 lru-cache: 7.18.3 - pac-proxy-agent: 7.0.1 + pac-proxy-agent: 7.2.0 proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.3 + socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color - dev: false - /proxy-chain@2.4.0: - resolution: {integrity: sha512-fbFfzJDxWcLYYvI+yx0VXjTgJPfXsGdhFnCJN4rq/5GZSgn9CQpRgm1KC2iEJfhL8gkeZCWJYBAllmGMT356cg==} - engines: {node: '>=14'} + proxy-chain@2.5.8: dependencies: - tslib: 2.6.2 - dev: false - - /proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: false + socks: 2.8.4 + socks-proxy-agent: 8.0.5 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color - /pseudomap@1.0.2: - resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - dev: false + proxy-from-env@1.1.0: {} - /psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + psl@1.15.0: + dependencies: + punycode: 2.3.1 - /pump@3.0.0: - resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + pump@3.0.2: dependencies: end-of-stream: 1.4.4 once: 1.4.0 - dev: false - /punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - dev: false + punycode.js@2.3.1: {} - /punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} + punycode@2.3.1: {} - /puppeteer-core@22.6.2: - resolution: {integrity: sha512-Sws/9V2/7nFrn3MSsRPHn1pXJMIFn6FWHhoMFMUBXQwVvcBstRIa9yW8sFfxePzb56W1xNfSYzPRnyAd0+qRVQ==} - engines: {node: '>=18'} + puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@puppeteer/browsers': 2.2.0 chromium-bidi: 0.5.16(devtools-protocol@0.0.1262051) debug: 4.3.4 devtools-protocol: 0.0.1262051 - ws: 8.16.0 + ws: 8.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: + - bare-buffer - bufferutil - supports-color - utf-8-validate - dev: false - /puppeteer-extra-plugin-stealth@2.11.2(puppeteer-extra@3.3.6): - resolution: {integrity: sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ==} - engines: {node: '>=8'} - peerDependencies: - playwright-extra: '*' - puppeteer-extra: '*' - peerDependenciesMeta: - playwright-extra: - optional: true - puppeteer-extra: - optional: true + puppeteer-extra-plugin-stealth@2.11.2(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))): dependencies: - debug: 4.3.4 - puppeteer-extra: 3.3.6(puppeteer@22.6.2) - puppeteer-extra-plugin: 3.2.3(puppeteer-extra@3.3.6) - puppeteer-extra-plugin-user-preferences: 2.4.1(puppeteer-extra@3.3.6) + debug: 4.4.0 + puppeteer-extra-plugin: 3.2.3(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))) + puppeteer-extra-plugin-user-preferences: 2.4.1(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))) + optionalDependencies: + puppeteer-extra: 3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)) transitivePeerDependencies: - supports-color - dev: false - /puppeteer-extra-plugin-user-data-dir@2.4.1(puppeteer-extra@3.3.6): - resolution: {integrity: sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g==} - engines: {node: '>=8'} - peerDependencies: - playwright-extra: '*' - puppeteer-extra: '*' - peerDependenciesMeta: - playwright-extra: - optional: true - puppeteer-extra: - optional: true + puppeteer-extra-plugin-user-data-dir@2.4.1(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))): dependencies: - debug: 4.3.4 + debug: 4.4.0 fs-extra: 10.1.0 - puppeteer-extra: 3.3.6(puppeteer@22.6.2) - puppeteer-extra-plugin: 3.2.3(puppeteer-extra@3.3.6) + puppeteer-extra-plugin: 3.2.3(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))) rimraf: 3.0.2 + optionalDependencies: + puppeteer-extra: 3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)) transitivePeerDependencies: - supports-color - dev: false - /puppeteer-extra-plugin-user-preferences@2.4.1(puppeteer-extra@3.3.6): - resolution: {integrity: sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A==} - engines: {node: '>=8'} - peerDependencies: - playwright-extra: '*' - puppeteer-extra: '*' - peerDependenciesMeta: - playwright-extra: - optional: true - puppeteer-extra: - optional: true + puppeteer-extra-plugin-user-preferences@2.4.1(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))): dependencies: - debug: 4.3.4 + debug: 4.4.0 deepmerge: 4.3.1 - puppeteer-extra: 3.3.6(puppeteer@22.6.2) - puppeteer-extra-plugin: 3.2.3(puppeteer-extra@3.3.6) - puppeteer-extra-plugin-user-data-dir: 2.4.1(puppeteer-extra@3.3.6) + puppeteer-extra-plugin: 3.2.3(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))) + puppeteer-extra-plugin-user-data-dir: 2.4.1(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))) + optionalDependencies: + puppeteer-extra: 3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)) transitivePeerDependencies: - supports-color - dev: false - /puppeteer-extra-plugin@3.2.3(puppeteer-extra@3.3.6): - resolution: {integrity: sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q==} - engines: {node: '>=9.11.2'} - peerDependencies: - playwright-extra: '*' - puppeteer-extra: '*' - peerDependenciesMeta: - playwright-extra: - optional: true - puppeteer-extra: - optional: true + puppeteer-extra-plugin@3.2.3(puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))): dependencies: '@types/debug': 4.1.12 - debug: 4.3.4 + debug: 4.4.0 merge-deep: 3.0.3 - puppeteer-extra: 3.3.6(puppeteer@22.6.2) + optionalDependencies: + puppeteer-extra: 3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)) transitivePeerDependencies: - supports-color - dev: false - /puppeteer-extra@3.3.6(puppeteer@22.6.2): - resolution: {integrity: sha512-rsLBE/6mMxAjlLd06LuGacrukP2bqbzKCLzV1vrhHFavqQE/taQ2UXv3H5P0Ls7nsrASa+6x3bDbXHpqMwq+7A==} - engines: {node: '>=8'} - peerDependencies: - '@types/puppeteer': '*' - puppeteer: '*' - puppeteer-core: '*' - peerDependenciesMeta: - '@types/puppeteer': - optional: true - puppeteer: - optional: true - puppeteer-core: - optional: true + puppeteer-extra@3.3.6(puppeteer-core@22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)): dependencies: '@types/debug': 4.1.12 - debug: 4.3.4 + debug: 4.4.0 deepmerge: 4.3.1 - puppeteer: 22.6.2(typescript@5.4.5) + optionalDependencies: + puppeteer: 22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + puppeteer-core: 22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - supports-color - dev: false - /puppeteer@22.6.2(typescript@5.4.5): - resolution: {integrity: sha512-3GMAJ9adPUSdIHGuYV1b1RqRB6D2UScjnq779uZsvpAP6HOWw2+9ezZiUZaAXVST+Ku7KWsxOjkctEvRasJClA==} - engines: {node: '>=18'} - hasBin: true - requiresBuild: true + puppeteer@22.6.2(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10): dependencies: '@puppeteer/browsers': 2.2.0 - cosmiconfig: 9.0.0(typescript@5.4.5) + cosmiconfig: 9.0.0(typescript@5.8.2) devtools-protocol: 0.0.1262051 - puppeteer-core: 22.6.2 + puppeteer-core: 22.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: + - bare-buffer - bufferutil - supports-color - typescript - utf-8-validate - dev: false - /qs@6.11.2: - resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} - engines: {node: '>=0.6'} + qs@6.14.0: dependencies: - side-channel: 1.0.5 + side-channel: '@nolyfill/side-channel@1.0.44' - /qs@6.5.3: - resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} - engines: {node: '>=0.6'} - dev: false + qs@6.5.3: {} - /query-string@7.1.3: - resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} - engines: {node: '>=6'} + query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 filter-obj: 1.1.0 split-on-first: 1.1.0 strict-uri-encode: 2.0.0 - dev: false - /query-string@9.0.0: - resolution: {integrity: sha512-4EWwcRGsO2H+yzq6ddHcVqkCQ2EFUSfDMEjF8ryp8ReymyZhIuaFRGLomeOQLkrzacMHoyky2HW0Qe30UbzkKw==} - engines: {node: '>=18'} + query-string@9.1.1: dependencies: decode-uri-component: 0.4.1 filter-obj: 5.1.0 - split-on-first: 3.0.0 - dev: false - - /querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + split-on-first: 3.0.0 - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true + querystringify@2.2.0: {} - /queue-tick@1.0.1: - resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} - requiresBuild: true - dev: false + queue-microtask@1.2.3: {} - /quick-format-unescaped@4.0.4: - resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - dev: false + quick-format-unescaped@4.0.4: {} - /quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} + quick-lru@5.1.1: {} - /re2js@0.4.1: - resolution: {integrity: sha512-Kxb+OKXrEPowP4bXAF07NDXtgYX07S8HeVGgadx5/D/R41LzWg1kgTD2szIv2iHJM3vrAPnDKaBzfUE/7QWX9w==} - dev: false + rate-limiter-flexible@6.2.1: {} - /react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: true + re2js@1.1.0: {} - /read-pkg-up@7.0.1: - resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} - engines: {node: '>=8'} + read-package-up@11.0.0: dependencies: - find-up: 4.1.0 - read-pkg: 5.2.0 - type-fest: 0.8.1 - dev: true + find-up-simple: 1.0.1 + read-pkg: 9.0.1 + type-fest: 4.38.0 - /read-pkg@5.2.0: - resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} - engines: {node: '>=8'} + read-pkg@9.0.1: dependencies: '@types/normalize-package-data': 2.4.4 - normalize-package-data: 2.5.0 - parse-json: 5.2.0 - type-fest: 0.6.0 - dev: true + normalize-package-data: 6.0.2 + parse-json: 8.2.0 + type-fest: 4.38.0 + unicorn-magic: 0.1.0 - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - /readable-stream@4.5.2: - resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - abort-controller: 3.0.0 - buffer: 6.0.3 - events: 3.3.0 - process: 0.11.10 - string_decoder: 1.3.0 - dev: false - - /real-cancellable-promise@1.2.0: - resolution: {integrity: sha512-FYhmx1FVSgoPRjneoTjh+EKZcNb8ijl/dyatTzase5eujYhVrLNDOiIY6AgQq7GU1kOoLgEd9jLVbhFg8k8dOQ==} - dev: false + real-cancellable-promise@1.2.1: {} - /real-require@0.2.0: - resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} - engines: {node: '>= 12.13.0'} - dev: false + real-require@0.2.0: {} - /redis-errors@1.2.0: - resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} - engines: {node: '>=4'} - dev: false + redis-errors@1.2.0: {} - /redis-parser@3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} + redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 - dev: false - /reflect-metadata@0.1.14: - resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} - dev: false + reflect-metadata@0.1.14: {} - /regenerate-unicode-properties@10.1.1: - resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} - engines: {node: '>=4'} + regenerate-unicode-properties@10.2.0: dependencies: regenerate: 1.4.2 - dev: true - /regenerate@1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - dev: true + regenerate@1.4.2: {} - /regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regenerator-runtime@0.14.1: {} - /regenerator-transform@0.15.2: - resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + regenerator-transform@0.15.2: dependencies: - '@babel/runtime': 7.24.4 - dev: true + '@babel/runtime': 7.27.0 - /regexp-tree@0.1.27: - resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} - hasBin: true - dev: true + regexp-tree@0.1.27: {} - /regexpu-core@5.3.2: - resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} - engines: {node: '>=4'} + regexpu-core@6.2.0: dependencies: - '@babel/regjsgen': 0.8.0 regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.1 - regjsparser: 0.9.1 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.1.0 - dev: true + unicode-match-property-value-ecmascript: 2.2.0 - /regjsparser@0.10.0: - resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} - hasBin: true - dependencies: - jsesc: 0.5.0 - dev: true + regjsgen@0.8.0: {} - /regjsparser@0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} - hasBin: true + regjsparser@0.12.0: dependencies: - jsesc: 0.5.0 - dev: true + jsesc: 3.0.2 - /relateurl@0.2.7: - resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} - engines: {node: '>= 0.10'} - dev: false + relateurl@0.2.7: {} - /remark-parse@11.0.0: - resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + remark-parse@11.0.0: dependencies: - '@types/mdast': 4.0.3 - mdast-util-from-markdown: 2.0.0 - micromark-util-types: 2.0.0 - unified: 11.0.4 + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 transitivePeerDependencies: - supports-color - dev: true - /repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - dev: false + repeat-string@1.6.1: {} - /request-promise-core@1.1.4(request@2.88.2): - resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==} - engines: {node: '>=0.10.0'} - peerDependencies: - request: ^2.34 + request-promise-core@1.1.4(request@2.88.2): dependencies: lodash: 4.17.21 request: 2.88.2 - dev: false - /request-promise@4.2.6(request@2.88.2): - resolution: {integrity: sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==} - engines: {node: '>=0.10.0'} - deprecated: request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 - peerDependencies: - request: ^2.34 + request-promise@4.2.6(request@2.88.2): dependencies: bluebird: 3.7.2 request: 2.88.2 request-promise-core: 1.1.4(request@2.88.2) stealthy-require: 1.1.1 tough-cookie: 2.5.0 - dev: false - /request@2.88.2: - resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} - engines: {node: '>= 6'} - deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + request@2.88.2: dependencies: aws-sign2: 0.7.0 - aws4: 1.12.0 + aws4: 1.13.2 caseless: 0.12.0 combined-stream: 1.0.8 extend: 3.0.2 @@ -8110,412 +11321,235 @@ packages: oauth-sign: 0.9.0 performance-now: 2.1.0 qs: 6.5.3 - safe-buffer: 5.2.1 + safe-buffer: '@nolyfill/safe-buffer@1.0.44' tough-cookie: 2.5.0 tunnel-agent: 0.6.0 uuid: 3.4.0 - dev: false - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} + require-directory@2.1.1: {} - /requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.0 + module-details-from-path: 1.0.3 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color - /resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + requires-port@1.0.0: {} - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} + resolve-alpn@1.2.1: {} - /resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - dev: true + resolve-from@4.0.0: {} - /resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-from@5.0.0: {} - /resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: dependencies: - is-core-module: 2.13.1 + is-core-module: '@nolyfill/is-core-module@1.0.39' path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true - /responselike@3.0.0: - resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} - engines: {node: '>=14.16'} + responselike@3.0.0: dependencies: lowercase-keys: 3.0.0 - /restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 - dev: true - /restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + restore-cursor@5.1.0: dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - dev: true + onetime: 7.0.0 + signal-exit: 4.1.0 - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true + reusify@1.1.0: {} - /rfc4648@1.5.3: - resolution: {integrity: sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==} - dev: false + rfc4648@1.5.4: {} - /rfdc@1.3.1: - resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} - dev: true + rfdc@1.4.1: {} - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true + rimraf@3.0.2: dependencies: glob: 7.2.3 - /rollup@4.14.3: - resolution: {integrity: sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true + rimraf@5.0.10: + dependencies: + glob: 10.4.5 + + rollup@4.37.0: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.14.3 - '@rollup/rollup-android-arm64': 4.14.3 - '@rollup/rollup-darwin-arm64': 4.14.3 - '@rollup/rollup-darwin-x64': 4.14.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.14.3 - '@rollup/rollup-linux-arm-musleabihf': 4.14.3 - '@rollup/rollup-linux-arm64-gnu': 4.14.3 - '@rollup/rollup-linux-arm64-musl': 4.14.3 - '@rollup/rollup-linux-powerpc64le-gnu': 4.14.3 - '@rollup/rollup-linux-riscv64-gnu': 4.14.3 - '@rollup/rollup-linux-s390x-gnu': 4.14.3 - '@rollup/rollup-linux-x64-gnu': 4.14.3 - '@rollup/rollup-linux-x64-musl': 4.14.3 - '@rollup/rollup-win32-arm64-msvc': 4.14.3 - '@rollup/rollup-win32-ia32-msvc': 4.14.3 - '@rollup/rollup-win32-x64-msvc': 4.14.3 + '@rollup/rollup-android-arm-eabi': 4.37.0 + '@rollup/rollup-android-arm64': 4.37.0 + '@rollup/rollup-darwin-arm64': 4.37.0 + '@rollup/rollup-darwin-x64': 4.37.0 + '@rollup/rollup-freebsd-arm64': 4.37.0 + '@rollup/rollup-freebsd-x64': 4.37.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.37.0 + '@rollup/rollup-linux-arm-musleabihf': 4.37.0 + '@rollup/rollup-linux-arm64-gnu': 4.37.0 + '@rollup/rollup-linux-arm64-musl': 4.37.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.37.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.37.0 + '@rollup/rollup-linux-riscv64-gnu': 4.37.0 + '@rollup/rollup-linux-riscv64-musl': 4.37.0 + '@rollup/rollup-linux-s390x-gnu': 4.37.0 + '@rollup/rollup-linux-x64-gnu': 4.37.0 + '@rollup/rollup-linux-x64-musl': 4.37.0 + '@rollup/rollup-win32-arm64-msvc': 4.37.0 + '@rollup/rollup-win32-ia32-msvc': 4.37.0 + '@rollup/rollup-win32-x64-msvc': 4.37.0 fsevents: 2.3.3 - dev: true - /rrweb-cssom@0.6.0: - resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + rrweb-cssom@0.8.0: {} - /rss-parser@3.13.0: - resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} + rss-parser@3.13.0: dependencies: entities: 2.2.0 xml2js: 0.5.0 - dev: false - /run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - dev: true + run-async@2.4.1: {} - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - dev: true - /rxjs@6.6.7: - resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} - engines: {npm: '>=2.0.0'} + rxjs@6.6.7: dependencies: tslib: 1.14.1 - dev: false - /rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + rxjs@7.8.2: dependencies: - tslib: 2.6.2 - dev: true - - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - /safe-stable-stringify@2.4.3: - resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} - engines: {node: '>=10'} - dev: false + tslib: 2.8.1 - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + safe-stable-stringify@2.5.0: {} - /sanitize-html@2.13.0: - resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==} + sanitize-html@2.15.0: dependencies: deepmerge: 4.3.1 escape-string-regexp: 4.0.0 htmlparser2: 8.0.2 is-plain-object: 5.0.0 parse-srcset: 1.0.2 - postcss: 8.4.35 - dev: false + postcss: 8.5.3 - /sax@1.3.0: - resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} - dev: false + sax@1.4.1: {} - /saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} + saxes@6.0.0: dependencies: xmlchars: 2.2.0 - /selderee@0.11.0: - resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + selderee@0.11.0: dependencies: parseley: 0.12.1 - dev: false - - /semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - dev: true - /semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - dev: true + semver@6.3.1: {} - /semver@7.6.0: - resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} - engines: {node: '>=10'} - hasBin: true + semver@7.6.0: dependencies: lru-cache: 6.0.0 - /set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: true - - /set-function-length@1.2.1: - resolution: {integrity: sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.2.4 - gopd: 1.0.1 - has-property-descriptors: 1.0.2 + semver@7.7.1: {} - /shallow-clone@0.1.2: - resolution: {integrity: sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==} - engines: {node: '>=0.10.0'} + shallow-clone@0.1.2: dependencies: is-extendable: 0.1.1 kind-of: 2.0.1 lazy-cache: 0.2.7 mixin-object: 2.0.1 - dev: false - - /shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - dependencies: - shebang-regex: 1.0.0 - dev: false - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 - /shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - dev: false - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + shebang-regex@3.0.0: {} - /side-channel@1.0.5: - resolution: {integrity: sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - get-intrinsic: 1.2.4 - object-inspect: 1.13.1 + shimmer@1.2.1: {} - /siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - dev: true + siginfo@2.0.0: {} - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@3.0.7: {} - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - dev: true + signal-exit@4.1.0: {} - /simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 - dev: false - - /simplecc-wasm@0.1.5: - resolution: {integrity: sha512-cf7QTS/ubpNH4Nk4YiSvlRgMSirpg2bj7NLPV6GqZewz5uHljea4XCGB3DriNh1QgmjIjpbA0pt34cN+MDO6BQ==} - dev: false - /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: true + simplecc-wasm@1.1.0: {} - /slice-ansi@5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.1 is-fullwidth-code-point: 4.0.0 - dev: true - /slice-ansi@7.1.0: - resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} - engines: {node: '>=18'} + slice-ansi@7.1.0: dependencies: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 - dev: true - /slide@1.1.6: - resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==} - dev: false + slide@1.1.6: {} - /smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - dev: false + smart-buffer@4.2.0: {} - /snakecase-keys@3.2.1: - resolution: {integrity: sha512-CjU5pyRfwOtaOITYv5C8DzpZ8XA/ieRsDpr93HI2r6e3YInC6moZpSQbmUtg8cTk58tq2x3jcG2gv+p1IZGmMA==} - engines: {node: '>=8'} + snakecase-keys@3.2.1: dependencies: map-obj: 4.3.0 to-snake-case: 1.0.0 - dev: false - /socks-proxy-agent@8.0.3: - resolution: {integrity: sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==} - engines: {node: '>= 14'} + socks-proxy-agent@8.0.5: dependencies: - agent-base: 7.1.1 - debug: 4.3.4 - socks: 2.8.1 + agent-base: 7.1.3 + debug: 4.4.0 + socks: 2.8.4 transitivePeerDependencies: - supports-color - dev: false - - /socks@2.8.1: - resolution: {integrity: sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - dependencies: - ip-address: 9.0.5 - smart-buffer: 4.2.0 - dev: false - /socks@2.8.3: - resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + socks@2.8.4: dependencies: ip-address: 9.0.5 smart-buffer: 4.2.0 - dev: false - /sonic-boom@3.8.0: - resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==} + sonic-boom@4.2.0: dependencies: atomic-sleep: 1.0.0 - dev: false - /source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} + source-map-js@1.2.1: {} - /source-map@0.5.7: - resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} - engines: {node: '>=0.10.0'} - dev: false + source-map@0.5.7: {} - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: false + source-map@0.6.1: {} - /source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} - dev: false + source-map@0.7.4: {} - /spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.17 - dev: true + spdx-license-ids: 3.0.21 - /spdx-exceptions@2.4.0: - resolution: {integrity: sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==} - dev: true + spdx-exceptions@2.5.0: {} - /spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + spdx-expression-parse@3.0.1: dependencies: - spdx-exceptions: 2.4.0 - spdx-license-ids: 3.0.17 - dev: true + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 - /spdx-license-ids@3.0.17: - resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} - dev: true + spdx-license-ids@3.0.21: {} - /split-on-first@1.1.0: - resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} - engines: {node: '>=6'} - dev: false + split-on-first@1.1.0: {} - /split-on-first@3.0.0: - resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} - engines: {node: '>=12'} - dev: false + split-on-first@3.0.0: {} - /split2@4.2.0: - resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} - engines: {node: '>= 10.x'} - dev: false + split2@4.2.0: {} - /sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - dev: false + sprintf-js@1.1.3: {} - /sshpk@1.18.0: - resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} - engines: {node: '>=0.10.0'} - hasBin: true + sshpk@1.18.0: dependencies: asn1: 0.2.6 assert-plus: 1.0.0 @@ -8524,265 +11558,158 @@ packages: ecc-jsbn: 0.1.2 getpass: 0.1.7 jsbn: 0.1.1 - safer-buffer: 2.1.2 + safer-buffer: '@nolyfill/safer-buffer@1.0.44' tweetnacl: 0.14.5 - dev: false - /stack-trace@0.0.10: - resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} - dev: false + stack-trace@0.0.10: {} - /stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - dev: true + stackback@0.0.2: {} - /standard-as-callback@2.1.0: - resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - dev: false + standard-as-callback@2.1.0: {} - /statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - dev: true + statuses@2.0.1: {} - /std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} - dev: true + std-env@3.8.1: {} - /stealthy-require@1.1.1: - resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} - engines: {node: '>=0.10.0'} - dev: false + stealthy-require@1.1.1: {} - /store2@2.14.3: - resolution: {integrity: sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==} - dev: false + store2@2.14.4: {} - /stream-length@1.0.2: - resolution: {integrity: sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==} + stream-length@1.0.2: dependencies: bluebird: 2.11.0 - dev: false - /streamx@2.15.8: - resolution: {integrity: sha512-6pwMeMY/SuISiRsuS8TeIrAzyFbG5gGPHFQsYjUr/pbBadaL1PCWmzKw+CHZSwainfvcF6Si6cVLq4XTEwswFQ==} + streamx@2.22.0: dependencies: fast-fifo: 1.3.2 - queue-tick: 1.0.1 + text-decoder: 1.2.3 optionalDependencies: - bare-events: 2.2.0 - dev: false + bare-events: 2.5.4 - /strict-event-emitter@0.5.1: - resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} - dev: true + strict-event-emitter@0.5.1: {} - /strict-uri-encode@2.0.0: - resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} - engines: {node: '>=4'} - dev: false + strict-uri-encode@2.0.0: {} - /string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - dev: true + string-argv@0.3.2: {} - /string-direction@0.1.2: - resolution: {integrity: sha512-NJHQRg6GlOEMLA6jEAlSy21KaXvJDNoAid/v6fBAJbqdvOEIiPpCrIPTHnl4636wUF/IGyktX5A9eddmETb1Cw==} - dev: false + string-direction@0.1.2: {} - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} + string-width@5.1.2: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 strip-ansi: 7.1.0 - dev: true - /string-width@7.1.0: - resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==} - engines: {node: '>=18'} + string-width@7.2.0: dependencies: - emoji-regex: 10.3.0 - get-east-asian-width: 1.2.0 + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 strip-ansi: 7.1.0 - dev: true - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + string_decoder@1.3.0: dependencies: - safe-buffer: 5.2.1 + safe-buffer: '@nolyfill/safe-buffer@1.0.44' - /strip-ansi@3.0.1: - resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} - engines: {node: '>=0.10.0'} + strip-ansi@3.0.1: dependencies: ansi-regex: 2.1.1 - dev: true - /strip-ansi@5.2.0: - resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} - engines: {node: '>=6'} + strip-ansi@5.2.0: dependencies: ansi-regex: 4.1.1 - dev: true - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} + strip-ansi@7.1.0: dependencies: - ansi-regex: 6.0.1 - dev: true - - /strip-eof@1.0.0: - resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} - engines: {node: '>=0.10.0'} - dev: false + ansi-regex: 6.1.0 - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true + strip-final-newline@3.0.0: {} - /strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} + strip-indent@4.0.0: dependencies: min-indent: 1.0.1 - dev: true - - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true - /strip-literal@2.0.0: - resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} - dependencies: - js-tokens: 8.0.3 - dev: true + strip-json-comments@3.1.1: {} - /superagent@9.0.1: - resolution: {integrity: sha512-CcRSdb/P2oUVaEpQ87w9Obsl+E9FruRd6b2b7LdiBtJoyMr2DQt7a89anAfiX/EL59j9b2CbRFvf2S91DhuCww==} - engines: {node: '>=14.18.0'} + superagent@9.0.2: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.3.4 + debug: 4.4.0 fast-safe-stringify: 2.1.1 - form-data: 4.0.0 - formidable: 3.5.1 + form-data: 4.0.2 + formidable: 3.5.2 methods: 1.1.2 mime: 2.6.0 - qs: 6.11.2 - semver: 7.6.0 + qs: 6.14.0 transitivePeerDependencies: - supports-color - dev: true - /supertest@7.0.0: - resolution: {integrity: sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==} - engines: {node: '>=14.18.0'} + supertest@7.1.0: dependencies: methods: 1.1.2 - superagent: 9.0.1 + superagent: 9.0.2 transitivePeerDependencies: - supports-color - dev: true - - /supports-color@2.0.0: - resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} - engines: {node: '>=0.8.0'} - dev: true - /supports-color@4.5.0: - resolution: {integrity: sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==} - engines: {node: '>=4'} - dependencies: - has-flag: 2.0.0 - dev: false + supports-color@2.0.0: {} - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 - dev: true - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: true + supports-preserve-symlinks-flag@1.0.0: {} - /symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + symbol-tree@3.2.4: {} - /synckit@0.8.8: - resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} - engines: {node: ^14.18.0 || >=16.0.0} + synckit@0.10.3: dependencies: - '@pkgr/core': 0.1.1 - tslib: 2.6.2 - dev: true + '@pkgr/core': 0.2.0 + tslib: 2.8.1 - /tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} - dev: true + system-architecture@0.1.0: {} - /tar-fs@3.0.5: - resolution: {integrity: sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==} + tapable@2.2.1: {} + + tar-fs@3.0.5: dependencies: - pump: 3.0.0 + pump: 3.0.2 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 2.2.3 - bare-path: 2.1.1 - dev: false + bare-fs: 2.3.5 + bare-path: 2.1.3 + transitivePeerDependencies: + - bare-buffer - /tar-stream@3.1.7: - resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tar-stream@3.1.7: dependencies: - b4a: 1.6.6 + b4a: 1.6.7 fast-fifo: 1.3.2 - streamx: 2.15.8 - dev: false + streamx: 2.22.0 - /tar@6.2.0: - resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} - engines: {node: '>=10'} + tar@7.4.3: dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - dev: true + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.1 + mkdirp: 3.0.1 + yallist: 5.0.0 - /telegram@2.20.15: - resolution: {integrity: sha512-yc5ScUlg5X47z9BhGTJEaHqSYe8cGsyjUHw1+vGD2vj9ntrbp2C/YpOvP0cepTJk/k5xJ39ZJeFY0xQzkk/LMg==} + telegram@2.26.22: dependencies: '@cryptography/aes': 0.1.1 async-mutex: 0.3.2 @@ -8793,678 +11720,378 @@ packages: node-localstorage: 2.2.1 pako: 2.1.0 path-browserify: 1.0.1 - real-cancellable-promise: 1.2.0 - socks: 2.8.1 - store2: 2.14.3 + real-cancellable-promise: 1.2.1 + socks: 2.8.4 + store2: 2.14.4 ts-custom-error: 3.3.1 - websocket: 1.0.34 + websocket: 1.0.35 optionalDependencies: - bufferutil: 4.0.8 + bufferutil: 4.0.9 utf-8-validate: 5.0.10 transitivePeerDependencies: - supports-color - dev: false - /test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - dev: true + glob: 10.4.5 + minimatch: 9.0.5 - /text-hex@1.0.0: - resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - dev: false + text-decoder@1.2.3: + dependencies: + b4a: 1.6.7 - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true + text-hex@1.0.0: {} - /thirty-two@1.0.2: - resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} - engines: {node: '>=0.2.6'} - dev: false + text-table@0.2.0: {} + + thirty-two@1.0.2: {} - /thread-stream@2.4.1: - resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==} + thread-stream@3.1.0: dependencies: real-require: 0.2.0 - dev: false - /through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + through@2.3.8: {} - /tiny-async-pool@2.1.0: - resolution: {integrity: sha512-ltAHPh/9k0STRQqaoUX52NH4ZQYAJz24ZAEwf1Zm+HYg3l9OXTWeqWKyYsHu40wF/F0rxd2N2bk5sLvX2qlSvg==} - dev: false + tiny-async-pool@2.1.0: {} - /tinybench@2.6.0: - resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} - dev: true + tinybench@2.9.0: {} - /tinypool@0.8.3: - resolution: {integrity: sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==} - engines: {node: '>=14.0.0'} - dev: true + tinyexec@0.3.2: {} - /tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} - engines: {node: '>=14.0.0'} - dev: true + tinypool@1.0.2: {} - /title@3.5.3: - resolution: {integrity: sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==} - hasBin: true - dependencies: - arg: 1.0.0 - chalk: 2.3.0 - clipboardy: 1.2.2 - titleize: 1.0.0 - dev: false + tinyrainbow@1.2.0: {} - /titleize@1.0.0: - resolution: {integrity: sha512-TARUb7z1pGvlLxgPk++7wJ6aycXF3GJ0sNSBTAsTuJrQG5QuZlkUQP+zl+nbjAh4gMX9yDw9ZYklMd7vAfJKEw==} - engines: {node: '>=0.10.0'} - dev: false + tinyspy@3.0.2: {} - /tlds@1.250.0: - resolution: {integrity: sha512-rWsBfFCWKrjM/o2Q1TTUeYQv6tHSd/umUutDjVs6taTuEgRDIreVYIBgWRWW4ot7jp6n0UVUuxhTLWBtUmPu/w==} - hasBin: true - dev: false + title@4.0.1: + dependencies: + arg: 5.0.2 + chalk: 5.4.1 + clipboardy: 4.0.0 - /tlds@1.252.0: - resolution: {integrity: sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==} - hasBin: true - dev: false + tlds@1.255.0: {} - /tldts-core@6.1.18: - resolution: {integrity: sha512-e4wx32F/7dMBSZyKAx825Yte3U0PQtZZ0bkWxYQiwLteRVnQ5zM40fEbi0IyNtwQssgJAk3GCr7Q+w39hX0VKA==} - dev: false + tlds@1.256.0: {} - /tldts@6.1.18: - resolution: {integrity: sha512-F+6zjPFnFxZ0h6uGb8neQWwHQm8u3orZVFribsGq4eBgEVrzSkHxzWS2l6aKr19T1vXiOMFjqfff4fQt+WgJFg==} - hasBin: true + tldts-core@6.1.85: {} + + tldts@6.1.85: dependencies: - tldts-core: 6.1.18 - dev: false + tldts-core: 6.1.85 - /tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 - dev: true - - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - dev: true - /to-no-case@1.0.2: - resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} - dev: false + to-no-case@1.0.2: {} - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - dev: true - /to-snake-case@1.0.0: - resolution: {integrity: sha512-joRpzBAk1Bhi2eGEYBjukEWHOe/IvclOkiJl3DtA91jV6NwQ3MwXA4FHYeqk8BNp/D8bmi9tcNbRu/SozP0jbQ==} + to-snake-case@1.0.0: dependencies: to-space-case: 1.0.0 - dev: false - /to-space-case@1.0.0: - resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + to-space-case@1.0.0: dependencies: to-no-case: 1.0.2 - dev: false - /tosource@2.0.0-alpha.3: - resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} - engines: {node: '>=10'} - dev: false + tosource@2.0.0-alpha.3: {} - /tough-cookie@2.5.0: - resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} - engines: {node: '>=0.8'} + tough-cookie@2.5.0: dependencies: - psl: 1.9.0 + psl: 1.15.0 punycode: 2.3.1 - dev: false - - /tough-cookie@4.1.4: - resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} - engines: {node: '>=6'} + + tough-cookie@4.1.4: dependencies: - psl: 1.9.0 + psl: 1.15.0 punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10 - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.85 - /tr46@5.0.0: - resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} - engines: {node: '>=18'} + tr46@0.0.3: {} + + tr46@5.1.0: dependencies: punycode: 2.3.1 - /triple-beam@1.4.1: - resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} - engines: {node: '>= 14.0.0'} - dev: false + triple-beam@1.4.1: {} - /trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - dev: true + trough@2.2.0: {} - /ts-api-utils@1.3.0(typescript@5.4.5): - resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' + ts-api-utils@2.1.0(typescript@5.8.2): dependencies: - typescript: 5.4.5 - dev: true + typescript: 5.8.2 - /ts-custom-error@2.2.2: - resolution: {integrity: sha512-I0FEdfdatDjeigRqh1JFj67bcIKyRNm12UVGheBjs2pXgyELg2xeiQLVaWu1pVmNGXZVnz/fvycSU41moBIpOg==} - engines: {node: '>=8.0.0'} - deprecated: npm package tarball contains useless codeclimate-reporter binary, please update to version 3.1.1. See https://github.com/adriengibrat/ts-custom-error/issues/32 - dev: false + ts-case-convert@2.1.0: {} - /ts-custom-error@3.3.1: - resolution: {integrity: sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==} - engines: {node: '>=14.0.0'} - dev: false + ts-custom-error@2.2.2: {} - /ts-xor@1.3.0: - resolution: {integrity: sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA==} - dev: false + ts-custom-error@3.3.1: {} - /tsconfck@3.0.3(typescript@5.4.5): - resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - dependencies: - typescript: 5.4.5 - dev: true + ts-xor@1.3.0: {} - /tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: false + tsconfck@3.1.5(typescript@5.8.2): + optionalDependencies: + typescript: 5.8.2 - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@1.14.1: {} - /tsx@4.8.2: - resolution: {integrity: sha512-hmmzS4U4mdy1Cnzpl/NQiPUC2k34EcNSTZYVJThYKhdqTwuBeF+4cG9KUK/PFQ7KHaAaYwqlb7QfmsE2nuj+WA==} - engines: {node: '>=18.0.0'} - hasBin: true + tslib@2.8.1: {} + + tsx@4.19.3: dependencies: - esbuild: 0.20.2 - get-tsconfig: 4.7.3 + esbuild: 0.25.1 + get-tsconfig: 4.10.0 optionalDependencies: fsevents: 2.3.3 - dev: false - /tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tunnel-agent@0.6.0: dependencies: - safe-buffer: 5.2.1 - dev: false + safe-buffer: '@nolyfill/safe-buffer@1.0.44' - /turndown@7.1.2: - resolution: {integrity: sha512-ntI9R7fcUKjqBP6QU8rBK2Ehyt8LAzt3UBT9JR9tgo6GtuKvyUzpayWmeMKJw1DPdXzktvtIT8m2mVXz+bL/Qg==} + turndown@7.2.0: dependencies: - domino: 2.1.6 - dev: false + '@mixmark-io/domino': 2.2.0 - /tweetnacl@0.14.5: - resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} - dev: false + tweetnacl@0.14.5: {} - /twitter-api-v2@1.16.3: - resolution: {integrity: sha512-T9Wbq1y3IrTshBvtVawpWp1GmL3mduF4RhieSfM1HsL10Qda0EeJMDUwr5C6IeelnSZ65tiiXzs0xUS8FHM9XA==} - dev: false + twitter-api-v2@1.22.0: {} - /type-check@0.3.2: - resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} - engines: {node: '>= 0.8.0'} + type-check@0.3.2: dependencies: prelude-ls: 1.1.2 - dev: false - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - dev: true - - /type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - dev: true - - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - /type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - dev: true - - /type-fest@0.6.0: - resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} - engines: {node: '>=8'} - dev: true - - /type-fest@0.8.1: - resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} - engines: {node: '>=8'} - dev: true - - /type-fest@1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} - dev: false + type-fest@0.20.2: {} - /type-fest@3.13.1: - resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} - engines: {node: '>=14.16'} - dev: true + type-fest@0.21.3: {} - /type-fest@4.14.0: - resolution: {integrity: sha512-on5/Cw89wwqGZQu+yWO0gGMGu8VNxsaW9SB2HE8yJjllEk7IDTwnSN1dUVldYILhYPN5HzD7WAaw2cc/jBfn0Q==} - engines: {node: '>=16'} - dev: true + type-fest@1.4.0: {} - /type@1.2.0: - resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==} - dev: false + type-fest@4.38.0: {} - /type@2.7.2: - resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} - dev: false + type@2.7.3: {} - /typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + typedarray-to-buffer@3.1.5: dependencies: is-typedarray: 1.0.0 - dev: false - /typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} - engines: {node: '>=14.17'} - hasBin: true + typescript@5.8.2: {} - /uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - dev: false + uc.micro@2.1.0: {} - /ufo@1.5.3: - resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + ufo@1.5.4: {} - /uglify-js@3.4.10: - resolution: {integrity: sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==} - engines: {node: '>=0.8.0'} - hasBin: true - dependencies: - commander: 2.19.0 - source-map: 0.6.1 - dev: false + uglify-js@3.19.3: {} - /unbzip2-stream@1.4.3: - resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + unbzip2-stream@1.4.3: dependencies: buffer: 5.7.1 through: 2.3.8 - dev: false - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.20.0: {} - /undici@6.15.0: - resolution: {integrity: sha512-VviMt2tlMg1BvQ0FKXxrz1eJuyrcISrL2sPfBf7ZskX/FCEc/7LeThQaoygsMJpNqrATWQIsRVx+1Dpe4jaYuQ==} - engines: {node: '>=18.17'} - dev: false + undici@6.21.2: {} - /unicode-canonical-property-names-ecmascript@2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} - dev: true + unicode-canonical-property-names-ecmascript@2.0.1: {} - /unicode-match-property-ecmascript@2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} + unicode-match-property-ecmascript@2.0.0: dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-canonical-property-names-ecmascript: 2.0.1 unicode-property-aliases-ecmascript: 2.1.0 - dev: true - /unicode-match-property-value-ecmascript@2.1.0: - resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} - engines: {node: '>=4'} - dev: true + unicode-match-property-value-ecmascript@2.2.0: {} - /unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} - engines: {node: '>=4'} - dev: true + unicode-property-aliases-ecmascript@2.1.0: {} + + unicorn-magic@0.1.0: {} - /unified@11.0.4: - resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} + unified@11.0.5: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 bail: 2.0.2 devlop: 1.1.0 extend: 3.0.2 is-plain-obj: 4.1.0 trough: 2.2.0 - vfile: 6.0.1 - dev: true + vfile: 6.0.3 - /unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + unist-util-stringify-position@4.0.0: dependencies: - '@types/unist': 3.0.2 - dev: true + '@types/unist': 3.0.3 - /universalify@0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} + universalify@0.2.0: {} - /universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} + universalify@2.0.1: {} - /update-browserslist-db@1.0.13(browserslist@4.23.0): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + update-browserslist-db@1.1.3(browserslist@4.24.4): dependencies: - browserslist: 4.23.0 - escalade: 3.1.2 - picocolors: 1.0.0 - dev: true + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 - /upper-case@1.1.3: - resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} - dev: false + upper-case@1.1.3: {} - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uri-js@4.4.1: dependencies: punycode: 2.3.1 - /url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + url-parse@1.5.10: dependencies: querystringify: 2.2.0 requires-port: 1.0.0 - /url-regex-safe@3.0.0: - resolution: {integrity: sha512-+2U40NrcmtWFVjuxXVt9bGRw6c7/MgkGKN9xIfPrT/2RX0LTkkae6CCEDp93xqUN0UKm/rr821QnHd2dHQmN3A==} - engines: {node: '>= 10.12.0'} - peerDependencies: - re2: ^1.17.2 - peerDependenciesMeta: - re2: - optional: true + url-regex-safe@3.0.0: dependencies: ip-regex: 4.3.0 - tlds: 1.250.0 - dev: false + tlds: 1.256.0 - /url-template@2.0.8: - resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} - dev: false + url-template@2.0.8: {} - /urlpattern-polyfill@10.0.0: - resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} - dev: false + urlpattern-polyfill@10.0.0: {} - /utf-8-validate@5.0.10: - resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} - engines: {node: '>=6.14.2'} - requiresBuild: true + utf-8-validate@5.0.10: dependencies: - node-gyp-build: 4.8.0 - dev: false + node-gyp-build: 4.8.4 - /utf8@3.0.0: - resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} - dev: true + utf8@3.0.0: {} - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util-deprecate@1.0.2: {} - /utility-types@3.11.0: - resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} - engines: {node: '>= 4'} - dev: false + utility-types@3.11.0: {} - /uuid@3.4.0: - resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. - hasBin: true - dev: false + uuid@11.1.0: {} - /uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - dev: false + uuid@3.4.0: {} - /uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - dev: false + uuid@8.3.2: {} - /valid-url@1.0.9: - resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} - dev: false + uuid@9.0.1: {} - /validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + valid-url@1.0.9: {} + + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - dev: true - /verror@1.10.0: - resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} - engines: {'0': node >=0.6.0} + verror@1.10.0: dependencies: assert-plus: 1.0.0 core-util-is: 1.0.2 extsprintf: 1.3.0 - dev: false - /vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + vfile-message@4.0.2: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 - dev: true - /vfile@6.0.1: - resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} + vfile@6.0.3: dependencies: - '@types/unist': 3.0.2 - unist-util-stringify-position: 4.0.0 + '@types/unist': 3.0.3 vfile-message: 4.0.2 - dev: true - /vite-node@1.5.3(@types/node@20.12.8): - resolution: {integrity: sha512-axFo00qiCpU/JLd8N1gu9iEYL3xTbMbMrbe5nDp9GL0nb6gurIdZLkkFogZXWnE8Oyy5kfSLwNVIcVsnhE7lgQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true + vite-node@2.1.9(@types/node@22.13.15): dependencies: cac: 6.7.14 - debug: 4.3.4 + debug: 4.4.0 + es-module-lexer: 1.6.0 pathe: 1.1.2 - picocolors: 1.0.0 - vite: 5.2.9(@types/node@20.12.8) + vite: 5.4.15(@types/node@22.13.15) transitivePeerDependencies: - '@types/node' - less - lightningcss - sass + - sass-embedded - stylus - sugarss - supports-color - terser - dev: true - /vite-tsconfig-paths@4.3.2(typescript@5.4.5): - resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} - peerDependencies: - vite: '*' - peerDependenciesMeta: - vite: - optional: true + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@5.4.15(@types/node@22.13.15)): dependencies: - debug: 4.3.4 + debug: 4.4.0 globrex: 0.1.2 - tsconfck: 3.0.3(typescript@5.4.5) + tsconfck: 3.1.5(typescript@5.8.2) + optionalDependencies: + vite: 5.4.15(@types/node@22.13.15) transitivePeerDependencies: - supports-color - typescript - dev: true - /vite@5.2.9(@types/node@20.12.8): - resolution: {integrity: sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true + vite@5.4.15(@types/node@22.13.15): dependencies: - '@types/node': 20.12.8 - esbuild: 0.20.2 - postcss: 8.4.38 - rollup: 4.14.3 + esbuild: 0.21.5 + postcss: 8.5.3 + rollup: 4.37.0 optionalDependencies: + '@types/node': 22.13.15 fsevents: 2.3.3 - dev: true - /vitest@1.5.3(@types/node@20.12.8)(jsdom@24.0.0): - resolution: {integrity: sha512-2oM7nLXylw3mQlW6GXnRriw+7YvZFk/YNV8AxIC3Z3MfFbuziLGWP9GPxxu/7nRlXhqyxBikpamr+lEEj1sUEw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.5.3 - '@vitest/ui': 1.5.3 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - dependencies: - '@types/node': 20.12.8 - '@vitest/expect': 1.5.3 - '@vitest/runner': 1.5.3 - '@vitest/snapshot': 1.5.3 - '@vitest/spy': 1.5.3 - '@vitest/utils': 1.5.3 - acorn-walk: 8.3.2 - chai: 4.4.1 - debug: 4.3.4 - execa: 8.0.1 - jsdom: 24.0.0 - local-pkg: 0.5.0 - magic-string: 0.30.8 + vitest@2.1.9(@types/node@22.13.15)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.4.3(typescript@5.8.2)): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(msw@2.4.3(typescript@5.8.2))(vite@5.4.15(@types/node@22.13.15)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.0 + expect-type: 1.2.0 + magic-string: 0.30.17 pathe: 1.1.2 - picocolors: 1.0.0 - std-env: 3.7.0 - strip-literal: 2.0.0 - tinybench: 2.6.0 - tinypool: 0.8.3 - vite: 5.2.9(@types/node@20.12.8) - vite-node: 1.5.3(@types/node@20.12.8) - why-is-node-running: 2.2.2 + std-env: 3.8.1 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.15(@types/node@22.13.15) + vite-node: 2.1.9(@types/node@22.13.15) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.13.15 + jsdom: 26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - less - lightningcss + - msw - sass + - sass-embedded - stylus - sugarss - supports-color - terser - dev: true - /w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + wcwidth@1.0.1: dependencies: defaults: 1.0.4 - dev: true - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@3.0.1: {} - /webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} + webidl-conversions@7.0.0: {} - /websocket@1.0.34: - resolution: {integrity: sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==} - engines: {node: '>=4.0.0'} + websocket@1.0.35: dependencies: - bufferutil: 4.0.8 + bufferutil: 4.0.9 debug: 2.6.9 es5-ext: 0.10.64 typedarray-to-buffer: 3.1.5 @@ -9472,271 +12099,160 @@ packages: yaeti: 0.0.6 transitivePeerDependencies: - supports-color - dev: false - /whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 - /whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} + whatwg-mimetype@4.0.0: {} - /whatwg-url@14.0.0: - resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} - engines: {node: '>=18'} + whatwg-url@14.2.0: dependencies: - tr46: 5.0.0 + tr46: 5.1.0 webidl-conversions: 7.0.0 - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - /which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: false - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + which@2.0.2: dependencies: isexe: 2.0.0 - /why-is-node-running@2.2.2: - resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} - engines: {node: '>=8'} - hasBin: true + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - dev: true - - /wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - dependencies: - string-width: 4.2.3 - dev: true - /winston-transport@4.7.0: - resolution: {integrity: sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==} - engines: {node: '>= 12.0.0'} + winston-transport@4.9.0: dependencies: - logform: 2.6.0 + logform: 2.7.0 readable-stream: 3.6.2 triple-beam: 1.4.1 - dev: false - /winston@3.13.0: - resolution: {integrity: sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==} - engines: {node: '>= 12.0.0'} + winston@3.17.0: dependencies: '@colors/colors': 1.6.0 '@dabh/diagnostics': 2.0.3 - async: 3.2.5 + async: 3.2.6 is-stream: 2.0.1 - logform: 2.6.0 + logform: 2.7.0 one-time: 1.0.0 readable-stream: 3.6.2 - safe-stable-stringify: 2.4.3 + safe-stable-stringify: 2.5.0 stack-trace: 0.0.10 triple-beam: 1.4.1 - winston-transport: 4.7.0 - dev: false + winston-transport: 4.9.0 - /word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - dev: false + word-wrap@1.2.5: {} - /wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} + wrap-ansi@8.1.0: dependencies: ansi-styles: 6.2.1 string-width: 5.1.2 strip-ansi: 7.1.0 - dev: true - /wrap-ansi@9.0.0: - resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} - engines: {node: '>=18'} + wrap-ansi@9.0.0: dependencies: ansi-styles: 6.2.1 - string-width: 7.1.0 + string-width: 7.2.0 strip-ansi: 7.1.0 - dev: true - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + wrappy@1.0.2: {} - /write-file-atomic@1.3.4: - resolution: {integrity: sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==} + write-file-atomic@1.3.4: dependencies: graceful-fs: 4.2.11 imurmurhash: 0.1.4 slide: 1.1.6 - dev: false - /ws@8.16.0: - resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true + ws@8.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 - /wuzzy@0.1.8: - resolution: {integrity: sha512-FUzKQepFSTnANsDYwxpIzGJ/dIJaqxuMre6tzzbvWwFAiUHPsI1nVQVCLK4Xqr67KO7oYAK0kaCcI/+WYj/7JA==} + ws@8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + wuzzy@0.1.8: dependencies: lodash: 4.17.21 - dev: false - /xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} + xml-name-validator@5.0.0: {} - /xml2js@0.5.0: - resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} - engines: {node: '>=4.0.0'} + xml2js@0.5.0: dependencies: - sax: 1.3.0 + sax: 1.4.1 xmlbuilder: 11.0.1 - dev: false - /xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - dev: false + xmlbuilder@11.0.1: {} - /xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlchars@2.2.0: {} - /xxhash-wasm@1.0.2: - resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==} - dev: false + xtend@4.0.2: {} - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} + xxhash-wasm@1.1.0: {} - /yaeti@0.0.6: - resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==} - engines: {node: '>=0.10.32'} - dev: false + y18n@5.0.8: {} - /yallist@2.1.2: - resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} - dev: false + yaeti@0.0.6: {} - /yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true + yallist@3.1.1: {} - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@4.0.0: {} - /yaml-eslint-parser@1.2.2: - resolution: {integrity: sha512-pEwzfsKbTrB8G3xc/sN7aw1v6A6c/pKxLAkjclnAyo5g5qOh6eL9WGu0o3cSDQZKrTNk4KL4lQSwZW+nBkANEg==} - engines: {node: ^14.17.0 || >=16.0.0} + yallist@5.0.0: {} + + yaml-eslint-parser@1.3.0: dependencies: eslint-visitor-keys: 3.4.3 - lodash: 4.17.21 - yaml: 2.3.4 - dev: true - - /yaml@2.3.4: - resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} - engines: {node: '>= 14'} - dev: true + yaml: 2.7.0 - /yaml@2.4.1: - resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} - engines: {node: '>= 14'} - hasBin: true - dev: false + yaml@2.7.0: {} - /yargs-parser@15.0.3: - resolution: {integrity: sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==} + yargs-parser@15.0.3: dependencies: camelcase: 5.3.1 decamelize: 1.2.0 - dev: false - /yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} + yargs-parser@21.1.1: {} - /yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + yargs@17.7.2: dependencies: cliui: 8.0.1 - escalade: 3.1.2 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - /yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yauzl@2.10.0: dependencies: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 - dev: false - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true + yocto-queue@0.1.0: {} - /yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} - dev: true + yoctocolors-cjs@2.1.2: {} - /zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} - dev: false + zhead@2.2.4: {} - /zod@3.23.5: - resolution: {integrity: sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==} - dev: false + zod@3.22.4: {} - github.com/postlight/difflib.js/32e8e38c7fcd935241b9baab71bb432fd9b166ed: - resolution: {tarball: https://codeload.github.com/postlight/difflib.js/tar.gz/32e8e38c7fcd935241b9baab71bb432fd9b166ed} - name: difflib - version: 0.2.6 - dependencies: - heap: 0.2.7 - dev: false + zod@3.24.2: {} diff --git a/scripts/workflow/build-docs.ts b/scripts/workflow/build-docs.ts index c2faf99f9fafd5..8aa2a39f256efe 100644 --- a/scripts/workflow/build-docs.ts +++ b/scripts/workflow/build-docs.ts @@ -96,8 +96,17 @@ function generateMd(lang) { } }); + const processedPaths = new Set(); + for (const realPath of realPaths) { const data = docs[category][namespace].routes[realPath]; + if (Array.isArray(data.path)) { + if (processedPaths.has(data.path[0])) { + continue; + } + processedPaths.add(data.path[0]); + } + const test = testResult.find((t) => t.title === realPath); const parsedTest = test ? { diff --git a/scripts/workflow/data.ts b/scripts/workflow/data.ts index dcf4cf8ffe5b87..24726828f79818 100644 --- a/scripts/workflow/data.ts +++ b/scripts/workflow/data.ts @@ -1,4 +1,10 @@ export const categories = [ + { + icon: '🌟', + link: '/routes/popular', + en: 'Popular', + zh: '热门', + }, { icon: '💬', link: '/routes/social-media', diff --git a/scripts/workflow/test-route/identify.mjs b/scripts/workflow/test-route/identify.mjs index 4aa342d8c4cd12..01ffa6293ee640 100644 --- a/scripts/workflow/test-route/identify.mjs +++ b/scripts/workflow/test-route/identify.mjs @@ -6,8 +6,8 @@ export default async function identify({ github, context, core }, body, number, core.debug(`sender: ${sender}`); core.debug(`body: ${body}`); // Remove all HTML comments before performing the match - const bodyNoCmts = body.replaceAll(//g, ''); - const m = bodyNoCmts.match(/```routes\s+([\S\s]*?)```/); + const bodyNoCmts = body?.replaceAll(//g, ''); + const m = bodyNoCmts?.match(/```routes\s+([\S\s]*?)```/); core.debug(`match: ${m}`); let routes = null; @@ -21,6 +21,13 @@ export default async function identify({ github, context, core }, body, number, repo: context.repo.repo, pull_number: number, }; + const { data: issue } = await github.rest.issues + .get({ + ...issueFacts, + }) + .catch((error) => { + core.warning(error); + }); const addLabels = (labels) => github.rest.issues @@ -31,7 +38,6 @@ export default async function identify({ github, context, core }, body, number, .catch((error) => { core.warning(error); }); - const removeLabel = (labelName = noFound) => github.rest.issues .removeLabel({ @@ -41,7 +47,6 @@ export default async function identify({ github, context, core }, body, number, .catch((error) => { core.warning(error); }); - const updatePrState = (state) => github.rest.pulls .update({ @@ -51,7 +56,6 @@ export default async function identify({ github, context, core }, body, number, .catch((error) => { core.warning(error); }); - const createComment = (body) => github.rest.issues .createComment({ @@ -61,7 +65,6 @@ export default async function identify({ github, context, core }, body, number, .catch((error) => { core.warning(error); }); - const createFailedComment = () => { const logUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; @@ -74,18 +77,11 @@ export default async function identify({ github, context, core }, body, number, 路由测试失败,请确认评论部分符合格式规范,详情请检查 [日志](${logUrl})。`); }; - const pr = await github.rest.issues - .get({ - ...issueFacts, - }) - .catch((error) => { - core.warning(error); - }); - if (pr.pull_request) { - if (pr.state === 'closed') { + if (issue.pull_request) { + if (issue.state === 'closed') { await updatePrState('open'); } - if (pr.labels.some((e) => e.name === testFailed)) { + if (issue.labels.some((e) => e.name === testFailed)) { await removeLabel(testFailed); } } diff --git a/scripts/workflow/test-route/test.mjs b/scripts/workflow/test-route/test.mjs index 8fc1966f1bffee..58cb40345dcfbe 100644 --- a/scripts/workflow/test-route/test.mjs +++ b/scripts/workflow/test-route/test.mjs @@ -31,14 +31,17 @@ export default async function test({ github, context, core }, baseUrl, routes, n detail += '\n\n'; detail += errInfoList .slice(0, 5) - .map((e) => (e.length > 1000 ? e.slice(0, 1000) + '...' : e).trim()) + .map((e) => { + e = e.replaceAll(/|<\/code>/g, '').trim(); + return e.length > 1000 ? e.slice(0, 1000) + '...' : e; + }) .join('\n'); } } let routeFeedback = `

    -${lks} - ${success ? 'Success ✔️' : 'Failed ❌'} +${lks.replaceAll('&', '&')} - ${success ? 'Success ✔️' : 'Failed ❌'} \`\`\`${success ? 'rss' : ''}`; routeFeedback += `