diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5a3ccf02dd99ff..77026d75ece61f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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" ] } 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 2db3e27fd7e5f7..72d8543ed327e7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ updates: schedule: interval: daily time: '08:00' - open-pull-requests-limit: 10 + open-pull-requests-limit: 100 labels: - dependencies ignore: @@ -17,6 +17,6 @@ updates: schedule: interval: daily time: '08:00' - open-pull-requests-limit: 10 + open-pull-requests-limit: 100 labels: - dependencies diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml index a790b82fa0ad8e..e824ab20547146 100644 --- a/.github/workflows/build-assets.yml +++ b/.github/workflows/build-assets.yml @@ -8,14 +8,11 @@ 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: @@ -43,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 + 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/comment-on-issue.yml b/.github/workflows/comment-on-issue.yml index c80d16de9ebd7c..f0fdb344784301 100644 --- a/.github/workflows/comment-on-issue.yml +++ b/.github/workflows/comment-on-issue.yml @@ -9,6 +9,8 @@ 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 diff --git a/.github/workflows/dependabot-fork.yml b/.github/workflows/dependabot-fork.yml index b39ec2ae7cd20f..baeeff107b6ca8 100644 --- a/.github/workflows/dependabot-fork.yml +++ b/.github/workflows/dependabot-fork.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v4 - name: Comment Dependabot PR - uses: thollander/actions-comment-pull-request@v2 + uses: thollander/actions-comment-pull-request@v3 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..1d8c5b6bd546d6 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -29,10 +29,9 @@ 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 @@ -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@v6 with: context: . push: true @@ -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@v6 with: context: . build-args: PUPPETEER_SKIP_DOWNLOAD=0 diff --git a/.github/workflows/docker-test-cont.yml b/.github/workflows/docker-test-cont.yml index b2aacb695d03c0..be3c207be45aa4 100644 --- a/.github/workflows/docker-test-cont.yml +++ b/.github/workflows/docker-test-cont.yml @@ -9,6 +9,8 @@ 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 @@ -49,16 +51,19 @@ jobs: - name: Fetch Docker image if: (env.TEST_CONTINUE) - uses: dawidd6/action-download-artifact@v3 + uses: dawidd6/action-download-artifact@v7 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 \ diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 2f3031e84921a5..79144429814db9 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,8 +20,11 @@ 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 @@ -41,7 +40,7 @@ jobs: flavor: latest=true - name: Build Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . build-args: PUPPETEER_SKIP_DOWNLOAD=0 # also test bundling Chromium diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index a1faa0400eb658..a2ca9899fa5b32 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -5,9 +5,6 @@ on: branches: - master -permissions: - contents: read - jobs: format: permissions: diff --git a/.github/workflows/issue-command.yml b/.github/workflows/issue-command.yml index 6bdd3c421cf5e9..b80c2b5ea2dfa4 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 @@ -24,7 +21,7 @@ jobs: - 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,7 +29,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 permissions: - contents: read issues: write steps: - uses: bdougie/take-action@v1.6.1 @@ -46,12 +42,31 @@ 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@v2.x + 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 + if: ${{ !github.event.issue.pull_request }} uses: actions/checkout@v4 + - name: Checkout PR + if: github.event.issue.pull_request + uses: actions/checkout@v4 + with: + ref: ${{ fromJson(steps.pr-data.outputs.data).head.ref }} + - name: Install pnpm uses: pnpm/action-setup@v4 @@ -109,7 +124,7 @@ 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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3be6542dccc234..4f317a38646bd1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,7 +36,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 permissions: - contents: read security-events: write steps: - uses: actions/checkout@v4 @@ -63,8 +62,6 @@ jobs: name: Validate PR title runs-on: ubuntu-latest timeout-minutes: 5 - permissions: - pull-requests: read steps: - uses: amannn/action-semantic-pull-request@v5 env: @@ -75,9 +72,8 @@ 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 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 2530deb366721e..bd6666ae3af8f8 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: @@ -25,8 +24,7 @@ jobs: - uses: pnpm/action-setup@v4 - 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..5fb469de005d95 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,7 +20,6 @@ jobs: image: returntocorp/semgrep if: (github.triggering_actor != 'dependabot[bot]') permissions: - contents: read security-events: write steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test-full-routes.yml b/.github/workflows/test-full-routes.yml index 6546f81cc2c59f..ce46651922ba53 100644 --- a/.github/workflows/test-full-routes.yml +++ b/.github/workflows/test-full-routes.yml @@ -5,14 +5,11 @@ 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: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2193ae18693e64..f335abdebb7725 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,34 +12,9 @@ on: pull_request: {} permissions: - contents: read + checks: write jobs: - fix-pnpm-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@v4 - - 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' - commit_user_name: dependabot[bot] - commit_user_email: 49699333+dependabot[bot]@users.noreply.github.com - commit_author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - vitest: runs-on: ubuntu-latest timeout-minutes: 10 @@ -52,7 +27,7 @@ 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 @@ -68,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 @@ -83,7 +58,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [ 20, 22 ] + node-version: [ latest, lts/*, lts/-1 ] chromium: - name: bundled Chromium dependency: '' @@ -135,10 +110,12 @@ 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 @@ -150,6 +127,11 @@ jobs: - run: pnpm i - name: Build radar and maintainer run: npm run build + - name: Upload assets + uses: actions/upload-artifact@v4 + with: + name: generated-assets-${{ matrix.node-version }} + path: assets/build/ automerge: if: github.triggering_actor == 'dependabot[bot]' && github.event_name == 'pull_request' 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/Dockerfile b/Dockerfile index 1c6d64fa90d1df..9089f0ca955d48 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,7 @@ WORKDIR /app ARG USE_CHINA_NPM_REGISTRY=0 RUN \ set -ex && \ + corepack enable pnpm && \ if [ "$USE_CHINA_NPM_REGISTRY" = 1 ]; then \ echo 'use npm mirror' && \ npm config set registry https://registry.npmmirror.com && \ @@ -23,7 +24,6 @@ COPY ./package.json /app/ RUN \ set -ex && \ export PUPPETEER_SKIP_DOWNLOAD=true && \ - corepack enable pnpm && \ pnpm install --frozen-lockfile && \ pnpm rb @@ -33,7 +33,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 +45,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 +79,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. @@ -111,12 +111,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 +132,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 debaf1bff5cd97..defee8667f8dac 100644 --- a/README.md +++ b/README.md @@ -10,30 +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://x.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://x.com/intent/follow?screen_name=_RSSHub) | [中文文档](https://docs.rsshub.app/zh/) - -## Special Thanks - -### 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 @@ -42,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://x.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..b7137de1118c2d --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,276 @@ +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 globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import parser 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, + allConfig: js.configs.all +}); + +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:n/recommended', + 'plugin:unicorn/recommended', + 'plugin:prettier/recommended', + 'plugin:yml/recommended', + 'plugin:@typescript-eslint/recommended', +), { + plugins: { + prettier, + '@stylistic': stylistic, + unicorn, + '@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-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, + }, + + 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/index.ts b/lib/api/index.ts index bf6c16694a6fe3..71b8d0c75dd152 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -3,8 +3,9 @@ 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 { OpenAPIHono } from '@hono/zod-openapi'; -import { swaggerUI } from '@hono/swagger-ui'; +import { apiReference } from '@scalar/hono-api-reference'; const app = new OpenAPIHono(); @@ -12,6 +13,7 @@ app.openapi(namespaceAllRoute, namespaceAllHandler); app.openapi(namespaceOneRoute, namespaceOneHandler); app.openapi(radarRulesAllRoute, radarRulesAllHandler); app.openapi(radarRulesOneRoute, radarRulesOneHandler); +app.openapi(categoryOneRoute, categoryOneHandler); const docs = app.getOpenAPI31Document({ openapi: '3.1.0', @@ -24,8 +26,12 @@ 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({ + spec: { 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 942427e59cb80a..5967249cbe9340 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,16 @@ export type Config = { temperature?: number; maxTokens?: number; endpoint: string; - prompt?: string; + inputOption: string; + promptTitle: string; + promptDescription: string; }; + + // Route-specific Configurations bilibili: { cookies: Record; dmImgList?: string; + dmImgInter?: string; }; bitbucket: { username?: string; @@ -87,9 +104,15 @@ export type Config = { bupt: { portal_cookie?: string; }; + caixin: { + cookie?: string; + }; civitai: { cookie?: string; }; + dianping: { + cookie?: string; + }; dida365: { username?: string; password?: string; @@ -144,6 +167,9 @@ export type Config = { google: { fontsApiKey?: string; }; + guozaoke: { + cookies?: string; + }; hefeng: { key?: string; }; @@ -154,7 +180,6 @@ export type Config = { username?: string; password?: string; bearertoken?: string; - iap_receipt?: string; }; instagram: { username?: string; @@ -169,12 +194,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; }; @@ -214,6 +252,9 @@ export type Config = { notion: { key?: string; }; + patreon: { + sessionId?: string; + }; pianyuan: { cookie?: string; }; @@ -233,6 +274,9 @@ export type Config = { qingting: { id?: string; }; + readwise: { + accessToken?: string; + }; saraba1st: { cookie?: string; }; @@ -245,24 +289,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,9 +352,16 @@ export type Config = { device_id?: string; refresh_token?: string; }; + xiaohongshu: { + cookie?: string; + }; ximalaya: { token?: string; }; + xsijishe: { + cookie?: string; + userAgent?: string; + }; xueqiu: { cookies?: string; }; @@ -364,7 +443,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: { @@ -388,8 +466,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 @@ -399,11 +477,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, @@ -423,13 +506,16 @@ 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.', }, // Route-specific Configurations bilibili: { cookies: bilibili_cookies, dmImgList: envs.BILIBILI_DM_IMG_LIST, + dmImgInter: envs.BILIBILI_DM_IMG_INTER, }, bitbucket: { username: envs.BITBUCKET_USERNAME, @@ -442,9 +528,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, @@ -499,6 +591,9 @@ const calculateValue = () => { google: { fontsApiKey: envs.GOOGLE_FONTS_API_KEY, }, + guozaoke: { + cookies: envs.GUOZAOKE_COOKIES, + }, hefeng: { // weather key: envs.HEFENG_KEY, @@ -510,7 +605,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, @@ -525,12 +619,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, }, @@ -570,6 +677,9 @@ const calculateValue = () => { notion: { key: envs.NOTION_TOKEN, }, + patreon: { + sessionId: envs.PATREON_SESSION_ID, + }, pianyuan: { cookie: envs.PIANYUAN_COOKIE, }, @@ -589,6 +699,9 @@ const calculateValue = () => { qingting: { id: envs.QINGTING_ID, }, + readwise: { + accessToken: envs.READWISE_ACCESS_TOKEN, + }, saraba1st: { cookie: envs.SARABA1ST_COOKIE, }, @@ -601,28 +714,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, @@ -640,9 +777,16 @@ 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, }, diff --git a/lib/errors/index.test.ts b/lib/errors/index.test.ts index c2dd36edeb44c8..e60fa114ac8328 100644 --- a/lib/errors/index.test.ts +++ b/lib/errors/index.test.ts @@ -22,7 +22,7 @@ describe('httperror', () => { it(`httperror`, async () => { const response = await request.get('/test/httperror'); expect(response.status).toBe(503); - expect(response.text).toMatch('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); }); diff --git a/lib/errors/index.tsx b/lib/errors/index.tsx index dd996a7f4fdce8..0ac4027939b322 100644 --- a/lib/errors/index.tsx +++ b/lib/errors/index.tsx @@ -7,14 +7,20 @@ 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 18b88329de6c73..61fbec2f9311f2 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -18,8 +18,11 @@ if (config.listenInaddrAny) { const server = serve({ fetch: app.fetch, - hostname: config.listenInaddrAny ? undefined : '127.0.0.1', + 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 6c5596f5af6d1d..d31a26a5b0b13b 100644 --- a/lib/middleware/cache.ts +++ b/lib/middleware/cache.ts @@ -8,7 +8,7 @@ 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) => { if (!cacheModule.status.available || bypassList.has(ctx.req.path)) { @@ -16,9 +16,11 @@ const middleware: MiddlewareHandler = async (ctx, 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(ctx.req.path); - const controlKey = 'rsshub:path-requested:' + h64ToString(ctx.req.path); + const key = 'rsshub:koa-redis-cache:' + h64ToString(requestPath + limit); + const controlKey = 'rsshub:path-requested:' + h64ToString(requestPath + limit); const isRequesting = await cacheModule.globalCache.get(controlKey); 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 65dbfa65f3cec1..6bcf3fd639502b 100644 --- a/lib/middleware/template.tsx +++ b/lib/middleware/template.tsx @@ -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..0ac903323a2724 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)); @@ -84,7 +87,7 @@ const app = new Hono(); for (const namespace in namespaces) { const subApp = app.basePath(`/${namespace}`); for (const path in namespaces[namespace].routes) { - const wrapedHandler: Handler = async (ctx) => { + 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}`); @@ -93,12 +96,17 @@ for (const namespace in namespaces) { ctx.set('data', await namespaces[namespace].routes[path].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 08571fa43c4a3d..d97f9b49a78c84 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')); @@ -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')); @@ -885,9 +874,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')); @@ -1466,9 +1452,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/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/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/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/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/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.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/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/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..d91db909f94073 --- /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/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..06d1860d08d24c 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..f1c700f8abca7d 100644 --- a/lib/routes/141jav/namespace.ts +++ b/lib/routes/141jav/namespace.ts @@ -6,4 +6,5 @@ export const namespace: Namespace = { 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..bda31ee5e6aee7 100644 --- a/lib/routes/141ppv/namespace.ts +++ b/lib/routes/141ppv/namespace.ts @@ -6,4 +6,5 @@ export const namespace: Namespace = { description: `:::tip 官方提供的订阅源不支持 BT 下载订阅,地址为 [https://141ppv.com/feeds/](https://141ppv.com/feeds/) :::`, + lang: 'en', }; 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..4c150cbb705bec 100644 --- a/lib/routes/163/namespace.ts +++ b/lib/routes/163/namespace.ts @@ -6,4 +6,5 @@ export const namespace: Namespace = { description: `:::tip 部分歌单及听歌排行信息为登陆后可见,自建时将环境变量\`NCM_COOKIES\`设为登陆后的 Cookie 值,即可正常获取。 :::`, + lang: 'zh-CN', }; diff --git a/lib/routes/18comic/namespace.ts b/lib/routes/18comic/namespace.ts index e910cb8fb0a7c8..5773dfaf262f56 100644 --- a/lib/routes/18comic/namespace.ts +++ b/lib/routes/18comic/namespace.ts @@ -6,4 +6,5 @@ export const namespace: Namespace = { 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/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/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/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/thread.ts b/lib/routes/1point3acres/thread.ts index 6940d7536daa2b..48987e0373d119 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'], 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/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 fa169f5f66884c..4aede42b464378 100644 --- a/lib/routes/2048/index.ts +++ b/lib/routes/2048/index.ts @@ -76,11 +76,11 @@ async function handler(ctx) { }, }); const $ = load(response); - const targetLink = $('table.group-table tr').eq(1).find('td a').eq(0).attr('href'); + const targetLink = new URL($('table.group-table tr').eq(1).find('td a').eq(0).attr('href')).href; return targetLink; }); - const currentUrl = `${entranceDomain}/2048/thread.php?fid-${id}.html`; + const currentUrl = `${entranceDomain}2048/thread.php?fid-${id}.html`; const response = await ofetch.raw(currentUrl); 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/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/36kr/hot-list.ts b/lib/routes/36kr/hot-list.ts index 640cdd74f848cf..b546d4b1437ca2 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: { @@ -54,6 +55,10 @@ export const route: Route = { 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..82b64f29bb82ef 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..b8f3b66a79c557 100644 --- a/lib/routes/3dmgame/news-center.ts +++ b/lib/routes/3dmgame/news-center.ts @@ -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/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/namespace.ts b/lib/routes/423down/namespace.ts index e04849800f77f6..580ea8bbfa328d 100644 --- a/lib/routes/423down/namespace.ts +++ b/lib/routes/423down/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: '423down.com', categories: ['program-update'], description: '', + lang: 'zh-CN', }; 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/4ksj/forum.ts b/lib/routes/4ksj/forum.ts index 79c042020f91cf..0a48c18b884881 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?', @@ -27,6 +27,16 @@ export const route: Route = { 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/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/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/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/69shu/article.ts b/lib/routes/69shu/article.ts index e153292a11a6ef..4371e35dce616f 100644 --- a/lib/routes/69shu/article.ts +++ b/lib/routes/69shu/article.ts @@ -6,7 +6,7 @@ import type { Route, DataItem } from '@/types'; export const route: Route = { path: '/article/:id', name: '章节', - url: 'www.69shuba.pro', + url: 'www.69shuba.cx', maintainers: ['eternasuno'], example: '/69shu/article/47117', parameters: { id: '小说 id, 可在对应小说页 URL 中找到' }, @@ -21,19 +21,19 @@ export const route: Route = { }, radar: [ { - source: ['www.69shuba.pro/book/:id.htm'], + source: ['www.69shuba.cx/book/:id.htm'], target: '/article/:id', }, ], handler: async (ctx) => { const { id } = ctx.req.param(); - const link = `https://www.69shuba.pro/book/${id}.htm`; + const link = `https://www.69shuba.cx/book/${id}.htm`; const $ = load(await get(link)); const item = await Promise.all( $('.qustime li>a') - .map((_, chapter) => createItem(chapter.attribs.href)) .toArray() + .map((chapter) => createItem(chapter.attribs.href)) ); return { @@ -88,9 +88,9 @@ const decrypt = (txt: string, articleid: string, chapterid: string, decryptionMa } return txt + .replaceAll(/\u2003|\n/g, '') .split('

') - .map((line, index, array) => (lineMap[index] ? array[lineMap[index]] : line)) - .slice(1, -1) - .join('

') - .replaceAll(/\u2003|()/g, ''); + .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 index e02b40b527d26a..6c19ebe51938bd 100644 --- a/lib/routes/69shu/namespace.ts +++ b/lib/routes/69shu/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '69书吧', - url: '69shuba.pro', + url: '69shuba.cx', + lang: 'zh-CN', }; 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/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 e08f5a4b315f90..d1425e8c58e4f7 100644 --- a/lib/routes/6v123/utils.ts +++ b/lib/routes/6v123/utils.ts @@ -19,11 +19,11 @@ export async function loadDetailPage(link) { .replaceAll(/,免费下载,迅雷下载|,6v电影/g, ''), description: $('meta[name="description"]').attr('content'), enclosure_urls: $('table td') - .map((i, e) => ({ + .toArray() + .map((e) => ({ title: $(e).text().replace('磁力:', ''), magnet: $(e).find('a').attr('href'), })) - .toArray() .filter((item) => item.magnet?.includes('magnet')), }; } diff --git a/lib/routes/78dm/index.ts b/lib/routes/78dm/index.ts index 490990fa1f888a..eab9eb5fec913f 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/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/namespace.ts b/lib/routes/81/namespace.ts index 9a60c8f378391a..3019d5fbba3f6f 100644 --- a/lib/routes/81/namespace.ts +++ b/lib/routes/81/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { 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..9ac5d4883a3a60 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/namespace.ts b/lib/routes/91porn/namespace.ts index d9c38b15c6965d..a4340ed42aaff4 100644 --- a/lib/routes/91porn/namespace.ts +++ b/lib/routes/91porn/namespace.ts @@ -6,4 +6,5 @@ export const namespace: Namespace = { 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/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/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..7b0db41e031ad0 --- /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/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 4dd9d046b5fbb7..770ef16c4bc102 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*'], @@ -30,7 +31,7 @@ export const route: Route = { 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); @@ -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..f1981a6af26849 --- /dev/null +++ b/lib/routes/academia/topics.ts @@ -0,0 +1,49 @@ +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'], + 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}/MostRecent`); + const $ = load(response); + const list = $('.works > .u-borderBottom1') + .toArray() + .map((item) => { + const tagsElem = $(item).find('li.InlineList-item.u-positionRelative > span > script').text().replaceAll('}{', '},{'); + let categories = []; + if (tagsElem !== null) { + const categoriesJSON = JSON.parse(`[${tagsElem}]`); + categories = categoriesJSON.map((category) => category.name); + } + + return { + title: $(item).find('.header .title').text(), + link: $(item).find('.header .title > a').attr('href'), + author: $(item).find('span[itemprop=author] > a').text(), + description: $(item).find('.complete').text(), + category: categories, + }; + }); + return { + title: `academia.edu | ${interest} documents`, + link: `https://academia.edu/Documents/in/${interest}/MostRecent`, + item: list, + }; +} 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..597e3acefb73a5 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, diff --git a/lib/routes/acfun/bangumi.ts b/lib/routes/acfun/bangumi.ts index 34774763f747a2..9eef398bb8fe84 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: { 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/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..1f51c1d57cd6e0 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/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 97% rename from lib/routes/agirls/index.ts rename to lib/routes/agirls/z-index.ts index 1d6f0a8109b884..71a9d2aaeb125a 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: { 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..6ff28e398de7ba --- /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..ac70b06cfb3567 --- /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..1921ba2577c763 --- /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/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/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/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/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..2a1387b7966983 --- /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 { + title, + description, + pubDate: timezone(parseDate($$('div.newstag_l').text().split(/\s/)[0]), +8), + author: item.author, + 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/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/namespace.ts b/lib/routes/alipan/namespace.ts index e123290f95bc88..91c32c55173649 100644 --- a/lib/routes/alipan/namespace.ts +++ b/lib/routes/alipan/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: '阿里云盘', url: 'www.alipan.com', categories: ['multimedia'], + lang: 'zh-CN', }; diff --git a/lib/routes/aliresearch/information.ts b/lib/routes/aliresearch/information.ts index 2ee2597b067e2e..a98eb99d788bc6 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: { 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/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/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/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/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/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/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/namespace.ts b/lib/routes/anquanke/namespace.ts index 8373e6e9d3b73a..fe64481676dec1 100644 --- a/lib/routes/anquanke/namespace.ts +++ b/lib/routes/anquanke/namespace.ts @@ -6,4 +6,5 @@ export const namespace: Namespace = { 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/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/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 3b5f3cc8f0576d..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?', - categories: ['traditional-media', 'popular'], + 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,7 +38,7 @@ 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 { ...res, 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 2379614b1bb38c..bd89c1ba353a7b 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 { 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,29 @@ 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() + .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') - .map((item) => (new URL(item.link).hostname === 'apnews.com' ? fetchArticle(item) : item)) + .map((item) => (ctx.req.query('fulltext') === 'true' ? fetchArticle(item) : item)) ); 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 7ff2edd2634673..5db057510ade07 100644 --- a/lib/routes/apnews/utils.ts +++ b/lib/routes/apnews/utils.ts @@ -2,21 +2,73 @@ import cache from '@/utils/cache'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; 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); - const ldjson = JSON.parse($('#link-ld-json').text())[0]; - $('div.Enhancement').remove(); - return { - pubDate: parseDate(ldjson.datePublished), - updated: parseDate(ldjson.dateModified), - description: $('div.RichTextStoryBody').html(), - category: [`section:${$("meta[property='article:section']").attr('content')}`, ...ldjson.keywords], - guid: $("meta[name='brightspot.contentId']").attr('content'), - author: ldjson.author, - ...item, - }; + 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/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/apple/apps.ts b/lib/routes/apple/apps.ts index a243fba251d442..e638747938d918 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,10 +62,7 @@ export const route: Route = { name: 'App Update', maintainers: ['EkkoG', 'nczitzk'], handler, - description: `| All | iOS | macOS | tvOS | - | --- | --- | ----- | ---- | - | | iOS | macOS | tvOS | - + 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). :::`, @@ -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; } 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..bcafb0e3b69f45 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: { 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/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/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/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/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/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..2bd81a185d5012 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,52 @@ 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}&pdid=${productInfo.productID}&sitelang=cn` + : `https://www.asus.com/support/api/product.asmx/GetPDBIOS?website=global&model=${model}&pdid=${productInfo.productID}&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/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/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/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 94% rename from lib/routes/bangumi/tv/subject/index.ts rename to lib/routes/bangumi.tv/subject/index.ts index 83416b2d83528e..cbc7a4cbee89fe 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, 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 3271ac28ae14cd..00000000000000 --- a/lib/routes/bangumi/tv/other/followrank.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Route } from '@/types'; -import { load } from 'cheerio'; -import { config } from '@/config'; -import ofetch from '@/utils/ofetch'; - -export const route: Route = { - path: '/:type/followrank', - categories: ['anime'], - example: '/bangumi/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'], - handler, -}; - -async function handler(ctx) { - let type = ctx.req.param('type'); - if (!type || type === 'tv') { - type = 'anime'; - } - const url = `https://bgm.tv/${type}`; - - const response = await ofetch(url, { - headers: { - 'User-Agent': config.trueUA, - }, - }); - - const $ = load(response); - - 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'), - })), - ]; - - 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/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/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 293b3c21e8e77d..e56cba3535bfe4 100644 --- a/lib/routes/bbc/index.ts +++ b/lib/routes/bbc/index.ts @@ -66,7 +66,9 @@ async function handler(ctx) { linkURL.hostname = 'www.bbc.co.uk'; } - const response = await ofetch(linkURL.href); + const response = await ofetch(linkURL.href, { + retryStatusCodes: [403], + }); const $ = load(response); 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/namespace.ts b/lib/routes/bdys/namespace.ts index d810d855cac1e5..9af7a356025e8d 100644 --- a/lib/routes/bdys/namespace.ts +++ b/lib/routes/bdys/namespace.ts @@ -6,4 +6,5 @@ export const namespace: Namespace = { 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/user.ts b/lib/routes/behance/user.ts index 35e61a4dc4dca7..34249494f7a96c 100644 --- a/lib/routes/behance/user.ts +++ b/lib/routes/behance/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,9 +6,20 @@ import { parseDate } from '@/utils/parse-date'; 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, diff --git a/lib/routes/beijingprice/index.ts b/lib/routes/beijingprice/index.ts new file mode 100644 index 00000000000000..e019d2f98d97d8 --- /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/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 232bd7a3bf735f..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; 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..395f1b0f9a9454 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}/`, @@ -250,7 +276,6 @@ const getArticleDataFromCvid = async (cvid, uid) => { export default { getCookie, - clearCookie, getWbiVerifyString, getUsernameFromUID, getUsernameAndFaceFromUID, @@ -260,4 +285,5 @@ export default { 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/dynamic.ts b/lib/routes/bilibili/dynamic.ts index 30f056cc1ea1bc..be3c3e8d797412 100644 --- a/lib/routes/bilibili/dynamic.ts +++ b/lib/routes/bilibili/dynamic.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 JSONbig from 'json-bigint'; @@ -11,8 +11,21 @@ import { BilibiliWebDynamicResponse, Item2, Modules } from './api-interface'; export const route: Route = { path: '/user/dynamic/:uid/:routeParams?', 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 | + +用例:\`/bilibili/user/dynamic/2267573/showEmoji=1&embed=0&useAvid=1\``, + }, features: { requireConfig: [ { @@ -26,7 +39,7 @@ export const route: Route = { }, ], requirePuppeteer: false, - antiCrawler: true, + antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, @@ -40,20 +53,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 +108,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 +147,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) => { @@ -227,21 +230,16 @@ 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 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(`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 +247,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,115 +258,125 @@ 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); + 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); - const data = item.modules; - const origin = item?.orig?.modules; + const data = item.modules; + const origin = item?.orig?.modules; - // link - let link = ''; - if (item.id_str) { - link = `https://t.bilibili.com/${item.id_str}`; - } + // link + let link = ''; + if (item.id_str) { + link = `https://t.bilibili.com/${item.id_str}`; + } - 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] || ''); + 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 (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?.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 (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?.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, - category: category.length ? category : undefined, - }; - }) + 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, + }; + }) ); 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/followings-dynamic.ts b/lib/routes/bilibili/followings-dynamic.ts index 364fdada9475f8..4b6e953e6b2914 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: [ { @@ -43,7 +55,7 @@ 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); @@ -74,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 = ''; // 动态图片 @@ -110,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..7f34ab44341d9d 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: [ { @@ -37,7 +38,7 @@ export const route: Route = { 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/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/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..ccb37d8310ffcc 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, @@ -135,7 +135,7 @@ export const route: Route = { 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 6372f40eee86f8..ab292460122f33 100644 --- a/lib/routes/bilibili/ranking.ts +++ b/lib/routes/bilibili/ranking.ts @@ -1,59 +1,219 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import got from '@/utils/got'; import utils 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'], + maintainers: ['DIYgod', 'hyoban'], categories: ['social-media', 'popular'], - example: '/bilibili/ranking/0/3/1', + 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; + } + + 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}`, 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..b0942b4358bf22 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}`; @@ -70,19 +70,12 @@ async function handler(ctx) { description: `${userName} 的 bilibili 频道`, 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..03a4976000dea5 100644 --- a/lib/routes/bilibili/user-collection.ts +++ b/lib/routes/bilibili/user-collection.ts @@ -3,20 +3,19 @@ import got from '@/utils/got'; import cache from './cache'; import utils from './utils'; import { parseDate } from '@/utils/parse-date'; -import { queryToBoolean } from '@/utils/readable-social'; const notFoundData = { title: '此 bilibili 频道不存在', }; 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', }, @@ -36,7 +35,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 = queryToBoolean(ctx.req.param('disableEmbed')); + const embed = !ctx.req.param('embed'); 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; @@ -62,19 +61,12 @@ async function handler(ctx) { description: `${userName} 的 bilibili 合集`, 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..3189adccb4cab2 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,207 @@ 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 default { - iframe, lsid, _uuid, hexsign, addWbiVerifyInfo, getDmImgList, + getDmImgInter, addDmVerifyInfo, + addDmVerifyInfoWithInter, bvidTime, + addRenderData, + getBangumi, + getBangumiItems, + renderUGCDescription, + renderOGVDescription, }; 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 7059a2a1870b50..9693a0d1b9ee59 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 logger from '@/utils/logger'; export const route: Route = { - path: '/user/video/:uid/:disableEmbed?', + 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,7 +68,7 @@ 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, diff --git a/lib/routes/bilibili/vsearch.ts b/lib/routes/bilibili/vsearch.ts index e76126756bd065..955f45e2fda3a0 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: [ { @@ -38,10 +42,22 @@ export const route: Route = { | 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..b66fab49a7313d 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: [ { @@ -38,7 +38,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 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..f98c1b3feb32a7 100644 --- a/lib/routes/biquge/namespace.ts +++ b/lib/routes/biquge/namespace.ts @@ -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/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/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/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/namespace.ts b/lib/routes/bjtu/namespace.ts index 74f3a9a242c099..4fd3ddb7a0c258 100644 --- a/lib/routes/bjtu/namespace.ts +++ b/lib/routes/bjtu/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { zh: { name: '北京交通大学', }, + lang: 'zh-CN', }; 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..fb172ddbe03e36 --- /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/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/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..778a865fd92467 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/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/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/bookfere/category.ts b/lib/routes/bookfere/category.ts index 8ecd286fbb2486..b8e895bcf93c44 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, 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/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/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/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..6aff1ab5eca50c 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, }, }); 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..b1d66d4447f9fe --- /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/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..91890bfe466f9f --- /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..e6f7a4ecf439ca 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..4df96e814edc55 100755 --- a/lib/routes/buaa/sme.ts +++ b/lib/routes/buaa/sme.ts @@ -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/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/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/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/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..7dc456c37c1145 100644 --- a/lib/routes/caixin/category.ts +++ b/lib/routes/caixin/category.ts @@ -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/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/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/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/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/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/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..c0eb76f92caa9e --- /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 = 'http://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 + 若订阅 [行业资讯](http://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=11),网址为 \`http://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)。 + ::: + +
    + 更多分类 + + #### [协会](http://www.cbpanet.com/dzp_xiehui.aspx) + + | [协会介绍](http://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=1) | [协会章程](http://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=2) | [理事会](http://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=3) | [内设机构](http://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=4) | [协会通知](http://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=5) | [协会活动](http://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) | + + | [出版物](http://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=7) | [会员权利与义务](http://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) | + + #### [行业资讯](http://www.cbpanet.com/dzp_news_list.aspx) + + | [国内资讯](http://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=8) | [海外资讯](http://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=9) | [企业新闻](http://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=10) | [行业资讯](http://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=11) | [热点聚焦](http://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=43) | [今日推荐](http://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) | + + #### [原料信息](http://www.cbpanet.com/dzp_yuanliao.aspx) + + | [价格行情](http://www.cbpanet.com/dzp_news.aspx?bigid=3&smallid=12) | [分析预测](http://www.cbpanet.com/dzp_news.aspx?bigid=3&smallid=13) | [原料信息](http://www.cbpanet.com/dzp_news.aspx?bigid=3&smallid=40) | [热点聚焦](http://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) | + + #### [法规标准](http://www.cbpanet.com/dzp_fagui.aspx) + + | [法规资讯](http://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=15) | [法律法规](http://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=16) | [国内标准](http://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=14) | [国外标准](http://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=17) | [法规聚焦](http://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=46) | [今日推荐](http://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) | + + #### [技术专区](http://www.cbpanet.com/dzp_jishu.aspx) + + | [产品介绍](http://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=18) | [科技成果](http://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=19) | [学术论文](http://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=20) | [资料下载](http://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=21) | [专家](http://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=50) | [民间智库](http://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) | + + #### [豆制品消费指南](http://www.cbpanet.com/dzp_zhinan.aspx) + + | [膳食指南](http://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=22) | [营养成分](http://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=23) | [豆食菜谱](http://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=24) | [问与答](http://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=31) | [今日推荐](http://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=48) | [消费热点](http://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) | + + #### [营养与健康](http://www.cbpanet.com/dzp_yingyang.aspx) + + | [大豆营养概况](http://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=25) | [大豆食品和人类健康](http://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=26) | [世界豆类日,爱豆大行动](http://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=27) | [谣言粉碎机](http://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=29) | [最新资讯](http://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=41) | [专家视点](http://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/cccfna/index.ts b/lib/routes/cccfna/index.ts new file mode 100644 index 00000000000000..e7b206dd859168 --- /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/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/ccfa/index.ts b/lib/routes/ccfa/index.ts new file mode 100644 index 00000000000000..f3f870eed38f1d --- /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..15eaebd67130f2 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', @@ -31,18 +32,21 @@ export const route: Route = { 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/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/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/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/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/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/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/namespace.ts b/lib/routes/cfr/namespace.ts index 46065157deaf08..939154e0cb5a5f 100644 --- a/lib/routes/cfr/namespace.ts +++ b/lib/routes/cfr/namespace.ts @@ -3,4 +3,5 @@ import { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Council on Foreign Relations', url: 'www.cfr.org', + lang: 'en', }; 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/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..7977b387300ae4 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: { 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/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/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..f723c05ee7090b --- /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..09015ea56837d1 --- /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/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/chinacdc/index.ts b/lib/routes/chinacdc/index.ts new file mode 100644 index 00000000000000..5ca8c021d7b947 --- /dev/null +++ b/lib/routes/chinacdc/index.ts @@ -0,0 +1,490 @@ +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') || aEl.text(); + + 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, + description, + pubDate: parseDate($item.find('span').text()), + 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 title: string = $$('h5').text(); + const description: string = art(path.join(__dirname, 'templates/description.art'), { + description: $$('div.TRS_Editor').html(), + }); + + return { + title, + description, + pubDate: parseDate($$('span.fb em').text()), + 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..418cc8596acbd1 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 7d54d7eb07179a..5de50965fc8b63 100644 --- a/lib/routes/chinaisa/index.ts +++ b/lib/routes/chinaisa/index.ts @@ -20,144 +20,144 @@ export const route: Route = { name: '栏目', maintainers: ['nczitzk'], handler, - description: `| 栏目 | id | - | -------- | ---------------------------------------------------------------- | + description: `| 栏目 | id | + | -------- | --------------------------------------------------------------- | | 钢协动态 | 58af05dfb6b4300151760176d2aad0a04c275aaadbb1315039263f021f920dcd | | 钢协要闻 | 67ea4f106bd8f0843c0538d43833c463a0cd411fc35642cbd555a5f39fcf352b | | 会议报道 | e5070694f299a43b20d990e53b6a69dc02e755fef644ae667cf75deaff80407a | | 领导讲话 | a873c2e67b26b4a2d8313da769f6e106abc9a1ff04b7f1a50674dfa47cf91a7b | | 图片新闻 | 806254321b2459bddb3c2cb5590fef6332bd849079d3082daf6153d7f8d62e1e | -
    - 更多栏目 - - #### 党建工作 - - | 栏目 | id | - | ---------------------------------------------------- | ---------------------------------------------------------------- | - | 党建工作 | 10e8911e0c852d91f08e173c768700da608abfb4e7b0540cb49fa5498f33522b | - | 学习贯彻习近平新时代中国特色社会主义思想主题教育专栏 | b7a7ad4b5d8ffaca4b29f3538fd289da9d07f827f89e6ea57ef07257498aacf9 | - | 党史学习教育专栏 | 4d8e7dec1b672704916331431156ea7628a598c191d751e4fc28408ccbd4e0c4 | - | 不忘初心、牢记使命 | 427f7c28c90ec9db1aab78db8156a63ff2e23f6a0cea693e3847fe6d595753db | - | 两学一做 | 5b0609fedc9052bb44f1cfe9acf5ec8c9fe960f22a07be69636f2cf1cacaa8f7 | - | 钢协党代会 | beaaa0314f0f532d4b18244cd70df614a4af97465d974401b1f5b3349d78144b | - | 创先争优 | e7ea82c886ba18691210aaf48b3582a92dca9c4f2aab912757cedafb066ff8a6 | - | 青年工作 | 2706ee3a4a4c3c23e90e13c8fdc3002855d1dba394b61626562a97b33af3dbd0 | - | 日常动态 | e21157a082fc0ab0d7062c8755e91472ee0d23de6ccc5c2a44b62e54062cf1e4 | - - #### 要闻 - - | 栏目 | id | - | ------------ | ---------------------------------------------------------------- | - | 要闻 | c42511ce3f868a515b49668dd250290c80d4dc8930c7e455d0e6e14b8033eae2 | - | 会员动态 | 268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c | - | 疫情应对专栏 | a83c48faeb34065fd9b33d3c84957a152675141458aedc0ec454b760c9fcad65 | - - #### 统计发布 - - | 栏目 | id | - | -------- | ---------------------------------------------------------------- | - | 统计发布 | 2e3c87064bdfc0e43d542d87fce8bcbc8fe0463d5a3da04d7e11b4c7d692194b | - | 生产经营 | 3238889ba0fa3aabcf28f40e537d440916a361c9170a4054f9fc43517cb58c1e | - | 进出口 | 95ef75c752af3b6c8be479479d8b931de7418c00150720280d78c8f0da0a438c | - | 环保统计 | 619ce7b53a4291d47c19d0ee0765098ca435e252576fbe921280a63fba4bc712 | - - #### 行业分析 - - | 栏目 | id | - | -------- | ---------------------------------------------------------------- | - | 行业分析 | 1b4316d9238e09c735365896c8e4f677a3234e8363e5622ae6e79a5900a76f56 | - | 市场分析 | a44207e193a5caa5e64102604b6933896a0025eb85c57c583b39626f33d4dafd | - | 板带材 | 05d0e136828584d2cd6e45bdc3270372764781b98546cce122d9974489b1e2f2 | - | 社会库存 | 197422a82d9a09b9cc86188444574816e93186f2fde87474f8b028fc61472d35 | - - #### 钢材价格指数 - - | 栏目 | id | - | ------------ | ---------------------------------------------------------------- | - | 钢材价格指数 | 17b6a9a214c94ccc28e56d4d1a2dbb5acef3e73da431ddc0a849a4dcfc487d04 | - | 综合价格指数 | 63913b906a7a663f7f71961952b1ddfa845714b5982655b773a62b85dd3b064e | - | 地区价格 | fc816c75aed82b9bc25563edc9cf0a0488a2012da38cbef5258da614d6e51ba9 | - - #### 宏观经济信息 - - | 栏目 | id | - | ------------ | ---------------------------------------------------------------- | - | 宏观经济信息 | 5d77b433182404193834120ceed16fe0625860fafd5fd9e71d0800c4df227060 | - | 相关行业信息 | ae2a3c0fd4936acf75f4aab6fadd08bc6371aa65bdd50419e74b70d6f043c473 | - | 国际动态 | 1bad7c56af746a666e4a4e56e54a9508d344d7bc1498360580613590c16b6c41 | - - #### 专题报道 - - | 栏目 | id | - | -------------------- | ---------------------------------------------------------------- | - | 专题报道 | 50e7242bfd78b4395f3338df7699a0ff8847b886c4c3a55bd7c102a2cfe32fe9 | - | 钢协理事会 | 40c6404418699f0f8cb4e513013bb110ef250c782f0959852601e7c75e1afcd8 | - | 钢协新闻发布会 | 11ea370f565c6c141b1a4dac60aa00c4331bd442382a5dd476a5e73e001b773c | - | 劳模表彰 | 907e4ae217bf9c981a132051572103f9c87cccb7f00caf5a1770078829e6bcb3 | - | 钢铁行业职业技能竞赛 | 563c15270a691e3c7cb9cd9ba457c5af392eb4630fa833fc1a55c8e2afbc28a9 | - - #### 成果奖励 - - | 栏目 | id | - | ---------------------- | ---------------------------------------------------------------- | - | 成果奖励 | a6c30053b66356b4d77fbf6668bda69f7e782b2ae08a21d5db171d50a504bd40 | - | 冶金科学技术奖 | 50fe0c63f657ee48e49cb13fe7f7c5502046acdb05e2ee8a317f907af4191683 | - | 企业管理现代化创新成果 | b5607d3b73c2c3a3b069a97b9dbfd59af64aea27bafd5eb87ba44d1b07a33b66 | - | 清洁生产环境友好企业 | 4475c8e21374d063a22f95939a2909837e78fab1832dc97bf64f09fa01c0c5f7 | - | 产品开发市场开拓奖 | 169e34d7b29e3deaf4d4496da594d3bbde2eb0a40f7244b54dbfb9cc89a37296 | - | 质量金杯奖 | 68029784be6d9a7bf9cb8cace5b8a5ce5d2d871e9a0cbcbf84eeae0ea2746311 | - - #### 节能减排 - - | 栏目 | id | - | ------------------------------------------ | ---------------------------------------------------------------- | - | 节能减排 | 08895f1681c198fdf297ab38e33e1f428f6ccf2add382f3844a52e410f10e5a0 | - | 先进节能环保技术 | 6e639343a517fd08e5860fba581d41940da523753956ada973b6952fc05ef94f | - | 钢铁企业超低排放改造和评估监测进展情况公示 | 50d99531d5dee68346653ca9548f308764ad38410a091e662834a5ed66770174 | - - #### 国际交流 - - | 栏目 | id | - | -------- | ---------------------------------------------------------------- | - | 国际交流 | 4753eef81b4019369d4751413d852ab9027944b84c612b5a08614e046d169e81 | - | 外事动态 | aa590ec6f835136a9ce8c9f3d0c3b194beb6b78037466ab40bb4aacc32adfcc9 | - | 国际会展 | 05ac1f2971bc375d25c9112e399f9c3cbb237809684ebc5b0ca4a68a1fcb971c | - - #### 政策法规 - - | 栏目 | id | - | -------- | ---------------------------------------------------------------- | - | 政策法规 | 63a69eb0087f1984c0b269a1541905f19a56e117d56b3f51dfae0e6c1d436533 | - | 政策法规 | a214b2e71c3c79fa4a36ff382ee5f822b9603634626f7e320f91ed696b3666f2 | - | 贸易规则 | 5988b2380d04d3efde8cc247377d19530c17904ec0b5decdd00f9b3e026e3715 | - - #### 分会园地 - - | 栏目 | id | - | ------------ | ---------------------------------------------------------------- | - | 分会园地 | d059d6751dcaae94e31a795072267f7959c35d012eebb9858b3ede2990e82ea9 | - | 法律分会 | 96000647f18ea78fa134a3932563e7d27c68d0482de498f179b44846234567a9 | - | 设备分会 | c8e1e3f52406115c2c03928271bbe883c0875b7c9f2f67492395685a62a1a2d8 | - | 国际产能合作 | 4fb8cc4b0d6f905a969ac3375f6d17b34df4dcae69d798d2a4616daa80af020c | - | 绿化分会 | ad55a0fbc1a44e94fb60e21b98cf967aca17ecf1450bdfb3699468fe8235103b | - - #### 钢铁知识 - - | 栏目 | id | - | ------------ | ---------------------------------------------------------------- | - | 钢铁知识 | 7f7509ff045023015e0d6c1ba22c32734b673be2ec14eae730a99c08e3badb3f | - | 钢铁材料使用 | 7e319d71258ed6bb663cf59b4cf67fe97894e60aa5520f3d2cf966f82f9b89ac | - | 钢铁标准 | fae0c4dd27f8fe4759941e78c9dc1dfe0088ce30d1b684d12be4c8172d2c08e1 | - - #### 钢协刊物 - - | 栏目 | id | - | ---------- | ---------------------------------------------------------------- | - | 钢协刊物 | ed51af486f6d4b313b3aaf8fea0b32a4a2d4a89714c61992caf01942eb61831b | - | 中国钢铁业 | 6440bdfccadf87908b13d8bbd9a66bb89bbd60cc5e175c018ca1c62c7d55e61f | - | 钢铁信息 | 2b66af0b2cda9b420739e55e255a6f72f277557670ef861c9956da8fde25da05 | -
    `, +
    + 更多栏目 + + #### 党建工作 + + | 栏目 | id | + | ---------------------------------------------------- | ---------------------------------------------------------------- | + | 党建工作 | 10e8911e0c852d91f08e173c768700da608abfb4e7b0540cb49fa5498f33522b | + | 学习贯彻习近平新时代中国特色社会主义思想主题教育专栏 | b7a7ad4b5d8ffaca4b29f3538fd289da9d07f827f89e6ea57ef07257498aacf9 | + | 党史学习教育专栏 | 4d8e7dec1b672704916331431156ea7628a598c191d751e4fc28408ccbd4e0c4 | + | 不忘初心、牢记使命 | 427f7c28c90ec9db1aab78db8156a63ff2e23f6a0cea693e3847fe6d595753db | + | 两学一做 | 5b0609fedc9052bb44f1cfe9acf5ec8c9fe960f22a07be69636f2cf1cacaa8f7 | + | 钢协党代会 | beaaa0314f0f532d4b18244cd70df614a4af97465d974401b1f5b3349d78144b | + | 创先争优 | e7ea82c886ba18691210aaf48b3582a92dca9c4f2aab912757cedafb066ff8a6 | + | 青年工作 | 2706ee3a4a4c3c23e90e13c8fdc3002855d1dba394b61626562a97b33af3dbd0 | + | 日常动态 | e21157a082fc0ab0d7062c8755e91472ee0d23de6ccc5c2a44b62e54062cf1e4 | + + #### 要闻 + + | 栏目 | id | + | ------------ | ---------------------------------------------------------------- | + | 要闻 | c42511ce3f868a515b49668dd250290c80d4dc8930c7e455d0e6e14b8033eae2 | + | 会员动态 | 268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c | + | 疫情应对专栏 | a83c48faeb34065fd9b33d3c84957a152675141458aedc0ec454b760c9fcad65 | + + #### 统计发布 + + | 栏目 | id | + | -------- | ---------------------------------------------------------------- | + | 统计发布 | 2e3c87064bdfc0e43d542d87fce8bcbc8fe0463d5a3da04d7e11b4c7d692194b | + | 生产经营 | 3238889ba0fa3aabcf28f40e537d440916a361c9170a4054f9fc43517cb58c1e | + | 进出口 | 95ef75c752af3b6c8be479479d8b931de7418c00150720280d78c8f0da0a438c | + | 环保统计 | 619ce7b53a4291d47c19d0ee0765098ca435e252576fbe921280a63fba4bc712 | + + #### 行业分析 + + | 栏目 | id | + | -------- | ---------------------------------------------------------------- | + | 行业分析 | 1b4316d9238e09c735365896c8e4f677a3234e8363e5622ae6e79a5900a76f56 | + | 市场分析 | a44207e193a5caa5e64102604b6933896a0025eb85c57c583b39626f33d4dafd | + | 板带材 | 05d0e136828584d2cd6e45bdc3270372764781b98546cce122d9974489b1e2f2 | + | 社会库存 | 197422a82d9a09b9cc86188444574816e93186f2fde87474f8b028fc61472d35 | + + #### 钢材价格指数 + + | 栏目 | id | + | ------------ | ---------------------------------------------------------------- | + | 钢材价格指数 | 17b6a9a214c94ccc28e56d4d1a2dbb5acef3e73da431ddc0a849a4dcfc487d04 | + | 综合价格指数 | 63913b906a7a663f7f71961952b1ddfa845714b5982655b773a62b85dd3b064e | + | 地区价格 | fc816c75aed82b9bc25563edc9cf0a0488a2012da38cbef5258da614d6e51ba9 | + + #### 宏观经济信息 + + | 栏目 | id | + | ------------ | ---------------------------------------------------------------- | + | 宏观经济信息 | 5d77b433182404193834120ceed16fe0625860fafd5fd9e71d0800c4df227060 | + | 相关行业信息 | ae2a3c0fd4936acf75f4aab6fadd08bc6371aa65bdd50419e74b70d6f043c473 | + | 国际动态 | 1bad7c56af746a666e4a4e56e54a9508d344d7bc1498360580613590c16b6c41 | + + #### 专题报道 + + | 栏目 | id | + | -------------------- | ---------------------------------------------------------------- | + | 专题报道 | 50e7242bfd78b4395f3338df7699a0ff8847b886c4c3a55bd7c102a2cfe32fe9 | + | 钢协理事会 | 40c6404418699f0f8cb4e513013bb110ef250c782f0959852601e7c75e1afcd8 | + | 钢协新闻发布会 | 11ea370f565c6c141b1a4dac60aa00c4331bd442382a5dd476a5e73e001b773c | + | 劳模表彰 | 907e4ae217bf9c981a132051572103f9c87cccb7f00caf5a1770078829e6bcb3 | + | 钢铁行业职业技能竞赛 | 563c15270a691e3c7cb9cd9ba457c5af392eb4630fa833fc1a55c8e2afbc28a9 | + + #### 成果奖励 + + | 栏目 | id | + | ---------------------- | ---------------------------------------------------------------- | + | 成果奖励 | a6c30053b66356b4d77fbf6668bda69f7e782b2ae08a21d5db171d50a504bd40 | + | 冶金科学技术奖 | 50fe0c63f657ee48e49cb13fe7f7c5502046acdb05e2ee8a317f907af4191683 | + | 企业管理现代化创新成果 | b5607d3b73c2c3a3b069a97b9dbfd59af64aea27bafd5eb87ba44d1b07a33b66 | + | 清洁生产环境友好企业 | 4475c8e21374d063a22f95939a2909837e78fab1832dc97bf64f09fa01c0c5f7 | + | 产品开发市场开拓奖 | 169e34d7b29e3deaf4d4496da594d3bbde2eb0a40f7244b54dbfb9cc89a37296 | + | 质量金杯奖 | 68029784be6d9a7bf9cb8cace5b8a5ce5d2d871e9a0cbcbf84eeae0ea2746311 | + + #### 节能减排 + + | 栏目 | id | + | ------------------------------------------ | ---------------------------------------------------------------- | + | 节能减排 | 08895f1681c198fdf297ab38e33e1f428f6ccf2add382f3844a52e410f10e5a0 | + | 先进节能环保技术 | 6e639343a517fd08e5860fba581d41940da523753956ada973b6952fc05ef94f | + | 钢铁企业超低排放改造和评估监测进展情况公示 | 50d99531d5dee68346653ca9548f308764ad38410a091e662834a5ed66770174 | + + #### 国际交流 + + | 栏目 | id | + | -------- | ---------------------------------------------------------------- | + | 国际交流 | 4753eef81b4019369d4751413d852ab9027944b84c612b5a08614e046d169e81 | + | 外事动态 | aa590ec6f835136a9ce8c9f3d0c3b194beb6b78037466ab40bb4aacc32adfcc9 | + | 国际会展 | 05ac1f2971bc375d25c9112e399f9c3cbb237809684ebc5b0ca4a68a1fcb971c | + + #### 政策法规 + + | 栏目 | id | + | -------- | ---------------------------------------------------------------- | + | 政策法规 | 63a69eb0087f1984c0b269a1541905f19a56e117d56b3f51dfae0e6c1d436533 | + | 政策法规 | a214b2e71c3c79fa4a36ff382ee5f822b9603634626f7e320f91ed696b3666f2 | + | 贸易规则 | 5988b2380d04d3efde8cc247377d19530c17904ec0b5decdd00f9b3e026e3715 | + + #### 分会园地 + + | 栏目 | id | + | ------------ | ---------------------------------------------------------------- | + | 分会园地 | d059d6751dcaae94e31a795072267f7959c35d012eebb9858b3ede2990e82ea9 | + | 法律分会 | 96000647f18ea78fa134a3932563e7d27c68d0482de498f179b44846234567a9 | + | 设备分会 | c8e1e3f52406115c2c03928271bbe883c0875b7c9f2f67492395685a62a1a2d8 | + | 国际产能合作 | 4fb8cc4b0d6f905a969ac3375f6d17b34df4dcae69d798d2a4616daa80af020c | + | 绿化分会 | ad55a0fbc1a44e94fb60e21b98cf967aca17ecf1450bdfb3699468fe8235103b | + + #### 钢铁知识 + + | 栏目 | id | + | ------------ | ---------------------------------------------------------------- | + | 钢铁知识 | 7f7509ff045023015e0d6c1ba22c32734b673be2ec14eae730a99c08e3badb3f | + | 钢铁材料使用 | 7e319d71258ed6bb663cf59b4cf67fe97894e60aa5520f3d2cf966f82f9b89ac | + | 钢铁标准 | fae0c4dd27f8fe4759941e78c9dc1dfe0088ce30d1b684d12be4c8172d2c08e1 | + + #### 钢协刊物 + + | 栏目 | id | + | ---------- | ---------------------------------------------------------------- | + | 钢协刊物 | ed51af486f6d4b313b3aaf8fea0b32a4a2d4a89714c61992caf01942eb61831b | + | 中国钢铁业 | 6440bdfccadf87908b13d8bbd9a66bb89bbd60cc5e175c018ca1c62c7d55e61f | + | 钢铁信息 | 2b66af0b2cda9b420739e55e255a6f72f277557670ef861c9956da8fde25da05 | +
    `, }; async function handler(ctx) { 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/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..d3cea7ae41f4f8 --- /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/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/chinaventure/index.ts b/lib/routes/chinaventure/index.ts index fa99ff0e4541bc..ce56b893487b71 100644 --- a/lib/routes/chinaventure/index.ts +++ b/lib/routes/chinaventure/index.ts @@ -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..3ea95397d04ee5 --- /dev/null +++ b/lib/routes/chongdiantou/index.ts @@ -0,0 +1,54 @@ +import { Route } from '@/types'; +import { namespace } from './namespace'; +import ofetch from '@/utils/ofetch'; +import logger from '@/utils/logger'; + +async function getPosts() { + try { + // Fetch data directly from the API without caching + const response = await ofetch('https://www.chongdiantou.com/wp-json/wp/v2/posts?_embed&per_page=10', { + headers: { + method: 'GET', + }, + }); + return response.map((post) => ({ + title: post.title.rendered, + link: post.link, + pubDate: new Date(post.date_gmt), // Use date_gmt instead of date + category: post._embedded['wp:term'][0].map((term) => term.name).join(', '), + description: post.content.rendered, + author: post._embedded.author[0].name, + image: post._embedded['wp:featuredmedia'] ? post._embedded['wp:featuredmedia'][0].source_url : '', + })); + } catch (error) { + logger.error('Error fetching posts:', error); + return []; + } +} + +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 items = await getPosts(); + + 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/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/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/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..2263a61a8345f2 --- /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/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..1785e2dc3d6a6b --- /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/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..d91efef8d549e5 --- /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/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/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/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/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/namespace.ts b/lib/routes/cnbeta/namespace.ts index 16da5989dec3ef..d703fd32ee9c3a 100644 --- a/lib/routes/cnbeta/namespace.ts +++ b/lib/routes/cnbeta/namespace.ts @@ -3,5 +3,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'cnBeta.COM', url: 'cnbeta.com.tw', - categories: ['new-media'], + categories: ['new-media', 'popular'], + lang: 'zh-TW', }; 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/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/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..03f351af45bbba 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: { diff --git a/lib/routes/cngold/index.ts b/lib/routes/cngold/index.ts new file mode 100644 index 00000000000000..3787256522bc11 --- /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 b3391d60acde9d..8bcbd5e46cb533 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 { parseDate } from '@/utils/parse-date'; import { ProcessItem } from './utils'; -const rootUrl = 'https://kns.cnki.net'; - 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: '作者期刊文献', + 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, - }); - 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 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, - }; + body: params.toString(), }); + 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 items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => 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 5b3f044d557da9..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(), }); 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/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/namespace.ts b/lib/routes/coindesk/namespace.ts index 9aa1ef1d7c703d..e8eb42ec10e36f 100644 --- a/lib/routes/coindesk/namespace.ts +++ b/lib/routes/coindesk/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'CoinDesk Consensus Magazine', url: 'coindesk.com', + lang: 'en', }; 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/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/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/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..ba865265aa5ab5 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, diff --git a/lib/routes/coolapk/hot.ts b/lib/routes/coolapk/hot.ts index e12624ad9fe4b2..fa7211abe9513d 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, 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..cc3de8a3aef871 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..74da783ff1d595 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, diff --git a/lib/routes/coolapk/tuwen.ts b/lib/routes/coolapk/tuwen.ts index ece427b0d6abbd..65a87d081cafaf 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, @@ -25,16 +31,15 @@ export const route: Route = { 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 index a16dab6fa3f8eb..e3e4119f0b86e0 100644 --- a/lib/routes/coomer/artist.ts +++ b/lib/routes/coomer/artist.ts @@ -16,7 +16,7 @@ export const route: Route = { }, radar: [ { - source: ['coomer.party/onlyfans/user/:id', 'coomer.party/'], + source: ['coomer.su/onlyfans/user/:id', 'coomer.su/'], }, ], name: 'Artist', 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 index 85a6d067efe419..ff7ec889995633 100644 --- a/lib/routes/coomer/posts.ts +++ b/lib/routes/coomer/posts.ts @@ -16,13 +16,13 @@ export const route: Route = { }, radar: [ { - source: ['coomer.party/posts', 'coomer.party/'], + source: ['coomer.su/posts', 'coomer.su/'], }, ], name: 'Recent Posts', maintainers: ['nczitzk'], handler, - url: 'coomer.party/posts', + url: 'coomer.su/posts', }; async function handler(ctx) { diff --git a/lib/routes/coomer/utils.ts b/lib/routes/coomer/utils.ts index 50483bea018590..1767efdae43e63 100644 --- a/lib/routes/coomer/utils.ts +++ b/lib/routes/coomer/utils.ts @@ -4,7 +4,7 @@ import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; const fetchItems = async (ctx, currentUrl) => { - const rootUrl = 'https://coomer.party'; + const rootUrl = 'https://coomer.su'; currentUrl = `${rootUrl}/${currentUrl}`; const response = await got({ 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..44b9a87812fd33 100644 --- a/lib/routes/copymanga/comic.ts +++ b/lib/routes/copymanga/comic.ts @@ -34,7 +34,7 @@ async function handler(ctx) { // 用于控制返回的章节数量 const chapterCnt = Number(ctx.req.param('chapterCnt') || 10); // 直接调用拷贝漫画的接口 - const host = 'copymanga.site'; + const host = 'copymanga.tv'; const baseUrl = `https://${host}`; const apiBaseUrl = `https://api.${host}`; const strBaseUrl = `${apiBaseUrl}/api/v3/comic/${id}/group/default/chapters`; @@ -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/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..7e4c2c6a1c74a5 --- /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..296999fade54bf 100644 --- a/lib/routes/cpcaauto/index.ts +++ b/lib/routes/cpcaauto/index.ts @@ -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/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/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/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/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/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/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/cs/index.ts b/lib/routes/cs/index.ts index b4446df9efc8ca..fd8aec323a2987 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..a84c50b6336cbd 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/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/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/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/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 index 88eca49be8c4eb..042cd9030176e6 100644 --- a/lib/routes/cuilingmag/index.ts +++ b/lib/routes/cuilingmag/index.ts @@ -135,6 +135,7 @@ export const route: Route = { path: '/:category?', name: '分类', url: 'cuilingmag.com', + categories: ['new-media', 'popular'], maintainers: ['nczitzk'], handler, example: '/cuilingmag', diff --git a/lib/routes/cuilingmag/namespace.ts b/lib/routes/cuilingmag/namespace.ts index ea8fd6e42f35a9..30740a1b5108e3 100644 --- a/lib/routes/cuilingmag/namespace.ts +++ b/lib/routes/cuilingmag/namespace.ts @@ -5,4 +5,5 @@ export const namespace: Namespace = { url: 'cuilingmag.com', categories: ['new-media'], description: '', + lang: 'zh-CN', }; 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/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/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/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/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..23f80e4b57ba9a 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 { baseUrl, getData, getList } 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,40 @@ const query = ` bio } `; -const graphqlQuery = { - query, - variables, -}; export const route: Route = { path: '/discussed', example: '/daily/discussed', 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', }; -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 link = `${baseUrl}/discussed`; + + const data = await getData({ + query, + variables: { + first: limit, + }, + }); + const items = getList(data); + 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 index 031f47c0a773eb..40f0fdfbeaef7d 100644 --- a/lib/routes/daily/index.ts +++ b/lib/routes/daily/index.ts @@ -1,5 +1,5 @@ import { Route } from '@/types'; -import { getData, getList, getRedirectedLink } from './utils.js'; +import { baseUrl, getData, getList } from './utils.js'; const variables = { version: 11, @@ -28,6 +28,7 @@ const query = ` edges { node { ...FeedPost + contentHtml } } } @@ -42,6 +43,7 @@ const query = ` image readTime permalink + commentsPermalink summary createdAt numUpvotes @@ -72,27 +74,28 @@ export const route: Route = { example: '/daily', radar: [ { - source: ['daily.dev/popular'], + source: ['app.daily.dev/popular'], }, ], name: 'Popular', maintainers: ['Rjnishant530'], handler, - url: 'daily.dev/popular', + url: 'app.daily.dev/popular', }; async function handler() { - const baseUrl = 'https://app.daily.dev/popular'; + const link = `${baseUrl}/popular`; + const data = await getData(graphqlQuery); - const list = getList(data); - const items = await getRedirectedLink(list); + const items = getList(data); + return { - title: 'Popular', - link: baseUrl, + title: 'Popular posts on daily.dev', + link, item: items, - description: 'Popular Posts on Daily.dev', - logo: 'https://app.daily.dev/favicon-32x32.png', - icon: 'https://app.daily.dev/favicon-32x32.png', + 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/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/source.ts b/lib/routes/daily/source.ts new file mode 100644 index 00000000000000..4594daa6d462c1 --- /dev/null +++ b/lib/routes/daily/source.ts @@ -0,0 +1,186 @@ +import { 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', + example: '/daily/source/hn', + parameters: { + sourceId: 'The source id', + }, + 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 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); + }, + config.cache.routeExpire, + false + ); + + return { + title: `${userData.name} posts on daily.dev`, + description: userData.description, + link, + item: items, + image: userData.image, + logo: userData.image, + icon: userData.image, + language: 'en-us', + }; +} diff --git a/lib/routes/daily/upvoted.ts b/lib/routes/daily/upvoted.ts index 68a2a19d6cc784..8326d0618ed6d0 100644 --- a/lib/routes/daily/upvoted.ts +++ b/lib/routes/daily/upvoted.ts @@ -1,5 +1,5 @@ import { Route } from '@/types'; -import { getData, getList, getRedirectedLink } from './utils.js'; +import { baseUrl, getData, getList } from './utils.js'; const variables = { period: 7, @@ -21,6 +21,7 @@ const query = ` edges { node { ...FeedPost + contentHtml } } } @@ -35,6 +36,7 @@ const query = ` image readTime permalink + commentsPermalink summary createdAt numUpvotes @@ -56,37 +58,35 @@ const query = ` `; -const graphqlQuery = { - query, - variables, -}; - export const route: Route = { path: '/upvoted', example: '/daily/upvoted', radar: [ { - source: ['daily.dev/popular'], + source: ['app.daily.dev/upvoted'], }, ], 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); + const link = `${baseUrl}/upvoted`; + const data = await getData({ + query, + variables, + }); + const items = getList(data); + 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..cdf7383684c7b7 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 { 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,20 +150,18 @@ const userPostQuery = ` downvoted }`; -const render = (data) => art(path.join(__dirname, 'templates/posts.art'), data); - export const route: Route = { path: '/user/:userId', example: '/daily/user/kramer', radar: [ { - source: ['daily.dev/:userId/posts', 'daily.dev/:userId'], + source: ['app.daily.dev/:userId/posts', 'app.daily.dev/:userId'], }, ], name: 'User Posts', maintainers: ['TonyRL'], handler, - url: 'daily.dev', + url: 'app.daily.dev', }; async function handler(ctx) { @@ -178,12 +171,12 @@ async function handler(ctx) { 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 +191,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); }, config.cache.routeExpire, false diff --git a/lib/routes/daily/utils.ts b/lib/routes/daily/utils.ts index 8f203c744d15a3..4576a869502544 100644 --- a/lib/routes/daily/utils.ts +++ b/lib/routes/daily/utils.ts @@ -2,10 +2,15 @@ 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'; +const __dirname = getCurrentPath(import.meta.url); -const baseUrl = 'https://app.daily.dev'; +export const baseUrl = 'https://app.daily.dev'; +const gqlUrl = `https://api.daily.dev/graphql`; -const getBuildId = () => +export const getBuildId = () => cache.tryGet( 'daily:buildId', async () => { @@ -17,40 +22,30 @@ const getBuildId = () => false ); -const getData = async (graphqlQuery) => { - const response = await ofetch(`${baseUrl}/api/graphql`, { +export const getData = async (graphqlQuery) => { + const response = await ofetch(gqlUrl, { method: 'POST', body: graphqlQuery, }); return 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); - return { - id, - title, - link: permalink, - description: summary, - author: author?.name, - itunes_item_image: image, - pubDate, - upvotes: numUpvotes, - comments: numComments, - category: tags, - }; - }); - -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') }; - }) - ) - ); +const render = (data) => art(path.join(__dirname, 'templates/posts.art'), data); -export { baseUrl, getBuildId, getData, getList, getRedirectedLink }; +export const getList = (edges) => + edges.map(({ node }) => ({ + id: node.id, + title: node.title, + link: node.commentsPermalink ?? node.permalink, + 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: parseDate(node.createdAt), + upvotes: node.numUpvotes, + comments: node.numComments, + category: node.tags, + })); 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..f80069b1a589b9 --- /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/namespace.ts b/lib/routes/darwinawards/namespace.ts index 20c3aa81fb7b16..e1895ace98e040 100644 --- a/lib/routes/darwinawards/namespace.ts +++ b/lib/routes/darwinawards/namespace.ts @@ -4,4 +4,5 @@ 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..74a40c31f5d1cd --- /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://dgcb20-ca-northeurope-dglive.yellowground-c1f17366.northeurope.azurecontainerapps.io/api/v1/content/articles?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://dgcb20-ca-northeurope-dglive.yellowground-c1f17366.northeurope.azurecontainerapps.io/api/v1/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/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..6cda36822b7d7c 100644 --- a/lib/routes/dcard/namespace.ts +++ b/lib/routes/dcard/namespace.ts @@ -6,4 +6,5 @@ export const namespace: Namespace = { 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..ff2235fb428dfa 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: '新聞中心', diff --git a/lib/routes/dcfever/reviews.ts b/lib/routes/dcfever/reviews.ts index f7ba620d79ab23..61e58354fb43be 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: [ 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..6c994e910f50e3 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: '二手市集', @@ -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..e1e740c9718ab5 --- /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/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..bd713c36e01870 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, }; 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 28e11dd69f4fd3..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: { diff --git a/lib/routes/dedao/namespace.ts b/lib/routes/dedao/namespace.ts index c77830eb8651cc..9a61a637f8007e 100644 --- a/lib/routes/dedao/namespace.ts +++ b/lib/routes/dedao/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '得到', url: 'dedao.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/dedao/user.ts b/lib/routes/dedao/user.ts index 7e8a41bee4ecf2..e679ab52143113 100644 --- a/lib/routes/dedao/user.ts +++ b/lib/routes/dedao/user.ts @@ -15,7 +15,7 @@ const types = { export const route: Route = { path: '/user/:id/:type?', - categories: ['new-media'], + categories: ['new-media', 'popular'], example: '/dedao/user/VkA5OqLX4RyGxmZRNBMlwBrDaJQ9og', parameters: { id: '用户 id,可在对应用户主页 URL 中找到', type: '类型,见下表,默认为`0`,即动态' }, features: { diff --git a/lib/routes/deepin/namespace.ts b/lib/routes/deepin/namespace.ts index eae1d434b24f66..9222bac2855b79 100644 --- a/lib/routes/deepin/namespace.ts +++ b/lib/routes/deepin/namespace.ts @@ -6,4 +6,5 @@ export const namespace: Namespace = { zh: { name: '深度Linux', }, + lang: 'zh-CN', }; diff --git a/lib/routes/deepin/thread.ts b/lib/routes/deepin/thread.ts new file mode 100644 index 00000000000000..99c30b9ec01f3d --- /dev/null +++ b/lib/routes/deepin/thread.ts @@ -0,0 +1,102 @@ +import { Route, DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/threads/:type?', + categories: ['bbs'], + example: '/deepin/threads/latest', + parameters: { + type: { + description: '主题类型', + options: [ + { + value: 'hot', + label: '最热主题', + }, + { + value: 'latest', + label: '最新主题', + }, + ], + }, + }, + name: '首页主题列表', + maintainers: ['myml'], + radar: [ + { + source: ['bbs.deepin.org'], + target: '/threads/latest', + }, + ], + handler, +}; + +interface ThreadIndexResult { + ThreadIndex: { + id: number; + subject: string; + created_at: string; + user: { nickname: string }; + forum: { name: string }; + }[]; +} +interface ThreadInfoResult { + data: { + id: number; + subject: string; + created_at: string; + user: { nickname: string }; + post: { message: string }; + }; +} + +const TypeMap = { + hot: { where: 'hot_value', title: '最热主题' }, + latest: { where: 'id', title: '最新主题' }, +}; + +async function handler(ctx) { + let type = TypeMap.latest; + if (ctx.req.param('type') === 'hot') { + type = TypeMap.hot; + } + const res = await ofetch('https://bbs.deepin.org.cn/api/v1/thread/index', { + query: { + languages: 'zh_CN', + order: 'updated_at', + where: type.where, + }, + headers: { + accept: 'application/json', + }, + }); + const items = await Promise.all( + res.ThreadIndex.map((thread) => { + const link = 'https://bbs.deepin.org.cn/post/' + thread.id; + return cache.tryGet(link, async () => { + const item: DataItem = { + id: String(thread.id), + title: thread.subject, + link, + pubDate: parseDate(thread.created_at), + author: thread.user.nickname, + category: [thread.forum.name], + description: '', + }; + const cacheData = await ofetch('https://bbs.deepin.org.cn/api/v1/thread/info?id=' + item.id); + if (cacheData) { + const info = cacheData as ThreadInfoResult; + item.description = info.data.post.message; + } + return item; + }) as Promise; + }) + ); + return { + title: 'deepin论坛主页 - ' + type.title, + link: 'https://bbs.deepin.org', + item: items, + }; +} diff --git a/lib/routes/deeplearning/namespace.ts b/lib/routes/deeplearning/namespace.ts index 6359a83cb9ca6c..85f560dc8625c0 100644 --- a/lib/routes/deeplearning/namespace.ts +++ b/lib/routes/deeplearning/namespace.ts @@ -1,6 +1,9 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'deeplearning.ai', + name: 'DeepLearning.AI', url: 'www.deeplearning.ai', + categories: ['programming'], + description: '', + lang: 'en', }; diff --git a/lib/routes/deeplearning/templates/description.art b/lib/routes/deeplearning/templates/description.art new file mode 100644 index 00000000000000..249654e7e618a4 --- /dev/null +++ b/lib/routes/deeplearning/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/deeplearning/the-batch.ts b/lib/routes/deeplearning/the-batch.ts new file mode 100644 index 00000000000000..11d50796b6a26d --- /dev/null +++ b/lib/routes/deeplearning/the-batch.ts @@ -0,0 +1,296 @@ +import { Route } from '@/types'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +export const handler = async (ctx) => { + const { tag } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 1; + + const rootUrl = 'https://www.deeplearning.ai'; + const currentUrl = new URL(`the-batch${tag ? `/tag/${tag.replace(/^tag\//, '').replace(/\/$/, '')}` : ''}/`, rootUrl).href; + + const response = await ofetch(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + const data = JSON.parse($('script#__NEXT_DATA__').text()); + + const nextBuildId = data.buildId; + const posts = data.props?.pageProps?.posts ?? []; + + let items = posts.slice(0, limit).map((item) => { + const title = item.title; + const description = art(path.join(__dirname, 'templates/description.art'), { + images: item.feature_image + ? [ + { + src: item.feature_image, + alt: item.feature_image_alt, + }, + ] + : undefined, + intro: item.excerpt ?? item.custom_excerpt, + }); + const image = item.feature_image; + const guid = `the-batch-${item.slug}`; + + return { + title, + description, + pubDate: parseDate(item.published_at), + link: new URL(`_next/data/${nextBuildId}/the-batch/${item.slug}.json`, rootUrl).href, + category: item.tags.map((t) => t.name), + guid, + id: guid, + content: { + html: description, + text: item.excerpt ?? item.custom_excerpt, + }, + image, + banner: image, + language, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await ofetch(item.link); + + const post = detailResponse.pageProps?.cmsData?.post ?? undefined; + + if (!post) { + return item; + } + + const $$ = load(post.html); + + $$('a').each((_, ele) => { + if (ele.attribs.href?.includes('utm_campaign')) { + const url = new URL(ele.attribs.href); + url.searchParams.delete('utm_campaign'); + url.searchParams.delete('utm_source'); + url.searchParams.delete('utm_medium'); + url.searchParams.delete('_hsenc'); + ele.attribs.href = url.href; + } + }); + + const title = post.title; + const description = art(path.join(__dirname, 'templates/description.art'), { + images: post.feature_image + ? [ + { + src: post.feature_image, + alt: post.feature_image_alt, + }, + ] + : undefined, + intro: post.excerpt ?? post.custom_excerpt, + description: $$.html(), + }); + const guid = `the-batch-${post.slug}`; + const image = post.feature_image; + + item.title = title; + item.description = description; + item.pubDate = parseDate(post.published_at); + item.link = new URL(`the-batch/${post.slug}`, rootUrl).href; + item.category = post.tags.map((t) => t.name); + item.author = post.authors.map((a) => a.name).join('/'); + item.guid = guid; + item.id = guid; + item.content = { + html: description, + text: post.excerpt ?? post.custom_excerpt, + }; + item.image = image; + item.banner = image; + item.updated = parseDate(post.updated_at); + item.language = language; + + return item; + }) + ) + ); + + const image = new URL($('meta[property="og:image"]').prop('content'), rootUrl).href; + + return { + title: $('title').text(), + description: $('meta[property="og:description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $('meta[property="og:site_name"]').prop('content'), + language, + }; +}; + +export const route: Route = { + path: '/the-batch/:tag{.+}?', + name: 'The Batch', + url: 'www.deeplearning.ai', + maintainers: ['nczitzk', 'juvenn', 'TonyRL'], + handler, + example: '/deeplearning/the-batch', + parameters: { tag: 'Tag, Weekly Issues by default' }, + description: `::: tip + If you subscribe to [Data Points](https://www.deeplearning.ai/the-batch/tag/data-points/),where the URL is \`https://www.deeplearning.ai/the-batch/tag/data-points/\`, extract the part \`https://www.deeplearning.ai/the-batch/tag\` to the end, which is \`data-points\`, and use it as the parameter to fill in. Therefore, the route will be [\`/deeplearning/the-batch/data-points\`](https://rsshub.app/deeplearning/the-batch/data-points). + + ::: + + | Tag | ID | + | ---------------------------------------------------------------------- | -------------------------------------------------------------------- | + | [Weekly Issues](https://www.deeplearning.ai/the-batch/) | [*null*](https://rsshub.app/deeplearning/the-batch) | + | [Andrew's Letters](https://www.deeplearning.ai/the-batch/tag/letters/) | [letters](https://rsshub.app/deeplearning/the-batch/letters) | + | [Data Points](https://www.deeplearning.ai/the-batch/tag/data-points/) | [data-points](https://rsshub.app/deeplearning/the-batch/data-points) | + | [ML Research](https://www.deeplearning.ai/the-batch/tag/research/) | [research](https://rsshub.app/deeplearning/the-batch/research) | + | [Business](https://www.deeplearning.ai/the-batch/tag/business/) | [business](https://rsshub.app/deeplearning/the-batch/business) | + | [Science](https://www.deeplearning.ai/the-batch/tag/science/) | [science](https://rsshub.app/deeplearning/the-batch/science) | + | [AI & Society](https://www.deeplearning.ai/the-batch/tag/ai-society/) | [ai-society](https://rsshub.app/deeplearning/the-batch/ai-society) | + | [Culture](https://www.deeplearning.ai/the-batch/tag/culture/) | [culture](https://rsshub.app/deeplearning/the-batch/culture) | + | [Hardware](https://www.deeplearning.ai/the-batch/tag/hardware/) | [hardware](https://rsshub.app/deeplearning/the-batch/hardware) | + | [AI Careers](https://www.deeplearning.ai/the-batch/tag/ai-careers/) | [ai-careers](https://rsshub.app/deeplearning/the-batch/ai-careers) | + + #### [Letters from Andrew Ng](https://www.deeplearning.ai/the-batch/tag/letters/) + + | Tag | ID | + | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | + | [All](https://www.deeplearning.ai/the-batch/tag/letters/) | [letters](https://rsshub.app/deeplearning/the-batch/letters) | + | [Personal Insights](https://www.deeplearning.ai/the-batch/tag/personal-insights/) | [personal-insights](https://rsshub.app/deeplearning/the-batch/personal-insights) | + | [Technical Insights](https://www.deeplearning.ai/the-batch/tag/technical-insights/) | [technical-insights](https://rsshub.app/deeplearning/the-batch/technical-insights) | + | [Business Insights](https://www.deeplearning.ai/the-batch/tag/business-insights/) | [business-insights](https://rsshub.app/deeplearning/the-batch/business-insights) | + | [Tech & Society](https://www.deeplearning.ai/the-batch/tag/tech-society/) | [tech-society](https://rsshub.app/deeplearning/the-batch/tech-society) | + | [DeepLearning.AI News](https://www.deeplearning.ai/the-batch/tag/deeplearning-ai-news/) | [deeplearning-ai-news](https://rsshub.app/deeplearning/the-batch/deeplearning-ai-news) | + | [AI Careers](https://www.deeplearning.ai/the-batch/tag/ai-careers/) | [ai-careers](https://rsshub.app/deeplearning/the-batch/ai-careers) | + | [Just For Fun](https://www.deeplearning.ai/the-batch/tag/just-for-fun/) | [just-for-fun](https://rsshub.app/deeplearning/the-batch/just-for-fun) | + | [Learning & Education](https://www.deeplearning.ai/the-batch/tag/learning-education/) | [learning-education](https://rsshub.app/deeplearning/the-batch/learning-education) | + `, + categories: ['programming'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.deeplearning.ai/the-batch', 'www.deeplearning.ai/the-batch/tag/:tag/'], + target: (params) => { + const tag = params.tag; + + return `/the-batch${tag ? `/${tag}` : ''}`; + }, + }, + { + title: 'Weekly Issues', + source: ['www.deeplearning.ai/the-batch/'], + target: '/the-batch', + }, + { + title: "Andrew's Letters", + source: ['www.deeplearning.ai/the-batch/tag/letters/'], + target: '/the-batch/letters', + }, + { + title: 'Data Points', + source: ['www.deeplearning.ai/the-batch/tag/data-points/'], + target: '/the-batch/data-points', + }, + { + title: 'ML Research', + source: ['www.deeplearning.ai/the-batch/tag/research/'], + target: '/the-batch/research', + }, + { + title: 'Business', + source: ['www.deeplearning.ai/the-batch/tag/business/'], + target: '/the-batch/business', + }, + { + title: 'Science', + source: ['www.deeplearning.ai/the-batch/tag/science/'], + target: '/the-batch/science', + }, + { + title: 'AI & Society', + source: ['www.deeplearning.ai/the-batch/tag/ai-society/'], + target: '/the-batch/ai-society', + }, + { + title: 'Culture', + source: ['www.deeplearning.ai/the-batch/tag/culture/'], + target: '/the-batch/culture', + }, + { + title: 'Hardware', + source: ['www.deeplearning.ai/the-batch/tag/hardware/'], + target: '/the-batch/hardware', + }, + { + title: 'AI Careers', + source: ['www.deeplearning.ai/the-batch/tag/ai-careers/'], + target: '/the-batch/ai-careers', + }, + { + title: 'Letters from Andrew Ng - All', + source: ['www.deeplearning.ai/the-batch/tag/letters/'], + target: '/the-batch/letters', + }, + { + title: 'Letters from Andrew Ng - Personal Insights', + source: ['www.deeplearning.ai/the-batch/tag/personal-insights/'], + target: '/the-batch/personal-insights', + }, + { + title: 'Letters from Andrew Ng - Technical Insights', + source: ['www.deeplearning.ai/the-batch/tag/technical-insights/'], + target: '/the-batch/technical-insights', + }, + { + title: 'Letters from Andrew Ng - Business Insights', + source: ['www.deeplearning.ai/the-batch/tag/business-insights/'], + target: '/the-batch/business-insights', + }, + { + title: 'Letters from Andrew Ng - Tech & Society', + source: ['www.deeplearning.ai/the-batch/tag/tech-society/'], + target: '/the-batch/tech-society', + }, + { + title: 'Letters from Andrew Ng - DeepLearning.AI News', + source: ['www.deeplearning.ai/the-batch/tag/deeplearning-ai-news/'], + target: '/the-batch/deeplearning-ai-news', + }, + { + title: 'Letters from Andrew Ng - AI Careers', + source: ['www.deeplearning.ai/the-batch/tag/ai-careers/'], + target: '/the-batch/ai-careers', + }, + { + title: 'Letters from Andrew Ng - Just For Fun', + source: ['www.deeplearning.ai/the-batch/tag/just-for-fun/'], + target: '/the-batch/just-for-fun', + }, + { + title: 'Letters from Andrew Ng - Learning & Education', + source: ['www.deeplearning.ai/the-batch/tag/learning-education/'], + target: '/the-batch/learning-education', + }, + ], +}; diff --git a/lib/routes/deeplearning/thebatch.ts b/lib/routes/deeplearning/thebatch.ts deleted file mode 100644 index 1264a03a051bcb..00000000000000 --- a/lib/routes/deeplearning/thebatch.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; - -export const route: Route = { - path: '/thebatch', - categories: ['programming'], - example: '/deeplearning/thebatch', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['www.deeplearning.ai/thebatch', 'www.deeplearning.ai/'], - }, - ], - name: 'TheBatch 周报', - maintainers: ['nczitzk', 'juvenn'], - handler, - url: 'www.deeplearning.ai/thebatch', -}; - -async function handler() { - const page = await got({ - method: 'get', - url: `https://www.deeplearning.ai/the-batch/`, - }); - const nextJs = page.data.match(/