mirror of
https://github.com/imsyy/DailyHotApi.git
synced 2026-01-12 21:24:55 +08:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcff976a4d | ||
|
|
62a8880ae4 | ||
|
|
e6f89c4868 | ||
|
|
fa80a29772 | ||
|
|
b8f7c1ad23 | ||
|
|
d5217c3dff | ||
|
|
e1beb5b534 | ||
|
|
14bc5a1dce | ||
|
|
11addd20ca | ||
|
|
23375519ba | ||
|
|
2ba46c5e0e | ||
|
|
d4a52a6b24 | ||
|
|
d73ca1170c | ||
|
|
93d90eb653 | ||
|
|
a3bb42d26c | ||
|
|
093312ea8c | ||
|
|
13f019a5aa | ||
|
|
ce5d381093 | ||
|
|
679fdab87e | ||
|
|
8c8a86957b | ||
|
|
8e077640b1 | ||
|
|
5555cc6f00 | ||
|
|
cdf2479044 | ||
|
|
067190b5a2 | ||
|
|
abadb692e4 | ||
|
|
f62b4ff4dd | ||
|
|
34ab73a3f1 | ||
|
|
7459858767 | ||
|
|
e38263ad40 | ||
|
|
49854b33d6 | ||
|
|
2a018c0640 | ||
|
|
b7260cf569 | ||
|
|
3343f2b5db | ||
|
|
2b91f3f32b | ||
|
|
fa8fb5a47f | ||
|
|
c0cbb3591b | ||
|
|
4c54be315f | ||
|
|
71a3621fd8 | ||
|
|
e6a02c667f | ||
|
|
07c0f6ed9b | ||
|
|
b8c16ad88a | ||
|
|
5df634058c | ||
|
|
029fed603b | ||
|
|
69fb0640be | ||
|
|
a4394588c1 | ||
|
|
b90c144a62 | ||
|
|
36c40b7870 | ||
|
|
b0dc506cc6 | ||
|
|
68edae7e74 | ||
|
|
d6fcb1628e | ||
|
|
9eb76a5f52 | ||
|
|
aab54d2190 | ||
|
|
85567ea638 | ||
|
|
6330914d09 | ||
|
|
813e88c993 | ||
|
|
e9dfcad437 | ||
|
|
8d7f394153 | ||
|
|
c42573c37c | ||
|
|
8cffc6c701 | ||
|
|
e079e2efb5 | ||
|
|
9e9fc713de | ||
|
|
4ae8e6cfd8 | ||
|
|
eaba3db4de | ||
|
|
c71e4267ff | ||
|
|
30f99cd010 | ||
|
|
87f0ca3c78 | ||
|
|
5b63365423 | ||
|
|
78e25f8b3a |
37
.dockerignore
Normal file
37
.dockerignore
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# folders
|
||||||
|
.devcontainer
|
||||||
|
.github
|
||||||
|
.husky
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
Dockerfile*
|
||||||
|
LICENSE
|
||||||
|
Procfile
|
||||||
|
node_modules
|
||||||
|
test
|
||||||
|
|
||||||
|
# files
|
||||||
|
.codecov.yml
|
||||||
|
.dockerignore
|
||||||
|
.editorconfig
|
||||||
|
.eslint*
|
||||||
|
.gitignore
|
||||||
|
.gitpod.yml
|
||||||
|
.markdownlint.jsonc
|
||||||
|
.prettier*
|
||||||
|
.(yarn|npm|nvm)rc
|
||||||
|
*.md
|
||||||
|
app.json
|
||||||
|
docker-compose*
|
||||||
|
fly.toml
|
||||||
|
jsconfig.json
|
||||||
|
npm-debug.log
|
||||||
|
process.json
|
||||||
|
package-lock.json
|
||||||
|
vercel.json
|
||||||
|
|
||||||
|
# git but keep the git commit hash
|
||||||
|
.git/logs
|
||||||
|
.git/index
|
||||||
|
.git/info
|
||||||
|
.git/hooks
|
||||||
19
.env
19
.env
@@ -1,5 +1,20 @@
|
|||||||
# 服务端口
|
# 服务端口
|
||||||
PORT=6688
|
PORT = 6688
|
||||||
|
|
||||||
# 允许的域名
|
# 允许的域名
|
||||||
ALLOWED_DOMAIN = '*'
|
ALLOWED_DOMAIN = "*"
|
||||||
|
|
||||||
|
# ROBOT
|
||||||
|
DISALLOW_ROBOT = true
|
||||||
|
|
||||||
|
# 缓存时长( 秒 )
|
||||||
|
CACHE_TTL = 3600
|
||||||
|
|
||||||
|
# 请求超时( 毫秒 )
|
||||||
|
REQUEST_TIMEOUT = 6000
|
||||||
|
|
||||||
|
# 是否输出日志
|
||||||
|
USE_LOG_FILE = true
|
||||||
|
|
||||||
|
# RSS Mode
|
||||||
|
RSS_MODE = false
|
||||||
5
.eslintignore
Normal file
5
.eslintignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.vscode
|
||||||
|
docker-compose.yml
|
||||||
|
dist
|
||||||
|
logs
|
||||||
|
!/.github
|
||||||
14
.eslintrc.json
Normal file
14
.eslintrc.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
56
.github/workflows/docker.yml
vendored
Normal file
56
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Publish Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-docker:
|
||||||
|
name: Push Docker image to multiple registries
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
imsyy/dailyhot-api
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
31
.github/workflows/npm.yml
vendored
Normal file
31
.github/workflows/npm.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Publish npm package
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-npm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20]
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
registry-url: https://registry.npmjs.org
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: npm publish
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -10,13 +10,8 @@ lerna-debug.log*
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
|
||||||
coverage
|
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
/cypress/videos/
|
|
||||||
/cypress/screenshots/
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
@@ -25,6 +20,4 @@ coverage
|
|||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
test.md
|
|
||||||
13
.hintrc
13
.hintrc
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": [
|
|
||||||
"development"
|
|
||||||
],
|
|
||||||
"hints": {
|
|
||||||
"meta-viewport": "off",
|
|
||||||
"disown-opener": "off",
|
|
||||||
"button-type": "off",
|
|
||||||
"axe/aria": "off",
|
|
||||||
"no-inline-styles": "off",
|
|
||||||
"axe/text-alternatives": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
.npmignore
Normal file
2
.npmignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist/logs/
|
||||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
pnpm-lock.yaml
|
||||||
|
LICENSE.md
|
||||||
|
# tsconfig.json
|
||||||
|
# tsconfig.*.json
|
||||||
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
84
CODE_OF_CONDUCT.md
Normal file
84
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our community include:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
- Giving and gracefully accepting constructive feedback
|
||||||
|
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||||
|
- Focusing on what is best not just for us as individuals, but for the overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at <i@diygod.me>. All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the project community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
||||||
|
available at <https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
<https://www.contributor-covenant.org/faq>. Translations are available at <https://www.contributor-covenant.org/translations>.
|
||||||
50
Dockerfile
Normal file
50
Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
|
ENV NODE_ENV=docker
|
||||||
|
|
||||||
|
# 安装 Puppeteer 所需的依赖库
|
||||||
|
RUN apk add libc6-compat
|
||||||
|
# RUN apk add chromium nss freetype harfbuzz ca-certificates
|
||||||
|
|
||||||
|
# 配置 Chromium
|
||||||
|
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
|
# ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*json tsconfig.json pnpm-lock.yaml .env ./
|
||||||
|
COPY src ./src
|
||||||
|
COPY public ./public
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
RUN pnpm build
|
||||||
|
RUN pnpm prune --production
|
||||||
|
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 创建用户和组
|
||||||
|
RUN addgroup --system --gid 114514 nodejs
|
||||||
|
RUN adduser --system --uid 114514 hono
|
||||||
|
|
||||||
|
# 创建日志目录
|
||||||
|
RUN mkdir -p /app/logs && chown -R hono:nodejs /app/logs
|
||||||
|
|
||||||
|
# 复制文件
|
||||||
|
COPY --from=builder --chown=hono:nodejs /app/node_modules /app/node_modules
|
||||||
|
COPY --from=builder --chown=hono:nodejs /app/dist /app/dist
|
||||||
|
COPY --from=builder /app/public /app/public
|
||||||
|
COPY --from=builder /app/.env /app/.env
|
||||||
|
COPY --from=builder /app/package.json /app/package.json
|
||||||
|
|
||||||
|
# 切换用户
|
||||||
|
USER hono
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 6688
|
||||||
|
|
||||||
|
# 运行
|
||||||
|
CMD ["node", "/app/dist/index.js"]
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 底层用户
|
Copyright (c) 2023 imsyy
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
291
README.md
291
README.md
@@ -2,199 +2,186 @@
|
|||||||
<img alt="logo" height="120" src="./public/favicon.png" width="120"/>
|
<img alt="logo" height="120" src="./public/favicon.png" width="120"/>
|
||||||
<h2>今日热榜</h2>
|
<h2>今日热榜</h2>
|
||||||
<p>一个聚合热门数据的 API 接口</p>
|
<p>一个聚合热门数据的 API 接口</p>
|
||||||
|
<br />
|
||||||
|
<img src="https://img.shields.io/github/last-commit/imsyy/DailyHotApi" alt="last commit"/>
|
||||||
|
<img src="https://img.shields.io/github/languages/code-size/imsyy/DailyHotApi" alt="code size"/>
|
||||||
|
<img src="https://img.shields.io/docker/image-size/imsyy/dailyhot-api" alt="docker-image-size"/>
|
||||||
|
<img src="https://github.com/imsyy/DailyHotApi/actions/workflows/docker.yml/badge.svg" alt="Publish Docker image"/>
|
||||||
|
<img src="https://github.com/imsyy/DailyHotApi/actions/workflows/npm.yml/badge.svg" alt="Publish npm package"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 示例
|
## 🚩 特性
|
||||||
|
|
||||||
> 这里是使用该 API 的示例站点
|
- 极快响应,便于开发
|
||||||
|
- 支持 RSS 模式和 JSON 模式
|
||||||
|
- 支持多种部署方式
|
||||||
|
- 简明的路由目录,便于新增
|
||||||
|
|
||||||
|
## 👀 示例
|
||||||
|
|
||||||
|
> 这里是使用该 API 的示例站点
|
||||||
|
> 示例站点可能由于访问量或者长久未维护而访问异常
|
||||||
|
> 若您也使用了本 API 搭建了网站,欢迎提交您的站点链接
|
||||||
|
|
||||||
- [今日热榜 - https://hot.imsyy.top/](https://hot.imsyy.top/)
|
- [今日热榜 - https://hot.imsyy.top/](https://hot.imsyy.top/)
|
||||||
|
|
||||||
## 总览
|
## 🎉 总览
|
||||||
|
|
||||||
> 🟢 状态正常
|
> 🟢 状态正常 / 🟠 可能失效 / ❌ 无法使用 / ⚠️ 需要科学上网
|
||||||
> 🟠 可能失效
|
|
||||||
> 🔴 无法使用
|
|
||||||
|
|
||||||
| **站点** | **类别** | **调用名称** | **状态** |
|
| **站点** | **类别** | **调用名称** | **状态** |
|
||||||
| ------------ | -------- | ------------ | -------- |
|
| ---------------- | ------------ | -------------- | -------- |
|
||||||
| 哔哩哔哩 | 热门榜 | bilibili | 🟢 |
|
| 哔哩哔哩 | 热门榜 | bilibili | 🟢 |
|
||||||
| 知乎 | 热榜 | zhihu | 🟢 |
|
| AcFun | 排行榜 | acfun | 🟢 |
|
||||||
| 百度 | 热搜榜 | baidu | 🟢 |
|
| 微博 | 热搜榜 | weibo | 🟢 |
|
||||||
| 百度贴吧 | 热议榜 | tieba | 🟢 |
|
| 知乎 | 热榜 | zhihu | 🟢 |
|
||||||
| 少数派 | 热榜 | sspai | 🟢 |
|
| 知乎日报 | 推荐榜 | zhihu-daily | 🟢 |
|
||||||
| IT 之家 | 热榜 | ithome | 🟠 |
|
| 百度 | 热搜榜 | baidu | 🟢 |
|
||||||
| 澎湃新闻 | 热榜 | thepaper | 🟢 |
|
| 抖音 | 热点榜 | douyin | 🟢 |
|
||||||
| 今日头条 | 热榜 | toutiao | 🟢 |
|
| 豆瓣电影 | 新片榜 | douban-movie | 🟢 |
|
||||||
| 微博热搜 | 热搜榜 | weibo | 🟢 |
|
| 豆瓣讨论小组 | 讨论精选 | douban-group | 🟢 |
|
||||||
| 36 氪 | 热榜 | 36kr | 🟢 |
|
| 百度贴吧 | 热议榜 | tieba | 🟢 |
|
||||||
| 稀土掘金 | 热榜 | juejin | 🟢 |
|
| 少数派 | 热榜 | sspai | 🟢 |
|
||||||
| 腾讯新闻 | 热点榜 | newsqq | 🟢 |
|
| IT之家 | 热榜 | ithome | 🟠 |
|
||||||
| 抖音热榜 | 热点榜 | douyin | 🟢 |
|
| IT之家「喜加一」 | 最新动态 | ithome-xijiayi | 🟠 |
|
||||||
| 英雄联盟 | 更新公告 | lol | 🟢 |
|
| 简书 | 热门推荐 | jianshu | 🟠 |
|
||||||
| 微信读书 | 飙升榜 | weread | 🟢 |
|
| 澎湃新闻 | 热榜 | thepaper | 🟢 |
|
||||||
| 历史上的今天 | 指定日期 | calendar | 🟢 |
|
| 今日头条 | 热榜 | toutiao | 🟢 |
|
||||||
|
| 36 氪 | 热榜 | 36kr | 🟢 |
|
||||||
|
| 51CTO | 推荐榜 | 51cto | 🟢 |
|
||||||
|
| CSDN | 排行榜 | csdn | 🟢 |
|
||||||
|
| 稀土掘金 | 热榜 | juejin | 🟢 |
|
||||||
|
| 腾讯新闻 | 热点榜 | qq-news | 🟢 |
|
||||||
|
| 网易新闻 | 热点榜 | netease-news | 🟢 |
|
||||||
|
| 吾爱破解 | 榜单 | 52pojie | ❌ |
|
||||||
|
| 全球主机交流 | 榜单 | hostloc | ❌ |
|
||||||
|
| 虎嗅 | 24小时 | huxiu | 🟢 |
|
||||||
|
| 爱范儿 | 快讯 | ifanr | 🟢 |
|
||||||
|
| 英雄联盟 | 更新公告 | lol | 🟢 |
|
||||||
|
| 原神 | 最新消息 | genshin | 🟢 |
|
||||||
|
| 崩坏3 | 最新动态 | honkai | 🟢 |
|
||||||
|
| 崩坏:星穹铁道 | 最新动态 | starrail | 🟢 |
|
||||||
|
| 微信读书 | 飙升榜 | weread | 🟢 |
|
||||||
|
| NGA | 热帖 | ngabbs | 🟢 |
|
||||||
|
| V2EX | 主题榜 | v2ex | ⚠️ |
|
||||||
|
| HelloGitHub | Trending | hellogithub | 🟢 |
|
||||||
|
| 中央气象台 | 全国气象预警 | weatheralarm | 🟢 |
|
||||||
|
| 中国地震台 | 地震速报 | earthquake | 🟢 |
|
||||||
|
| 历史上的今天 | 月-日 | history | 🟢 |
|
||||||
|
|
||||||
### 特殊接口说明
|
## ⚙️ 使用
|
||||||
|
|
||||||
#### 获取全部接口信息
|
本项目支持 `Node.js` 调用,可在安装完成后调用 `serveHotApi` 来开启服务器
|
||||||
|
|
||||||
获取除了下方特殊接口外的全部接口列表
|
```bash
|
||||||
|
pnpm add dailyhot-api
|
||||||
```http
|
|
||||||
GET https://{example.com}/all
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 历史上的今天(指定日期)
|
|
||||||
|
|
||||||
将指定的月份和日期传入即可得到当天数据,请注意格式
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET https://{example.com}/calendar/date?month=06&day=01
|
|
||||||
```
|
|
||||||
|
|
||||||
## 部署
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// 安装依赖
|
import serveHotApi from "dailyhot-api";
|
||||||
pnpm install
|
|
||||||
|
|
||||||
// 运行
|
/**
|
||||||
pnpm start
|
* 启动服务器
|
||||||
|
* @param {Number} [port] - 端口号
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
serveHotApi(3000);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Vercel 部署
|
## ⚙️ 部署
|
||||||
|
|
||||||
现已支持 Vercel 部署,无需服务器
|
具体使用说明可参考 [我的博客](https://blog.imsyy.top/posts/2024/0408),下方仅讲解基础操作:
|
||||||
|
|
||||||
### 操作方法
|
### Docker 部署
|
||||||
|
|
||||||
1. fork 本项目
|
> 安装及配置 Docker 将不在此处说明,请自行解决
|
||||||
2. 在 `Vercel` 官网点击 `New Project`
|
|
||||||
3. 点击 `Import Git Repository` 并选择你 fork 的此项目并点击 `import`
|
|
||||||
4. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可
|
|
||||||
|
|
||||||
## 调用
|
#### 本地构建
|
||||||
|
|
||||||
### 获取榜单数据
|
```bash
|
||||||
|
# 构建
|
||||||
> 获取数据只需在域名后面加上上方列表中的调用名称即可
|
docker build -t dailyhot-api .
|
||||||
|
# 运行
|
||||||
```http
|
docker run -p 6688:6688 -d dailyhot-api
|
||||||
GET https://api-hot.imsyy.top/bilibili/
|
# 或使用 Docker Compose
|
||||||
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
#### 在线部署
|
||||||
<summary>调用示例</summary>
|
|
||||||
|
|
||||||
```json
|
```bash
|
||||||
{
|
# 拉取
|
||||||
"code": 200,
|
docker pull imsyy/dailyhot-api:latest
|
||||||
"message": "获取成功",
|
# 运行
|
||||||
"title": "哔哩哔哩", // 榜单名称
|
docker run -p 6688:6688 -d imsyy/dailyhot-api:latest
|
||||||
"subtitle": "热门榜", // 榜单类别
|
|
||||||
"from": "server", // 此处返回是最新数据还是缓存
|
|
||||||
"total": 100, // 数据总数
|
|
||||||
"updateTime": "2023-03-14T07:40:51.846Z", // 数据获取时间
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": "BV1E84y1A7z2",
|
|
||||||
"title": "假如我的校园是一款RPG游戏!",
|
|
||||||
"desc": "所有取景都是在学校里面拍的,都是真实存在的场景哦!",
|
|
||||||
"pic": "http://i2.hdslb.com/bfs/archive/a24e442d0aae6d488db023c4ddcb450e9f2bf5f3.jpg",
|
|
||||||
"owner": {
|
|
||||||
"mid": 424658638,
|
|
||||||
"name": "四夕小田木_已黑化_",
|
|
||||||
"face": "https://i1.hdslb.com/bfs/face/afd9ba47933edc4842ccbeba2891a25465d1cf77.jpg"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"aid": 610872610,
|
|
||||||
"view": 4178745,
|
|
||||||
"danmaku": 4229,
|
|
||||||
"reply": 5317,
|
|
||||||
"favorite": 91020,
|
|
||||||
"coin": 133596,
|
|
||||||
"share": 46227,
|
|
||||||
"now_rank": 0,
|
|
||||||
"his_rank": 1,
|
|
||||||
"like": 616519,
|
|
||||||
"dislike": 0,
|
|
||||||
"vt": 0,
|
|
||||||
"vv": 0
|
|
||||||
},
|
|
||||||
"url": "https://b23.tv/BV1E84y1A7z2",
|
|
||||||
"mobileUrl": "https://m.bilibili.com/video/BV1E84y1A7z2"
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
### 手动部署
|
||||||
|
|
||||||
### 获取榜单最新数据
|
最直接的方式,您可以按照以下步骤将 DailyHotApi 部署在您的电脑、服务器或者其他任何地方
|
||||||
|
|
||||||
> 获取最新数据只需在原链接后面加上 `/new`,这样就会直接从服务端拉取最新数据,不会从本地缓存中读取
|
#### 安装
|
||||||
|
|
||||||
```http
|
```bash
|
||||||
GET https://api-hot.imsyy.top/bilibili/new
|
git clone https://github.com/imsyy/DailyHotApi.git
|
||||||
|
cd DailyHotApi
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
然后再执行安装依赖
|
||||||
<summary>调用示例</summary>
|
|
||||||
|
|
||||||
```json
|
```bash
|
||||||
{
|
npm install
|
||||||
"code": 200,
|
|
||||||
"message": "获取成功",
|
|
||||||
"title": "哔哩哔哩", // 榜单名称
|
|
||||||
"subtitle": "热门榜", // 榜单类别
|
|
||||||
"total": 100, // 数据总数
|
|
||||||
"updateTime": "2023-03-14T07:40:51.846Z", // 数据获取时间
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": "BV1E84y1A7z2",
|
|
||||||
"title": "假如我的校园是一款RPG游戏!",
|
|
||||||
"desc": "所有取景都是在学校里面拍的,都是真实存在的场景哦!",
|
|
||||||
"pic": "http://i2.hdslb.com/bfs/archive/a24e442d0aae6d488db023c4ddcb450e9f2bf5f3.jpg",
|
|
||||||
"owner": {
|
|
||||||
"mid": 424658638,
|
|
||||||
"name": "四夕小田木_已黑化_",
|
|
||||||
"face": "https://i1.hdslb.com/bfs/face/afd9ba47933edc4842ccbeba2891a25465d1cf77.jpg"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"aid": 610872610,
|
|
||||||
"view": 4178745,
|
|
||||||
"danmaku": 4229,
|
|
||||||
"reply": 5317,
|
|
||||||
"favorite": 91020,
|
|
||||||
"coin": 133596,
|
|
||||||
"share": 46227,
|
|
||||||
"now_rank": 0,
|
|
||||||
"his_rank": 1,
|
|
||||||
"like": 616519,
|
|
||||||
"dislike": 0,
|
|
||||||
"vt": 0,
|
|
||||||
"vv": 0
|
|
||||||
},
|
|
||||||
"url": "https://b23.tv/BV1E84y1A7z2",
|
|
||||||
"mobileUrl": "https://m.bilibili.com/video/BV1E84y1A7z2"
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
#### 开发
|
||||||
|
|
||||||
## 其他
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
- 本项目为了避免频繁请求官方数据,默认对数据做了缓存处理,默认为 `30` 分钟,如需更改,请自行前往 `utils\cacheData.js` 文件修改
|
成功启动后程序会在控制台输出可访问的地址
|
||||||
|
|
||||||
|
#### 编译运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
成功启动后程序会在控制台输出可访问的地址
|
||||||
|
|
||||||
|
### Vercel 部署
|
||||||
|
|
||||||
|
本项目支持通过 `Vercel` 进行一键部署,点击下方按钮或前往 [项目仓库](https://github.com/imsyy/DailyHotApi-Vercel) 进行手动部署
|
||||||
|
|
||||||
|
[](https://vercel.com/new/imsyys-projects/clone?repository-url=https%3A%2F%2Fgithub.com%2Fimsyy%2FDailyHotApi-Vercel)
|
||||||
|
|
||||||
|
### Railway 部署
|
||||||
|
|
||||||
|
本项目支持使用 [Railway](https://railway.app/) 一键部署,请先将本项目 fork 到您的仓库中,即可使用一键部署。
|
||||||
|
|
||||||
|
### Zeabur 部署
|
||||||
|
|
||||||
|
本项目支持使用 [Zeabur](https://zeabur.com/) 一键部署,请先将本项目 fork 到您的仓库中,即可使用一键部署。
|
||||||
|
|
||||||
|
## ⚠️ 须知
|
||||||
|
|
||||||
|
- 本项目为了避免频繁请求官方数据,默认对数据做了缓存处理,默认为 `60` 分钟,如需更改,请自行修改配置
|
||||||
- 本项目部分接口使用了 **页面爬虫**,若违反对应页面的相关规则,请 **及时通知我去除该接口**
|
- 本项目部分接口使用了 **页面爬虫**,若违反对应页面的相关规则,请 **及时通知我去除该接口**
|
||||||
|
|
||||||
## 免责声明
|
## 📢 免责声明
|
||||||
|
|
||||||
- 本项目提供的 `API` 仅供开发者进行技术研究和开发测试使用。使用该 `API` 获取的信息仅供参考,不代表本项目对信息的准确性、可靠性、合法性、完整性作出任何承诺或保证。本项目不对任何因使用该 `API` 获取信息而导致的任何直接或间接损失负责。本项目保留随时更改 `API` 接口地址、接口协议、接口参数及其他相关内容的权利。本项目对使用者使用 `API` 的行为不承担任何直接或间接的法律责任
|
- 本项目提供的 `API` 仅供开发者进行技术研究和开发测试使用。使用该 `API` 获取的信息仅供参考,不代表本项目对信息的准确性、可靠性、合法性、完整性作出任何承诺或保证。本项目不对任何因使用该 `API` 获取信息而导致的任何直接或间接损失负责。本项目保留随时更改 `API` 接口地址、接口协议、接口参数及其他相关内容的权利。本项目对使用者使用 `API` 的行为不承担任何直接或间接的法律责任
|
||||||
- 本项目并未与相关信息提供方建立任何关联或合作关系,获取的信息均来自公开渠道,如因使用该 `API` 获取信息而产生的任何法律责任,由使用者自行承担
|
- 本项目并未与相关信息提供方建立任何关联或合作关系,获取的信息均来自公开渠道,如因使用该 `API` 获取信息而产生的任何法律责任,由使用者自行承担
|
||||||
- 本项目对使用 `API` 获取的信息进行了最大限度的筛选和整理,但不保证信息的准确性和完整性。使用 `API` 获取信息时,请务必自行核实信息的真实性和可靠性,谨慎处理相关事项
|
- 本项目对使用 `API` 获取的信息进行了最大限度的筛选和整理,但不保证信息的准确性和完整性。使用 `API` 获取信息时,请务必自行核实信息的真实性和可靠性,谨慎处理相关事项
|
||||||
- 本项目保留对 `API` 的随时更改、停用、限制使用等措施的权利。任何因使用本 `API` 产生的损失,本项目不负担任何赔偿和责任
|
- 本项目保留对 `API` 的随时更改、停用、限制使用等措施的权利。任何因使用本 `API` 产生的损失,本项目不负担任何赔偿和责任
|
||||||
|
|
||||||
|
## 😘 鸣谢
|
||||||
|
|
||||||
|
特此感谢为本项目提供支持与灵感的项目
|
||||||
|
|
||||||
|
- [RSSHub](https://github.com/DIYgod/RSSHub)
|
||||||
|
|
||||||
|
## ⭐ Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#imsyy/DailyHotApi&Date)
|
||||||
|
|||||||
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
DailyhotApi:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: runner
|
||||||
|
ports:
|
||||||
|
- "6688:6688"
|
||||||
|
volumes:
|
||||||
|
- "./logs:/app/logs"
|
||||||
|
environment:
|
||||||
|
- PORT=6688
|
||||||
|
user: "114514"
|
||||||
|
restart: always
|
||||||
89
index.js
89
index.js
@@ -1,89 +0,0 @@
|
|||||||
require("dotenv").config();
|
|
||||||
const Koa = require("koa");
|
|
||||||
const bodyParser = require("koa-bodyparser");
|
|
||||||
const cors = require("koa2-cors");
|
|
||||||
const serve = require("koa-static");
|
|
||||||
const views = require("koa-views");
|
|
||||||
|
|
||||||
const app = new Koa();
|
|
||||||
const net = require("net");
|
|
||||||
const router = require("./routes");
|
|
||||||
|
|
||||||
// 配置信息
|
|
||||||
let domain = process.env.ALLOWED_DOMAIN || "*";
|
|
||||||
let port = process.env.PORT || 6688;
|
|
||||||
|
|
||||||
// 解析请求体
|
|
||||||
app.use(bodyParser());
|
|
||||||
|
|
||||||
// 静态文件目录
|
|
||||||
app.use(serve(__dirname + "/public"));
|
|
||||||
app.use(views(__dirname + "/public"));
|
|
||||||
|
|
||||||
// 跨域
|
|
||||||
app.use(
|
|
||||||
cors({
|
|
||||||
origin: domain,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(async (ctx, next) => {
|
|
||||||
if (domain === "*") {
|
|
||||||
await next();
|
|
||||||
} else {
|
|
||||||
if (ctx.headers.origin === domain || ctx.headers.referer === domain) {
|
|
||||||
await next();
|
|
||||||
} else {
|
|
||||||
ctx.status = 403;
|
|
||||||
ctx.body = {
|
|
||||||
code: 403,
|
|
||||||
message: "请通过正确的域名访问",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用路由中间件
|
|
||||||
app.use(router.routes());
|
|
||||||
app.use(router.allowedMethods());
|
|
||||||
|
|
||||||
// 启动应用程序并监听端口
|
|
||||||
const startApp = (port) => {
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(`成功在 ${port} 端口上运行`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检测端口是否被占用
|
|
||||||
const checkPort = (port) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const server = net
|
|
||||||
.createServer()
|
|
||||||
.once("error", (err) => {
|
|
||||||
if (err.code === "EADDRINUSE") {
|
|
||||||
console.log(`端口 ${port} 已被占用, 正在尝试其他端口...`);
|
|
||||||
server.close();
|
|
||||||
resolve(false);
|
|
||||||
} else {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.once("listening", () => {
|
|
||||||
server.close();
|
|
||||||
resolve(true);
|
|
||||||
})
|
|
||||||
.listen(port);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 尝试启动应用程序
|
|
||||||
const tryStartApp = async (port) => {
|
|
||||||
let isPortAvailable = await checkPort(port);
|
|
||||||
while (!isPortAvailable) {
|
|
||||||
port++;
|
|
||||||
isPortAvailable = await checkPort(port);
|
|
||||||
}
|
|
||||||
startApp(port);
|
|
||||||
};
|
|
||||||
|
|
||||||
tryStartApp(port);
|
|
||||||
76
package.json
76
package.json
@@ -1,27 +1,65 @@
|
|||||||
{
|
{
|
||||||
"name": "dailyhot_api",
|
"name": "dailyhot-api",
|
||||||
"version": "1.0.0",
|
"version": "2.0.2",
|
||||||
"description": "一个今日热榜",
|
"description": "An Api on Today's Hot list",
|
||||||
"main": "index.js",
|
"keywords": [
|
||||||
"scripts": {
|
"API",
|
||||||
"start": "node index.js",
|
"RSS"
|
||||||
"dev": "./node_modules/.bin/nodemon index.js",
|
],
|
||||||
"prd": "pm2 start index.js",
|
"homepage": "https://github.com/imsyy/DailyHotApi#readme",
|
||||||
"build": "node index.js"
|
"bugs": {
|
||||||
|
"url": "https://github.com/imsyy/DailyHotApi/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/imsyy/DailyHotApi.git"
|
||||||
},
|
},
|
||||||
"author": "imsyy",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"author": "imsyy",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"files": [
|
||||||
|
"LICENSE",
|
||||||
|
"README.md",
|
||||||
|
"dist/**/*",
|
||||||
|
"!dist/logs/**/*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||||
|
"dev": "cross-env NODE_ENV=development tsx watch --no-cache src/index.ts",
|
||||||
|
"dev:cache": "cross-env NODE_ENV=development tsx watch src/index.ts",
|
||||||
|
"build": "tsc --project tsconfig.json",
|
||||||
|
"start": "tsx src/index.ts"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.3.4",
|
"@hono/node-server": "^1.11.2",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
"cheerio": "1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"dotenv": "^16.0.3",
|
"dayjs": "^1.11.11",
|
||||||
"koa": "^2.14.1",
|
"dotenv": "^16.4.5",
|
||||||
"koa-bodyparser": "^4.3.0",
|
"feed": "^4.2.2",
|
||||||
"koa-router": "^12.0.0",
|
"hono": "^4.4.5",
|
||||||
"koa-static": "^5.0.0",
|
"md5": "^2.3.0",
|
||||||
"koa-views": "^8.0.0",
|
|
||||||
"koa2-cors": "^2.0.6",
|
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"nodemon": "^2.0.22"
|
"rss-parser": "^3.13.0",
|
||||||
|
"winston": "^3.13.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.13.0",
|
||||||
|
"@typescript-eslint/parser": "^7.13.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"tsx": "^3.14.0",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2823
pnpm-lock.yaml
generated
2823
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
195
public/404.html
195
public/404.html
@@ -1,195 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>404 | Linkbook API</title>
|
|
||||||
<link
|
|
||||||
rel="shortcut icon"
|
|
||||||
href="https://img.imsyy.top/logo/imsyy.png"
|
|
||||||
type="image/x-icon"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css"
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--text-color: #000;
|
|
||||||
--text-color-gray: #888;
|
|
||||||
--text-color-hover: #fff;
|
|
||||||
--icon-color: #444;
|
|
||||||
}
|
|
||||||
html.dark-mode {
|
|
||||||
--text-color: #fff;
|
|
||||||
--text-color-gray: #888;
|
|
||||||
--text-color-hover: #3c3c3c;
|
|
||||||
--icon-color: #cbcbcb;
|
|
||||||
}
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
html {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
background-color: var(--text-color-hover);
|
|
||||||
color: var(--text-color);
|
|
||||||
font-family: "PingFang SC", "Open Sans", "Microsoft YaHei", sans-serif;
|
|
||||||
transition: background-color 0.5s, color 0.5s;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.dark-mode body {
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text-color-gray);
|
|
||||||
transition: all 0.5s ease;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
.ico {
|
|
||||||
margin: 4rem 2rem;
|
|
||||||
font-size: 6rem;
|
|
||||||
color: var(--text-color-gray);
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
.control button {
|
|
||||||
background-color: var(--text-color-hover);
|
|
||||||
border: var(--text-color) solid;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
transition: all 0.5s ease;
|
|
||||||
margin: 1rem 0.2rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.control button:hover {
|
|
||||||
border: var(--text-color) solid;
|
|
||||||
background: var(--text-color);
|
|
||||||
color: var(--text-color-hover);
|
|
||||||
}
|
|
||||||
.control button i {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
line-height: 1.75rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 140px;
|
|
||||||
}
|
|
||||||
footer .social {
|
|
||||||
color: var(--icon-color);
|
|
||||||
font-size: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
footer .social i {
|
|
||||||
margin: 0 12px;
|
|
||||||
}
|
|
||||||
footer .point::before {
|
|
||||||
content: "·";
|
|
||||||
color: var(--text-color-gray);
|
|
||||||
}
|
|
||||||
.power,
|
|
||||||
.icp {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<div class="ico"><i class="fa-solid fa-circle-exclamation"></i></div>
|
|
||||||
<div class="title">404 Not Found</div>
|
|
||||||
<div class="text">请检查您的路径</div>
|
|
||||||
<div class="control">
|
|
||||||
<button onclick="window.location.href = '/'">
|
|
||||||
<i class="fa-solid fa-house"></i>
|
|
||||||
<span>回到首页</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<div class="social">
|
|
||||||
<i class="fa-brands fa-github" onclick="socialJump('github')"></i>
|
|
||||||
<div class="point"></div>
|
|
||||||
<i class="fa-solid fa-house" onclick="socialJump('home')"></i>
|
|
||||||
<div class="point"></div>
|
|
||||||
<i class="fa-solid fa-envelope" onclick="socialJump('email')"></i>
|
|
||||||
</div>
|
|
||||||
<div class="power">
|
|
||||||
Copyright © 2020
|
|
||||||
<script>
|
|
||||||
document.write(" - " + new Date().getFullYear());
|
|
||||||
</script>
|
|
||||||
<a href="https://imsyy.top/" target="_blank">無名</a>
|
|
||||||
</div>
|
|
||||||
<div class="icp">
|
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank"
|
|
||||||
>豫ICP备2022018134号-1</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
<script>
|
|
||||||
// 跟随系统主题
|
|
||||||
const darkModeMediaQuery = window.matchMedia(
|
|
||||||
"(prefers-color-scheme: dark)"
|
|
||||||
);
|
|
||||||
const toggleDarkMode = (darkModeMediaQuery) => {
|
|
||||||
if (darkModeMediaQuery.matches) {
|
|
||||||
document.documentElement.classList.add("dark-mode");
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove("dark-mode");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
darkModeMediaQuery.addListener(toggleDarkMode);
|
|
||||||
toggleDarkMode(darkModeMediaQuery);
|
|
||||||
|
|
||||||
// 按钮事件
|
|
||||||
const clickFunction = () => {
|
|
||||||
window.location.href = "/api/links";
|
|
||||||
};
|
|
||||||
|
|
||||||
// 社交链接跳转
|
|
||||||
const socialJump = (type) => {
|
|
||||||
switch (type) {
|
|
||||||
case "github":
|
|
||||||
window.location.href = "https://github.com/imsyy/";
|
|
||||||
break;
|
|
||||||
case "home":
|
|
||||||
window.location.href = "https://www.imsyy.top/";
|
|
||||||
break;
|
|
||||||
case "email":
|
|
||||||
window.location.href = "mailto:one@imsyy.top";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
|
|
||||||
<title>favicon-svg</title>
|
|
||||||
<defs>
|
|
||||||
<image width="1024" height="1024" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAQAAQMAAABF07nAAAAAAXNSR0IB2cksfwAAAAZQTFRF/wgIgICAg2lxxgAAAAJ0Uk5TAP9bkSK1AAANqElEQVR4nO2dzXErOw6FNTWLWb4QGApDI0NjKAzBSy1U0lRbliVL/QOSODisemAAF181ge+g5WvrdBo4/9y+z9fIvzFenoaQbi/nSq5PIIi3t3OxrR/e699uZ8v6/3zWN+3E/67Vv92KGcB6/dvNqv5KA5i2wcYF2F1C3AYwmcX/bde/3aoBQNoDMBDi7gOweAS7D8DgERw8APwjiEcA4EHYccDjFCjA4QMAP4L/HNe/3TIQIEgAkIkgqY8MxcMZvJ8KA4gyAFgbilpwORkEsLoJrh3UdpikAKBAEFjwcQoEIMgBMCoQ3wDoDhpuAHMHDTeAuYOGG4DcQdMNIO4gtAHo30FqA1C/g8Yb0L8DcQ48jnYepFYA5TsQJ/HzZFWA5hvQvoPYDqC7F7XX191Nm4dwOUURIPQAaMow9QAoDmLHEC4nqwEIX0jeT1UDCH0Aek2Q+gDUmqCzBfSaoLMF9Jog9AJoNUHqBVBqgu4W0GqC7hbQaoLQD6DTBKkfQKUJBlpApwkGWkCnCcIIgEYTxBEAjcVwpL7GYti1Dj5PGQboeCN4PeNvB3EMYLwJ0hjAsIqGNLScPAgwpKHl1EGAwR4c78I4CjDahaP1R1U03IOjXTjcg6NdGMYBxgIxjgOMdeF4/bEuHIzC+ykDAAo9ONaFQQNgpAujBsBIF2rUH+lCBQ8uJ3cDqPTgSBcOZ/H99Cdy1AHo78KkA9C9Fyr1YH8Xqoh4OaUTQKkH+7swaAH0yjhqAfSOgVb9XhmrDUHvGCiJeDm1C0BtCHrHIOgB9I1B0gPok7Fe/b4xUBPxckoHgOIQ9I2B4hD0jUHUBOiRcdIE6BkDzfo9Y6CYBMvJzQCqQ9AzBqpD0DMGQRegPQ2SLkD7GOjWbx8D5SFoHwPVKFpOaQRQHoL2MQjaAK1jELUBWuMoaQO0zqF2/dY5VB+C1jFQjqLl1CYA9SlsncOgD9A2h1EfoG0Okz5A0xyqR9FycgMAYArb5hAwhW1zCJjCtjkMCICWOYwIgJY5TAiAljlE1G/JQ8gUtswhZApb5hAyhS1zGDAA8jmMGAD5HCYMgHwOMfXlcwgJ4+VkIQBIA3IRgDQgFwFIA3IRBBSAVAQRBSAVQUIBSEWAqi8VAUwDUhHANCAVAUwDUhHANCAVQcAByEQQcQAyESQcgEwEuPoyEQA1IBMBUAMyEQA1IBMBUAMyEQQkgEQEEQkgEUFCAkhEgKwvEQFUAxIRQDUgEQFUAxIR0AGgHpKYKGABjk0UsQDHJkpYgGMTYesfmwjsoWMTgT10bCKwBo5FQAcAe+jYRAENcGSiiAY4MlFCAxyZCF3/yERwDx2ZiA4AF+GRCuEeOjIR3ENHJgp4gH0TRTzAvonoAAkPsK9CfP19FRp4aN9EBh7aNxEdwECE+yo0EOG+CoMFwJ4K6QDRAmBPhckCYE+FFvX3VGgiwj0V0gFMRLinQhMR7qnQRIR7KqQDBBuAbRVGG4BtFdIBkg3Atott6m+72EiE2yqkAxiZeNvFRibedjEdwMjE2y4OVgBbLo5WAFsqpAMkK4AtF1vV33QxG8DMxFsuNjPxlovpAGYm3nKxmYm3XEwHCHYA62EQ7QDWXUwHSHYA62FgV3/DxWwAwyhYDwNDE6+7mA5gGAXrYUAHMIyC9TAIlgBrYUAHiJYAa2FAB0iWAGtpZFl/NQzYAKZZtJZGdADTLFpLI9MoWAsDOoBpFq2lER0g2AJ8plG0BfgMAzpAsgX4TCM6gG39lTRiAxhn0Wca0QGMw/AzDo2z6DON6ADGWfSZRnSAYA3wHod0gGgN8B6HdIBkDfAeh3QA6/ofccgGMA/D9zikA5in8Xse0wHM0/g9j+kA5mn8nsd0gGAPcJ4LINoDXOYCSPYA17kA7Ou/5TEbgJDGf/OYDkBI4795TAcgpPHfPHYAwjrwdyGgAwQGwHkmgMgAuMwEkBgAVwd4AWDU/7ORsAEo+8jrRuIAlIXodSWiA1D2kdeNxAEoC9HrSkQHCByAswP8AkQOwMUBfgESB+A6DwCn/stO9q8HIG1kz53MAUgr4XMppAOQNrLnTuYApJXwuRQ6QGABnB2AuxI+l0IHSCyAqwOQd9LfrfRfD0DbSR9bqQPQluLHWuwAtKX4sRY7AB2AtpU/9nIHCDyAswPMARB5ABcHmAMg8QCuDkB+L/l5M3EANgDxxej+auQAdADim9n93cwB6ADEN7P7u5kD0AGIr4b3l0MHcIDABDg7wAwAkQlwcQAH4L6d39/PHcABmPW/PyBwAAdgA1A/IFk+InEAB6ADUD8iWj4kcgAHcADqp3TL53QO4AAOQP2ccvmk0gEcwAECF+DsAA7gAHyAyAW4OIADOIADOAD3Z1bLT60cwAEcwAEcwAEcgA7ArX+7OYADOIADOIADOIADOIADOIADOIADOIADOIADOIADOIADOIADOIADOAAfIHHrT/CTUwdwAAdwAAdwAAeIXIAJ/me1AziAAzgAHSBwASb4fUMHcAAHoP/6vwM4gAPQ/xiOAziAA9D/NJwDOIAD0P9QqgM4AB2A/kezHcAB+ACJWX+Kb3BwADpAZAJM8V02DkAHCEyAKb7VywHoX67nAA5A/5pRB6AD0L9u2AHoAPSvHXcAOgD1A4KTA0wBkHj1rw7wDRB5ABcHmAMg8ADODjAHAPHl8MsBvgGI72bVAeYAIL6bFQf4BiC+GmUHOJHfTE4OcOK+mVwd4AcgsgAuDvADEFgAZwf4AaDt5V8O8ANAW4urA/wA0NbiMgsAbSvNDnAib6UnB3icxKl/nQcgcgAuDvALEDgAZwf4BSAthV/zAJB2suoAvwCkpbDMA0DaybID/AKQdrLTRACJUf/qAC8AkQFwmQkgMADOMwFQVqKvmQAoG0l1gBcAykpUZgKgbCR5JgDKRnKaCiDZ17/OBRDtAS5zAQR7gPNcAISF4GsuAEIe17kACHlc5gIg5HGeC4CQx6fJAJJ1/etsANEa4DIbQLAGOM8GYJ7HX7MBmOdxnQ3API/LG4B5HObZAMzj8L0+HyDZ1n8PwwkAoi3AexhOABBsAd7D0DyN3rNoAgDjNKrzARjHYfkAME6jPB+AcRp91ucDJMv6n1lkHAafUTABQLAE+MyiCQBM0+gzi4zDoM4IYJpGZUYA0zTKKwCmYbBWnw+Q7OqvZZFpGKxFwQQAwQ5gLQpMw2AtCiYAMAyDugpg6OIyJ4BhGORVAEMXr9fnAySr+utRYOjidRNPABCsANajwNDF6yY2dHGdFcDMxWUDwMzFeVYAMxdv1bdy8ZaJzVS4JcIJAIINwJaJzVy8ZeIJAIxcXDcBjFxcNgGMVJjnBTBy8XZ9Gxdvm9hIhdsiNFLhtggnADBR4bYIjVRYdwBMVFhmBjBRYd4BMFHhXn0LFe6J0ESFeyKcACDgAfZEaKLCPRGaqLDuAhiYqOwCGJgozw1goML9+ngV7ovQwET7HjIw0b6HDEy07yEDE9XZAeAqLAcAcBPlAwC4iY7qo0105CG4iY48BDfRkYfgJjryEFwEdX4AsInKIQDYRPkQAGyi4/pYEx17CGyiYw+BTXTsIbCJjj0EFkEVAEBFUAQAUBFkAQBUBJL6SBFINAAVgUQDUBFINAAVgUQDUBFUEQBQBEUEABRBFgEARSCrjxOBTANAEcg0ABSBTANAEcg0ABRBFQLARFCEADARZCEATATS+igRSDUAE4FUAzARSDUAE4FUAzARVDEASARFDACaQ3l9zBzKpxA0h/IpBM2hfApBcyifQtAc1gYAyByWBgBIIOcGAMQctkwhZA5bphAyhy1TCJnDlimEzGFtAgDMYWkCAORhW339OWybQsActk0hYA7bphAwh21TCBiD0gigHke5EUB9Dlvra89h6xSqj0HrEKiPQesQqMdRbQZQHoPcDKA8Bu31dcegfQiU46g1ipajOgbtQ6A8BrUDQDWOSgeA6hj01Nccg54hUE2D9iRYjuIY9AyB6hjULgDFNMhdAIpj0FdfT8Y9Il5O0ALoGwLFMegbAkUZl04AtTHInQBaMu4T8XKiDkDvEKh1YW8Pqsm4dgModWHuBlCScX99nS7s70ElGfeKeDkqXVgHAFRkXAYAVLpwpL5GF470oEoXjvSgShfWIQAFF+YhAIUuHKs/3oVjPaiQyP1ZfD/DXVgHAYa7MA8CjO6F/fvg48QxgNEeHO7C0R4cDsQyDDCoovH6Y00w3gKDgTgWhfczpKKqADCkoqwAMKKicQ0tJ/QDaLTAUBNUFYCBJsgqAP1NoNMCA02g0wIDTVCVALqbICsB9DaBVgt0N4FWC3Q3QVUD6GyCrAbQ1wR6LdDZBHot0LkYFkWArsVQs37PYqixDj5Px9vB+BvB6+kYxKwK0D6ImkO4nOY70L2BjkEsygCtd6B9A80y1NTg/TTeQVEHaLsD/RtovAP9G2i8gwIAaLkDxA003QHiBpruoEAA5HeAuYGGPNDOgccRZ3IGAUj3It1d6PUIX1AqDEC4m+Lqy1SAkcD9iNowAwEkbYhrweUIbFigAMePAPsABJNYwQBHgYCKgec5eAQVDrD/CPAP4OARVAOAvUFAj8D97LigmABsJwIyBf6cLQCr+luXUMwA1rdD1Ca4elbawKwB7ie+17eZwJeT/ta3UOAeAaH+n0407b9PhKHy/wd1WlnkrMMslQAAAABJRU5ErkJggg=="/>
|
|
||||||
</defs>
|
|
||||||
<style>
|
|
||||||
.s0 { fill: #ffffff }
|
|
||||||
</style>
|
|
||||||
<use id="Layer" href="#img1" x="0" y="0"/>
|
|
||||||
<path id="Layer" class="s0" d="m408.6 448.2v-0.5q0-0.8-0.1-1.7 0-0.9 0-1.7 0-0.9 0-1.7-0.1-0.9-0.1-1.8v0.3-1.1q0-0.3 0-0.5 0-0.3 0-0.6 0-0.3-0.1-0.6 0-0.2 0-0.5 0.1-1 0.1-2 0-0.9 0-1.9 0.1-1 0.1-1.9 0.1-1 0.2-1.9l-0.2 1.7 0.2-2.7c1.2-14 5-31.4 10.8-49.2 9.6-27.6 26.1-57.3 52.3-79.2 35.1-29.4 85.5-44.7 135.9-44.7 5.7 0 11 2.9 13.8 7.5 2.9 4.6 2.9 10.3 0.1 15-2 3.2-86.3 81.7 11.5 137.3 38.8 22.1 96.5 79.9 96.5 148.8 0 54-22.6 105-63.7 143.5-41.2 38.6-95.9 59.9-153.9 59.9-57.8 0-112.5-21.6-153.9-60.7q-7.3-6.9-14-14.4-6.6-7.6-12.4-15.7-5.9-8.2-10.9-16.8-5.1-8.7-9.3-17.8-4.2-9.2-7.4-18.7-3.2-9.6-5.3-19.4-2.2-9.8-3.3-19.8-1.1-10-1.1-20.1c0-52.3 26.3-110.3 62.9-119.4l0.2 11c0.1 3.4 0.2 6.7 0.5 10.1l0.5 6.8c0.4 3.5 0.8 7 1.3 10.7l1.2 7.8 1.4 8.5 0.8 4.6 1.8 10c1.6 8.8 4.2 18.8 7.7 30.1 2 6.5 6.5 12 12.5 15.1 6 3.2 13 3.9 19.5 1.9 6.5-2 11.9-6.5 15.1-12.5 3.2-6 3.8-13 1.8-19.5q-0.9-3-1.8-6-0.9-3.1-1.7-6.2-0.7-3-1.4-6.1-0.7-3.1-1.4-6.2l-2.3-13-1.3-7.5-1-6.8q0-0.4-0.1-0.8-0.1-0.4-0.1-0.8-0.1-0.4-0.1-0.8-0.1-0.4-0.1-0.8l-0.7-6-0.5-5.7q0-0.3-0.1-0.6 0-0.3 0-0.6 0-0.3 0-0.6-0.1-0.3-0.1-0.6l-0.2-5-0.1-6.6z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 6.1 KiB |
BIN
public/ico/error.png
Normal file
BIN
public/ico/error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/ico/favicon.png
Normal file
BIN
public/ico/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
@@ -1,201 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>DailyHot API</title>
|
|
||||||
<link
|
|
||||||
rel="shortcut icon"
|
|
||||||
href="./favicon.svg"
|
|
||||||
type="image/x-icon"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css"
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--text-color: #000;
|
|
||||||
--text-color-gray: #888;
|
|
||||||
--text-color-hover: #fff;
|
|
||||||
--icon-color: #444;
|
|
||||||
}
|
|
||||||
html.dark-mode {
|
|
||||||
--text-color: #fff;
|
|
||||||
--text-color-gray: #888;
|
|
||||||
--text-color-hover: #3c3c3c;
|
|
||||||
--icon-color: #cbcbcb;
|
|
||||||
}
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
html {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
background-color: var(--text-color-hover);
|
|
||||||
color: var(--text-color);
|
|
||||||
font-family: "PingFang SC", "Open Sans", "Microsoft YaHei", sans-serif;
|
|
||||||
transition: background-color 0.5s, color 0.5s;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.dark-mode body {
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text-color-gray);
|
|
||||||
transition: all 0.5s ease;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
.ico {
|
|
||||||
margin: 4rem 2rem;
|
|
||||||
font-size: 6rem;
|
|
||||||
color: var(--text-color-gray);
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
.control button {
|
|
||||||
background-color: var(--text-color-hover);
|
|
||||||
border: var(--text-color) solid;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
transition: all 0.5s ease;
|
|
||||||
margin: 1rem 0.2rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.control button:hover {
|
|
||||||
border: var(--text-color) solid;
|
|
||||||
background: var(--text-color);
|
|
||||||
color: var(--text-color-hover);
|
|
||||||
}
|
|
||||||
.control button i {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
line-height: 1.75rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 140px;
|
|
||||||
}
|
|
||||||
footer .social {
|
|
||||||
color: var(--icon-color);
|
|
||||||
font-size: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
footer .social i {
|
|
||||||
margin: 0 12px;
|
|
||||||
}
|
|
||||||
footer .point::before {
|
|
||||||
content: "·";
|
|
||||||
color: var(--text-color-gray);
|
|
||||||
}
|
|
||||||
.power,
|
|
||||||
.icp {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<div class="ico"><i class="fa-solid fa-code"></i></div>
|
|
||||||
<div class="title">DailyHot API</div>
|
|
||||||
<div class="text">服务已正常运行</div>
|
|
||||||
<div class="control">
|
|
||||||
<button onclick="clickFunction()">
|
|
||||||
<i class="fa-solid fa-vial"></i>
|
|
||||||
<span>测试接口</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onclick="window.open(`https://www.apifox.cn/apidoc/shared-ed2f1803-746b-42bb-8321-b0f0bbc6634c`)"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-book"></i>
|
|
||||||
<span>接口文档</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<div class="social">
|
|
||||||
<i class="fa-brands fa-github" onclick="socialJump('github')"></i>
|
|
||||||
<div class="point"></div>
|
|
||||||
<i class="fa-solid fa-house" onclick="socialJump('home')"></i>
|
|
||||||
<div class="point"></div>
|
|
||||||
<i class="fa-solid fa-envelope" onclick="socialJump('email')"></i>
|
|
||||||
</div>
|
|
||||||
<div class="power">
|
|
||||||
Copyright © 2020
|
|
||||||
<script>
|
|
||||||
document.write(" - " + new Date().getFullYear());
|
|
||||||
</script>
|
|
||||||
<a href="https://imsyy.top/" target="_blank">無名</a>
|
|
||||||
</div>
|
|
||||||
<div class="icp">
|
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank"
|
|
||||||
>豫ICP备2022018134号-1</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
<script>
|
|
||||||
// 跟随系统主题
|
|
||||||
const darkModeMediaQuery = window.matchMedia(
|
|
||||||
"(prefers-color-scheme: dark)"
|
|
||||||
);
|
|
||||||
const toggleDarkMode = (darkModeMediaQuery) => {
|
|
||||||
if (darkModeMediaQuery.matches) {
|
|
||||||
document.documentElement.classList.add("dark-mode");
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove("dark-mode");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
darkModeMediaQuery.addListener(toggleDarkMode);
|
|
||||||
toggleDarkMode(darkModeMediaQuery);
|
|
||||||
|
|
||||||
// 按钮事件
|
|
||||||
const clickFunction = () => {
|
|
||||||
window.location.href = "/bilibili";
|
|
||||||
};
|
|
||||||
|
|
||||||
// 社交链接跳转
|
|
||||||
const socialJump = (type) => {
|
|
||||||
switch (type) {
|
|
||||||
case "github":
|
|
||||||
window.location.href = "https://github.com/imsyy/";
|
|
||||||
break;
|
|
||||||
case "home":
|
|
||||||
window.location.href = "https://www.imsyy.top/";
|
|
||||||
break;
|
|
||||||
case "email":
|
|
||||||
window.location.href = "mailto:one@imsyy.top";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
137
routes/36kr.js
137
routes/36kr.js
@@ -1,137 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const krRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "36氪",
|
|
||||||
subtitle: "热榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "krData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://gateway.36kr.com/api/mis/nav/home/nav/rank/hot";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.map((v) => {
|
|
||||||
return {
|
|
||||||
id: v.itemId,
|
|
||||||
title: v.templateMaterial.widgetTitle,
|
|
||||||
pic: v.templateMaterial.widgetImage,
|
|
||||||
owner: v.templateMaterial.authorName,
|
|
||||||
hot: v.templateMaterial.statRead,
|
|
||||||
data: v.templateMaterial,
|
|
||||||
url: `https://www.36kr.com/p/${v.itemId}`,
|
|
||||||
mobileUrl: `https://www.36kr.com/p/${v.itemId}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 36氪热榜
|
|
||||||
krRouter.get("/36kr", async (ctx) => {
|
|
||||||
console.log("获取36氪热榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取36氪热榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.post(url, {
|
|
||||||
partner_id: "wap",
|
|
||||||
param: {
|
|
||||||
siteId: 1,
|
|
||||||
platformId: 2,
|
|
||||||
},
|
|
||||||
timestamp: new Date().getTime(),
|
|
||||||
});
|
|
||||||
data = getData(response.data.data.hotRankList);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 36氪热榜 - 获取最新数据
|
|
||||||
krRouter.get("/36kr/new", async (ctx) => {
|
|
||||||
console.log("获取36氪热榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.post(url, {
|
|
||||||
partner_id: "wap",
|
|
||||||
param: {
|
|
||||||
siteId: 1,
|
|
||||||
platformId: 2,
|
|
||||||
},
|
|
||||||
timestamp: new Date().getTime(),
|
|
||||||
});
|
|
||||||
const newData = getData(response.data.data.hotRankList);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取36氪热榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
krRouter.info = routerInfo;
|
|
||||||
module.exports = krRouter;
|
|
||||||
135
routes/baidu.js
135
routes/baidu.js
@@ -1,135 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const baiduRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = { title: "百度", subtitle: "热搜榜" };
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "baiduData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://top.baidu.com/board?tab=realtime";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
const dataList = [];
|
|
||||||
try {
|
|
||||||
const pattern = /<\!--s-data:(.*?)-->/s;
|
|
||||||
const matchResult = data.match(pattern);
|
|
||||||
const jsonObject = JSON.parse(matchResult[1]).cards[0].content;
|
|
||||||
jsonObject.forEach((v) => {
|
|
||||||
dataList.push({
|
|
||||||
title: v.query,
|
|
||||||
desc: v.desc,
|
|
||||||
pic: v.img,
|
|
||||||
hot: Number(v.hotScore),
|
|
||||||
url: `https://www.baidu.com/s?wd=${encodeURIComponent(v.query)}`,
|
|
||||||
mobileUrl: v.url,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return dataList;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("数据处理出错" + error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 百度热搜
|
|
||||||
baiduRouter.get("/baidu", async (ctx) => {
|
|
||||||
console.log("获取百度热搜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取百度热搜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
if (!data) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 百度热搜 - 获取最新数据
|
|
||||||
baiduRouter.get("/baidu/new", async (ctx) => {
|
|
||||||
console.log("获取百度热搜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取百度热搜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
baiduRouter.info = routerInfo;
|
|
||||||
module.exports = baiduRouter;
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const bilibiliRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "哔哩哔哩",
|
|
||||||
subtitle: "热门榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "bilibiliData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://api.bilibili.com/x/web-interface/ranking/v2";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.map((v) => {
|
|
||||||
return {
|
|
||||||
id: v.bvid,
|
|
||||||
title: v.title,
|
|
||||||
desc: v.desc,
|
|
||||||
pic: v.pic.replace(/http:/, "https:"),
|
|
||||||
owner: v.owner,
|
|
||||||
data: v.stat,
|
|
||||||
hot: v.stat.view,
|
|
||||||
url: v.short_link_v2 || `https://b23.tv/${v.bvid}`,
|
|
||||||
mobileUrl: `https://m.bilibili.com/video/${v.bvid}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 哔哩哔哩热门榜
|
|
||||||
bilibiliRouter.get("/bilibili", async (ctx) => {
|
|
||||||
console.log("获取哔哩哔哩热门榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取哔哩哔哩热门榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data.data.list);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 哔哩哔哩热门榜 - 获取最新数据
|
|
||||||
bilibiliRouter.get("/bilibili/new", async (ctx) => {
|
|
||||||
console.log("获取哔哩哔哩热门榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data.data.list);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取哔哩哔哩热门榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
bilibiliRouter.info = routerInfo;
|
|
||||||
module.exports = bilibiliRouter;
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const calendarRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "calendarData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 获取月份
|
|
||||||
const month = (new Date().getMonth() + 1).toString().padStart(2, "0");
|
|
||||||
|
|
||||||
// 获取天数
|
|
||||||
const day = new Date().getDate().toString().padStart(2, "0");
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = `https://baike.baidu.com/cms/home/eventsOnHistory/${month}.json`;
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.map((v) => {
|
|
||||||
return {
|
|
||||||
year: v.year,
|
|
||||||
title: v.title.replace(/<[^>]+>/g, ""),
|
|
||||||
desc: v.desc.replace(/<[^>]+>/g, ""),
|
|
||||||
pic: v?.pic_share || v?.pic_index,
|
|
||||||
avatar: v?.pic_calendar,
|
|
||||||
type: v.type,
|
|
||||||
url: v.link,
|
|
||||||
mobileUrl: v.link,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 历史上的今天
|
|
||||||
calendarRouter.get("/calendar", async (ctx) => {
|
|
||||||
console.log("获取历史上的今天");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取历史上的今天");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data[month][month + day]);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
if (!data) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
title: "历史上的今天",
|
|
||||||
subtitle: month + "-" + day,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
title: "历史上的今天",
|
|
||||||
subtitle: month + "-" + day,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 历史上的今天 - 获取指定日期
|
|
||||||
calendarRouter.get("/calendar/date", async (ctx) => {
|
|
||||||
console.log("获取历史上的今天 - 指定日期");
|
|
||||||
try {
|
|
||||||
// 获取参数
|
|
||||||
const { month, day } = ctx.query;
|
|
||||||
if (!month || !day) {
|
|
||||||
ctx.body = { code: 400, message: "参数不完整" };
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (month.length == 1 || day.length == 1) {
|
|
||||||
ctx.body = { code: 400, message: "参数格式错误" };
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(
|
|
||||||
`https://baike.baidu.com/cms/home/eventsOnHistory/${month}.json`
|
|
||||||
);
|
|
||||||
const newData = getData(response.data[month][month + day]);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取历史上的今天");
|
|
||||||
// 返回数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
title: "历史上的今天",
|
|
||||||
subtitle: month + "-" + day,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
// 返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
title: "历史上的今天",
|
|
||||||
subtitle: month + "-" + day,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = calendarRouter;
|
|
||||||
185
routes/douyin.js
185
routes/douyin.js
@@ -1,185 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const douyinRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "抖音",
|
|
||||||
subtitle: "热点榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "douyinHotData";
|
|
||||||
const cacheCookieKey = "douyinCookieData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url =
|
|
||||||
"https://www.douyin.com/aweme/v1/web/hot/search/list/?device_platform=webapp&aid=6383&channel=channel_pc_web&detail_list=1&round_trip_time=50";
|
|
||||||
|
|
||||||
// Token 获取路径
|
|
||||||
const cookisUrl =
|
|
||||||
"https://www.douyin.com/passport/general/login_guiding_strategy/?aid=6383";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
const dataList = [];
|
|
||||||
try {
|
|
||||||
const jsonObject = data.data.word_list;
|
|
||||||
jsonObject.forEach((v) => {
|
|
||||||
dataList.push({
|
|
||||||
title: v.word,
|
|
||||||
pic: `${v.word_cover.url_list[0]}`,
|
|
||||||
hot: Number(v.hot_value),
|
|
||||||
url: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`,
|
|
||||||
mobileUrl: `https://www.douyin.com/hot/${encodeURIComponent(
|
|
||||||
v.sentence_id
|
|
||||||
)}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return dataList;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("数据处理出错" + error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理抖音 Cookis
|
|
||||||
const setDouyinCookies = (data) => {
|
|
||||||
if (!data) return null;
|
|
||||||
try {
|
|
||||||
const pattern = /passport_csrf_token=(.*); Path/s;
|
|
||||||
const matchResult = data.headers["set-cookie"][0].match(pattern);
|
|
||||||
const cookieData = matchResult[1];
|
|
||||||
return cookieData;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取抖音 Cookie 出错" + error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取抖音 Cookie数据
|
|
||||||
const getDouyinCookie = async () => {
|
|
||||||
try {
|
|
||||||
let cookie = await get(cacheCookieKey);
|
|
||||||
if (!cookie) {
|
|
||||||
const cookisResponse = await axios.get(cookisUrl);
|
|
||||||
cookie = setDouyinCookies(cookisResponse);
|
|
||||||
console.log("抖音 Cookie 写入缓存", cookie);
|
|
||||||
await set(cacheCookieKey, cookie);
|
|
||||||
}
|
|
||||||
return cookie;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取抖音 Cookie 出错", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 抖音热点榜
|
|
||||||
douyinRouter.get("/douyin", async (ctx) => {
|
|
||||||
console.log("获取抖音热点榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const cookie = await getDouyinCookie();
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取抖音热点榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url, {
|
|
||||||
headers: {
|
|
||||||
Cookie: `passport_csrf_token=${cookie}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
data = getData(response.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
if (!data) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 抖音热点榜 - 获取最新数据
|
|
||||||
douyinRouter.get("/douyin/new", async (ctx) => {
|
|
||||||
console.log("获取抖音热点榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const cookie = await getDouyinCookie();
|
|
||||||
const response = await axios.get(url, {
|
|
||||||
headers: {
|
|
||||||
Cookie: `passport_csrf_token=${cookie}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const newData = getData(response.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取抖音热点榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
douyinRouter.info = routerInfo;
|
|
||||||
module.exports = douyinRouter;
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const Router = require("koa-router");
|
|
||||||
|
|
||||||
const router = new Router();
|
|
||||||
|
|
||||||
// 全部路由数据
|
|
||||||
const allRouterInfo = {
|
|
||||||
name: "全部接口",
|
|
||||||
subtitle: "除了特殊接口外的全部接口列表",
|
|
||||||
total: 0,
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// 根目录
|
|
||||||
router.get("/", async (ctx) => {
|
|
||||||
await ctx.render("index");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 遍历所有路由模块
|
|
||||||
fs.readdirSync(__dirname)
|
|
||||||
.filter((filename) => filename.endsWith(".js") && filename !== "index.js")
|
|
||||||
.forEach((filename) => {
|
|
||||||
const routerPath = path.join(__dirname, filename);
|
|
||||||
const routerModule = require(routerPath);
|
|
||||||
// 自动注册路由
|
|
||||||
if (routerModule instanceof Router) {
|
|
||||||
// 写入路由数据
|
|
||||||
if (routerModule?.info) {
|
|
||||||
allRouterInfo.total++;
|
|
||||||
allRouterInfo.data.push({
|
|
||||||
...routerModule.info,
|
|
||||||
stack: routerModule.stack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 引用路由
|
|
||||||
router.use(routerModule.routes());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 全部接口路由
|
|
||||||
router.get("/all", async (ctx) => {
|
|
||||||
console.log("获取全部接口路由");
|
|
||||||
if (allRouterInfo.total > 0) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...allRouterInfo,
|
|
||||||
};
|
|
||||||
} else if (allRouterInfo.total === 0) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "暂无接口,请添加",
|
|
||||||
...allRouterInfo,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
message: "获取失败",
|
|
||||||
...allRouterInfo,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 404 路由
|
|
||||||
router.use(async (ctx) => {
|
|
||||||
await ctx.render("404");
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
163
routes/ithome.js
163
routes/ithome.js
@@ -1,163 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const itHomeRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const cheerio = require("cheerio");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "IT之家",
|
|
||||||
subtitle: "热榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "itHomeData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://m.ithome.com/rankm/";
|
|
||||||
const headers = {
|
|
||||||
"User-Agent":
|
|
||||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1",
|
|
||||||
};
|
|
||||||
|
|
||||||
// it之家特殊处理 - url
|
|
||||||
const replaceLink = (url) => {
|
|
||||||
const match = url.match(/[html|live]\/(\d+)\.htm/)[1];
|
|
||||||
return `https://www.ithome.com/0/${match.slice(0, 3)}/${match.slice(3)}.htm`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return false;
|
|
||||||
const dataList = [];
|
|
||||||
const $ = cheerio.load(data);
|
|
||||||
try {
|
|
||||||
$(".rank-name").each(function () {
|
|
||||||
const type = $(this).data("rank-type");
|
|
||||||
const newListHtml = $(this).next(".rank-box").html();
|
|
||||||
cheerio
|
|
||||||
.load(newListHtml)(".placeholder")
|
|
||||||
.get()
|
|
||||||
.map((v) => {
|
|
||||||
console.log($(v));
|
|
||||||
dataList.push({
|
|
||||||
title: $(v).find(".plc-title").text(),
|
|
||||||
img: $(v).find("img").attr("data-original"),
|
|
||||||
time: $(v).find(".post-time").text(),
|
|
||||||
type: $(this).text(),
|
|
||||||
typeName: type,
|
|
||||||
hot: Number($(v).find(".review-num").text().replace(/\D/g, "")),
|
|
||||||
url: replaceLink($(v).find("a").attr("href")),
|
|
||||||
mobileUrl: $(v).find("a").attr("href"),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// dataList[type] = {
|
|
||||||
// name: $(this).text(),
|
|
||||||
// total: newsList.length,
|
|
||||||
// list: newsList,
|
|
||||||
// };
|
|
||||||
});
|
|
||||||
return dataList;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("数据处理出错" + error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// IT之家热榜
|
|
||||||
itHomeRouter.get("/ithome", async (ctx) => {
|
|
||||||
console.log("获取IT之家热榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取IT之家热榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url, { headers });
|
|
||||||
data = getData(response.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
if (!data) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// IT之家热榜 - 获取最新数据
|
|
||||||
itHomeRouter.get("/ithome/new", async (ctx) => {
|
|
||||||
console.log("获取IT之家热榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url, { headers });
|
|
||||||
const newData = getData(response.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取IT之家热榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
updateTime,
|
|
||||||
total: data.length,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
itHomeRouter.info = routerInfo;
|
|
||||||
module.exports = itHomeRouter;
|
|
||||||
121
routes/juejin.js
121
routes/juejin.js
@@ -1,121 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const juejinRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "稀土掘金",
|
|
||||||
subtitle: "热榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "juejinData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url =
|
|
||||||
"https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.map((v) => {
|
|
||||||
return {
|
|
||||||
id: v.content.content_id,
|
|
||||||
title: v.content.title,
|
|
||||||
hot: v.content_counter.hot_rank,
|
|
||||||
url: `https://juejin.cn/post/${v.content.content_id}`,
|
|
||||||
mobileUrl: `https://juejin.cn/post/${v.content.content_id}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 掘金热榜
|
|
||||||
juejinRouter.get("/juejin", async (ctx) => {
|
|
||||||
console.log("获取掘金热榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取掘金热榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 掘金热榜 - 获取最新数据
|
|
||||||
juejinRouter.get("/juejin/new", async (ctx) => {
|
|
||||||
console.log("获取掘金热榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取掘金热榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
juejinRouter.info = routerInfo;
|
|
||||||
module.exports = juejinRouter;
|
|
||||||
143
routes/lol.js
143
routes/lol.js
@@ -1,143 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const lolRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "英雄联盟",
|
|
||||||
subtitle: "更新公告",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "lolData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url =
|
|
||||||
"https://apps.game.qq.com/cmc/zmMcnTargetContentList?r0=jsonp&page=1&num=16&target=24&source=web_pc&r1=jQuery191002324053053181463_1687855508930&_=1687855508933";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
const dataList = [];
|
|
||||||
try {
|
|
||||||
const pattern = /jQuery191002324053053181463_1687855508930\((.*?)\)/s;
|
|
||||||
const matchResult = data.match(pattern);
|
|
||||||
const jsonObject = JSON.parse(matchResult[1])["data"].result;
|
|
||||||
jsonObject.forEach((v) => {
|
|
||||||
dataList.push({
|
|
||||||
title: v.sTitle,
|
|
||||||
desc: v.sAuthor,
|
|
||||||
pic: `https:${v.sIMG}`,
|
|
||||||
hot: Number(v.iTotalPlay),
|
|
||||||
url: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(
|
|
||||||
v.iDocID
|
|
||||||
)}`,
|
|
||||||
mobileUrl: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(
|
|
||||||
v.iDocID
|
|
||||||
)}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return dataList;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("数据处理出错" + error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 英雄联盟更新公告
|
|
||||||
lolRouter.get("/lol", async (ctx) => {
|
|
||||||
console.log("获取英雄联盟更新公告");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取英雄联盟更新公告");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
if (!data) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 英雄联盟更新公告 - 获取最新数据
|
|
||||||
lolRouter.get("/lol/new", async (ctx) => {
|
|
||||||
console.log("获取英雄联盟更新公告 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取英雄联盟更新公告");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
lolRouter.info = routerInfo;
|
|
||||||
module.exports = lolRouter;
|
|
||||||
123
routes/newsqq.js
123
routes/newsqq.js
@@ -1,123 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const newsqqRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "腾讯新闻",
|
|
||||||
subtitle: "热点榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "newsqqData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://r.inews.qq.com/gw/event/hot_ranking_list?page_size=50";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.slice(1).map((v) => {
|
|
||||||
return {
|
|
||||||
id: v.id,
|
|
||||||
title: v.title,
|
|
||||||
desc: v.abstract,
|
|
||||||
descSm: v.nlpAbstract,
|
|
||||||
hot: v.readCount,
|
|
||||||
pic: v.miniProShareImage,
|
|
||||||
url: `https://new.qq.com/rain/a/${v.id}`,
|
|
||||||
mobileUrl: `https://view.inews.qq.com/a/${v.id}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 腾讯热点榜
|
|
||||||
newsqqRouter.get("/newsqq", async (ctx) => {
|
|
||||||
console.log("获取腾讯热点榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取腾讯热点榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data.idlist[0].newslist);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 腾讯热点榜 - 获取最新数据
|
|
||||||
newsqqRouter.get("/newsqq/new", async (ctx) => {
|
|
||||||
console.log("获取腾讯热点榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data.idlist[0].newslist);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取腾讯热点榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
newsqqRouter.info = routerInfo;
|
|
||||||
module.exports = newsqqRouter;
|
|
||||||
123
routes/sspai.js
123
routes/sspai.js
@@ -1,123 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const sspaiRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "少数派",
|
|
||||||
subtitle: "热榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "sspaiData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = `https://sspai.com/api/v1/article/tag/page/get?limit=40&tag=热门文章`;
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.map((v) => {
|
|
||||||
return {
|
|
||||||
id: v.id,
|
|
||||||
title: v.title,
|
|
||||||
desc: v.summary,
|
|
||||||
pic: `https://cdn.sspai.com/${v.banner}`,
|
|
||||||
owner: v.author,
|
|
||||||
hot: v.like_count,
|
|
||||||
url: `https://sspai.com/post/${v.id}`,
|
|
||||||
mobileUrl: `https://sspai.com/post/${v.itemId}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 少数派热榜
|
|
||||||
sspaiRouter.get("/sspai", async (ctx) => {
|
|
||||||
console.log("获取少数派热榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取少数派热榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 少数派热榜 - 获取最新数据
|
|
||||||
sspaiRouter.get("/sspai/new", async (ctx) => {
|
|
||||||
console.log("获取少数派热榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取少数派热榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sspaiRouter.info = routerInfo;
|
|
||||||
module.exports = sspaiRouter;
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const thepaperRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "澎湃新闻",
|
|
||||||
subtitle: "热榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "thepaperData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://cache.thepaper.cn/contentapi/wwwIndex/rightSidebar";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.map((v) => {
|
|
||||||
return {
|
|
||||||
id: v.contId,
|
|
||||||
title: v.name,
|
|
||||||
pic: v.pic,
|
|
||||||
hot: v.praiseTimes,
|
|
||||||
time: v.pubTime,
|
|
||||||
url: `https://www.thepaper.cn/newsDetail_forward_${v.contId}`,
|
|
||||||
mobileUrl: `https://m.thepaper.cn/newsDetail_forward_${v.contId}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 澎湃热榜
|
|
||||||
thepaperRouter.get("/thepaper", async (ctx) => {
|
|
||||||
console.log("获取澎湃热榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取澎湃热榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data.data.hotNews);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 澎湃热榜 - 获取最新数据
|
|
||||||
thepaperRouter.get("/thepaper/new", async (ctx) => {
|
|
||||||
console.log("获取澎湃热榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data.data.hotNews);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取澎湃热榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
thepaperRouter.info = routerInfo;
|
|
||||||
module.exports = thepaperRouter;
|
|
||||||
122
routes/tieba.js
122
routes/tieba.js
@@ -1,122 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const tiebaRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "百度贴吧",
|
|
||||||
subtitle: "热议榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "tiebaData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://tieba.baidu.com/hottopic/browse/topicList";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.map((v) => {
|
|
||||||
return {
|
|
||||||
id: v.topic_id,
|
|
||||||
title: v.topic_name,
|
|
||||||
desc: v.topic_desc,
|
|
||||||
pic: v.topic_pic,
|
|
||||||
hot: v.discuss_num,
|
|
||||||
url: v.topic_url,
|
|
||||||
mobileUrl: v.topic_url,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 贴吧热议榜
|
|
||||||
tiebaRouter.get("/tieba", async (ctx) => {
|
|
||||||
console.log("获取贴吧热议榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取贴吧热议榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data.data.bang_topic.topic_list);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 贴吧热议榜 - 获取最新数据
|
|
||||||
tiebaRouter.get("/tieba/new", async (ctx) => {
|
|
||||||
console.log("获取贴吧热议榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data.data.bang_topic.topic_list);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取贴吧热议榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tiebaRouter.info = routerInfo;
|
|
||||||
module.exports = tiebaRouter;
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const toutiaoRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "今日头条",
|
|
||||||
subtitle: "热榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "toutiaoData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.map((v) => {
|
|
||||||
return {
|
|
||||||
id: v.ClusterId,
|
|
||||||
title: v.Title,
|
|
||||||
pic: v.Image.url,
|
|
||||||
hot: v.HotValue,
|
|
||||||
url: `https://www.toutiao.com/trending/${v.ClusterIdStr}/`,
|
|
||||||
mobileUrl: `https://api.toutiaoapi.com/feoffline/amos_land/new/html/main/index.html?topic_id=${v.ClusterIdStr}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 头条热榜
|
|
||||||
toutiaoRouter.get("/toutiao", async (ctx) => {
|
|
||||||
console.log("获取头条热榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取头条热榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 头条热榜 - 获取最新数据
|
|
||||||
toutiaoRouter.get("/toutiao/new", async (ctx) => {
|
|
||||||
console.log("获取头条热榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取头条热榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
toutiaoRouter.info = routerInfo;
|
|
||||||
module.exports = toutiaoRouter;
|
|
||||||
135
routes/weibo.js
135
routes/weibo.js
@@ -1,135 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const weiboRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
// const cheerio = require("cheerio");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "微博",
|
|
||||||
subtitle: "热搜榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "weiboData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://weibo.com/ajax/side/hotSearch";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
// return data;
|
|
||||||
return data.map((v) => {
|
|
||||||
const key = v.word_scheme ? v.word_scheme : `#${v.word}`;
|
|
||||||
return {
|
|
||||||
title: v.word,
|
|
||||||
desc: key,
|
|
||||||
hot: v.raw_hot,
|
|
||||||
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(
|
|
||||||
key
|
|
||||||
)}&t=31&band_rank=1&Refer=top`,
|
|
||||||
mobileUrl: `https://s.weibo.com/weibo?q=${encodeURIComponent(
|
|
||||||
key
|
|
||||||
)}&t=31&band_rank=1&Refer=top`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 微博热搜
|
|
||||||
weiboRouter.get("/weibo", async (ctx) => {
|
|
||||||
console.log("获取微博热搜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取微博热搜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
data = getData(response.data.data.realtime);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
if (!data) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 微博热搜 - 获取最新数据
|
|
||||||
weiboRouter.get("/weibo/new", async (ctx) => {
|
|
||||||
console.log("获取微博热搜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data.data.realtime);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取微博热搜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
weiboRouter.info = routerInfo;
|
|
||||||
module.exports = weiboRouter;
|
|
||||||
137
routes/weread.js
137
routes/weread.js
@@ -1,137 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const wereadRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "微信读书",
|
|
||||||
subtitle: "飙升榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "wereadData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://weread.qq.com/web/bookListInCategory/rising?rank=1";
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.map((v) => {
|
|
||||||
const book = v.bookInfo;
|
|
||||||
console.log(book);
|
|
||||||
return {
|
|
||||||
id: book.bookId,
|
|
||||||
title: book.title,
|
|
||||||
desc: book.intro,
|
|
||||||
pic: book.cover.replace("s_", "t9_"),
|
|
||||||
hot: v.readingCount,
|
|
||||||
author: book.author,
|
|
||||||
url: "https://weread.qq.com/web/category/rising",
|
|
||||||
mobileUrl: "https://weread.qq.com/web/category/rising",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 微信读书
|
|
||||||
wereadRouter.get("/weread", async (ctx) => {
|
|
||||||
console.log("获取微信读书");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取微信读书");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url, {
|
|
||||||
Headers: {
|
|
||||||
"User-Agent":
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
data = getData(response.data.books);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
if (!data) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 微信读书 - 获取最新数据
|
|
||||||
wereadRouter.get("/weread/new", async (ctx) => {
|
|
||||||
console.log("获取微信读书 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const newData = getData(response.data.books);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取微信读书");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
wereadRouter.info = routerInfo;
|
|
||||||
module.exports = wereadRouter;
|
|
||||||
145
routes/zhihu.js
145
routes/zhihu.js
@@ -1,145 +0,0 @@
|
|||||||
const Router = require("koa-router");
|
|
||||||
const zhihuRouter = new Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const { get, set, del } = require("../utils/cacheData");
|
|
||||||
const router = require(".");
|
|
||||||
|
|
||||||
// 接口信息
|
|
||||||
const routerInfo = {
|
|
||||||
title: "知乎",
|
|
||||||
subtitle: "热榜",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存键名
|
|
||||||
const cacheKey = "zhihuData";
|
|
||||||
|
|
||||||
// 调用时间
|
|
||||||
let updateTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// 调用路径
|
|
||||||
const url = "https://www.zhihu.com/hot";
|
|
||||||
const headers = {
|
|
||||||
"User-Agent":
|
|
||||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 数据处理
|
|
||||||
const getData = (data) => {
|
|
||||||
if (!data) return [];
|
|
||||||
const dataList = [];
|
|
||||||
try {
|
|
||||||
const pattern =
|
|
||||||
/<script id="js-initialData" type="text\/json">(.*?)<\/script>/;
|
|
||||||
const matchResult = data.match(pattern);
|
|
||||||
const jsonObject = JSON.parse(matchResult[1]).initialState.topstory.hotList;
|
|
||||||
jsonObject.forEach((v) => {
|
|
||||||
dataList.push({
|
|
||||||
title: v.target.titleArea.text,
|
|
||||||
desc: v.target.excerptArea.text,
|
|
||||||
pic: v.target.imageArea.url,
|
|
||||||
hot: parseInt(v.target.metricsArea.text.replace(/[^\d]/g, "")) * 10000,
|
|
||||||
url: v.target.link.url,
|
|
||||||
mobileUrl: v.target.link.url,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return dataList;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("数据处理出错" + error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 知乎热榜
|
|
||||||
zhihuRouter.get("/zhihu", async (ctx) => {
|
|
||||||
console.log("获取知乎热榜");
|
|
||||||
try {
|
|
||||||
// 从缓存中获取数据
|
|
||||||
let data = await get(cacheKey);
|
|
||||||
const from = data ? "cache" : "server";
|
|
||||||
if (!data) {
|
|
||||||
// 如果缓存中不存在数据
|
|
||||||
console.log("从服务端重新获取知乎热榜");
|
|
||||||
// 从服务器拉取数据
|
|
||||||
const response = await axios.get(url, { headers });
|
|
||||||
data = getData(response.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
if (!data) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 将数据写入缓存
|
|
||||||
await set(cacheKey, data);
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
from,
|
|
||||||
total: data.length,
|
|
||||||
updateTime,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 知乎热榜 - 获取最新数据
|
|
||||||
zhihuRouter.get("/zhihu/new", async (ctx) => {
|
|
||||||
console.log("获取知乎热榜 - 最新数据");
|
|
||||||
try {
|
|
||||||
// 从服务器拉取最新数据
|
|
||||||
const response = await axios.get(url, { headers });
|
|
||||||
const newData = getData(response.data);
|
|
||||||
updateTime = new Date().toISOString();
|
|
||||||
console.log("从服务端重新获取知乎热榜");
|
|
||||||
|
|
||||||
// 返回最新数据
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: newData.length,
|
|
||||||
updateTime,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除旧数据
|
|
||||||
await del(cacheKey);
|
|
||||||
// 将最新数据写入缓存
|
|
||||||
await set(cacheKey, newData);
|
|
||||||
} catch (error) {
|
|
||||||
// 如果拉取最新数据失败,尝试从缓存中获取数据
|
|
||||||
console.error(error);
|
|
||||||
const cachedData = await get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
ctx.body = {
|
|
||||||
code: 200,
|
|
||||||
message: "获取成功",
|
|
||||||
...routerInfo,
|
|
||||||
total: cachedData.length,
|
|
||||||
updateTime,
|
|
||||||
data: cachedData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 如果缓存中也没有数据,则返回错误信息
|
|
||||||
ctx.body = {
|
|
||||||
code: 500,
|
|
||||||
...routerInfo,
|
|
||||||
message: "获取失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
zhihuRouter.info = routerInfo;
|
|
||||||
module.exports = zhihuRouter;
|
|
||||||
62
src/app.tsx
Normal file
62
src/app.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { cors } from "hono/cors";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
|
import { compress } from "hono/compress";
|
||||||
|
import { prettyJSON } from "hono/pretty-json";
|
||||||
|
import { trimTrailingSlash } from "hono/trailing-slash";
|
||||||
|
import logger from "./utils/logger.js";
|
||||||
|
import registry from "./registry.js";
|
||||||
|
import robotstxt from "./robots.txt.js";
|
||||||
|
import NotFound from "./views/NotFound.js";
|
||||||
|
import Home from "./views/Home.js";
|
||||||
|
import Error from "./views/Error.js";
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
// 压缩响应
|
||||||
|
app.use(compress());
|
||||||
|
|
||||||
|
// prettyJSON
|
||||||
|
app.use(prettyJSON());
|
||||||
|
|
||||||
|
// 尾部斜杠重定向
|
||||||
|
app.use(trimTrailingSlash());
|
||||||
|
|
||||||
|
// CORS
|
||||||
|
app.use(
|
||||||
|
"*",
|
||||||
|
cors({
|
||||||
|
// 可写为数组
|
||||||
|
origin: config.ALLOWED_DOMAIN,
|
||||||
|
allowMethods: ["POST", "GET", "OPTIONS"],
|
||||||
|
allowHeaders: ["X-Custom-Header", "Upgrade-Insecure-Requests"],
|
||||||
|
credentials: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 静态资源
|
||||||
|
app.use(
|
||||||
|
"/*",
|
||||||
|
serveStatic({
|
||||||
|
root: "./public",
|
||||||
|
rewriteRequestPath: (path) => (path === "/favicon.ico" ? "/favicon.png" : path),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 主路由
|
||||||
|
app.route("/", registry);
|
||||||
|
|
||||||
|
// robots
|
||||||
|
app.get("/robots.txt", robotstxt);
|
||||||
|
// 首页
|
||||||
|
app.get("/", (c) => c.html(<Home />));
|
||||||
|
// 404
|
||||||
|
app.notFound((c) => c.html(<NotFound />, 404));
|
||||||
|
// error
|
||||||
|
app.onError((err, c) => {
|
||||||
|
logger.error(`出现致命错误:${err}`);
|
||||||
|
return c.html(<Error error={err?.message} />, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
50
src/config.ts
Normal file
50
src/config.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
// 环境变量
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
PORT: number;
|
||||||
|
DISALLOW_ROBOT: boolean;
|
||||||
|
CACHE_TTL: number;
|
||||||
|
REQUEST_TIMEOUT: number;
|
||||||
|
ALLOWED_DOMAIN: string;
|
||||||
|
USE_LOG_FILE: boolean;
|
||||||
|
RSS_MODE: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证并提取环境变量
|
||||||
|
const getEnvVariable = (key: string): string | undefined => {
|
||||||
|
const value = process.env[key];
|
||||||
|
if (value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将环境变量转换为数值
|
||||||
|
const getNumericEnvVariable = (key: string, defaultValue: number): number => {
|
||||||
|
const value = getEnvVariable(key) ?? String(defaultValue);
|
||||||
|
const parsedValue = parseInt(value, 10);
|
||||||
|
if (isNaN(parsedValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return parsedValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将环境变量转换为布尔值
|
||||||
|
const getBooleanEnvVariable = (key: string, defaultValue: boolean): boolean => {
|
||||||
|
const value = getEnvVariable(key) ?? String(defaultValue);
|
||||||
|
return value.toLowerCase() === "true";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建配置对象
|
||||||
|
export const config: Config = {
|
||||||
|
PORT: getNumericEnvVariable("PORT", 6688),
|
||||||
|
DISALLOW_ROBOT: getBooleanEnvVariable("DISALLOW_ROBOT", true),
|
||||||
|
CACHE_TTL: getNumericEnvVariable("CACHE_TTL", 3600),
|
||||||
|
REQUEST_TIMEOUT: getNumericEnvVariable("CACHE_TTL", 6000),
|
||||||
|
ALLOWED_DOMAIN: getEnvVariable("ALLOWED_DOMAIN") || "*",
|
||||||
|
USE_LOG_FILE: getBooleanEnvVariable("USE_LOG_FILE", true),
|
||||||
|
RSS_MODE: getBooleanEnvVariable("RSS_MODE", false),
|
||||||
|
};
|
||||||
25
src/index.ts
Normal file
25
src/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { serve } from "@hono/node-server";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
import logger from "./utils/logger.js";
|
||||||
|
import app from "./app.js";
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
const serveHotApi: (port?: number) => void = (port: number = config.PORT) => {
|
||||||
|
try {
|
||||||
|
const apiServer = serve({
|
||||||
|
fetch: app.fetch,
|
||||||
|
port,
|
||||||
|
});
|
||||||
|
logger.info(`🔥 DailyHot API 成功在端口 ${port} 上运行`);
|
||||||
|
logger.info(`🔗 Local: 👉 http://localhost:${port}`);
|
||||||
|
return apiServer;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "docker") {
|
||||||
|
serveHotApi(config.PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default serveHotApi;
|
||||||
136
src/registry.ts
Normal file
136
src/registry.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import getRSS from "./utils/getRSS.js";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
// 模拟 __dirname
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// 路由数据
|
||||||
|
let allRoutePath: Array<string> = [];
|
||||||
|
const routersDirName: string = "routes";
|
||||||
|
|
||||||
|
// 排除路由
|
||||||
|
const excludeRoutes: Array<string> = ["52pojie", "hostloc"];
|
||||||
|
|
||||||
|
// 建立完整目录路径
|
||||||
|
const routersDirPath = path.join(__dirname, routersDirName);
|
||||||
|
|
||||||
|
// 递归查找函数
|
||||||
|
const findTsFiles = (dirPath: string, allFiles: string[] = [], basePath: string = ""): string[] => {
|
||||||
|
// 读取目录下的所有文件和文件夹
|
||||||
|
const items: Array<string> = fs.readdirSync(dirPath);
|
||||||
|
// 遍历每个文件或文件夹
|
||||||
|
items.forEach((item) => {
|
||||||
|
const fullPath: string = path.join(dirPath, item);
|
||||||
|
const relativePath: string = basePath ? path.posix.join(basePath, item) : item;
|
||||||
|
const stat: fs.Stats = fs.statSync(fullPath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
// 如果是文件夹,递归查找
|
||||||
|
findTsFiles(fullPath, allFiles, relativePath);
|
||||||
|
} else if (
|
||||||
|
stat.isFile() &&
|
||||||
|
(item.endsWith(".ts") || item.endsWith(".js")) &&
|
||||||
|
!item.endsWith(".d.ts")
|
||||||
|
) {
|
||||||
|
// 符合条件
|
||||||
|
allFiles.push(relativePath.replace(/\.(ts|js)$/, ""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return allFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取全部路由
|
||||||
|
if (fs.existsSync(routersDirPath) && fs.statSync(routersDirPath).isDirectory()) {
|
||||||
|
allRoutePath = findTsFiles(routersDirPath);
|
||||||
|
} else {
|
||||||
|
console.error(`目录 ${routersDirPath} 不存在或不是目录`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册全部路由
|
||||||
|
for (let index = 0; index < allRoutePath.length; index++) {
|
||||||
|
const router = allRoutePath[index];
|
||||||
|
// 是否处于排除名单
|
||||||
|
if (excludeRoutes.includes(router)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const listApp = app.basePath(`/${router}`);
|
||||||
|
// 返回榜单
|
||||||
|
listApp.get("/", async (c) => {
|
||||||
|
// 是否采用缓存
|
||||||
|
const noCache = c.req.query("cache") === "false";
|
||||||
|
// 限制显示条目
|
||||||
|
const limit = c.req.query("limit");
|
||||||
|
// 是否输出 RSS
|
||||||
|
const rssEnabled = c.req.query("rss") === "true";
|
||||||
|
// 获取路由路径
|
||||||
|
const { handleRoute } = await import(`./routes/${router}.js`);
|
||||||
|
const listData = await handleRoute(c, noCache);
|
||||||
|
// 是否限制条目
|
||||||
|
if (limit && listData?.data?.length > parseInt(limit)) {
|
||||||
|
listData.total = parseInt(limit);
|
||||||
|
listData.data = listData.data.slice(0, parseInt(limit));
|
||||||
|
}
|
||||||
|
// 是否输出 RSS
|
||||||
|
if (rssEnabled || config.RSS_MODE) {
|
||||||
|
const rss = getRSS(listData);
|
||||||
|
if (typeof rss === "string") {
|
||||||
|
c.header("Content-Type", "application/xml; charset=utf-8");
|
||||||
|
return c.body(rss);
|
||||||
|
} else {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
code: 500,
|
||||||
|
message: "RSS 生成失败",
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.json({
|
||||||
|
code: 200,
|
||||||
|
...listData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 请求方式错误
|
||||||
|
listApp.all("*", (c) =>
|
||||||
|
c.json(
|
||||||
|
{
|
||||||
|
code: 405,
|
||||||
|
message: "Method Not Allowed",
|
||||||
|
},
|
||||||
|
405,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取全部路由
|
||||||
|
app.get("/all", (c) =>
|
||||||
|
c.json(
|
||||||
|
{
|
||||||
|
code: 200,
|
||||||
|
count: allRoutePath.length,
|
||||||
|
routes: allRoutePath.map((path) => {
|
||||||
|
// 是否处于排除名单
|
||||||
|
if (excludeRoutes.includes(path)) {
|
||||||
|
return {
|
||||||
|
name: path,
|
||||||
|
path: null,
|
||||||
|
message: "该接口暂时下线",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: path,
|
||||||
|
path: `/${path}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default app;
|
||||||
13
src/robots.txt.ts
Normal file
13
src/robots.txt.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Handler } from "hono";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
|
||||||
|
const handler: Handler = (c) => {
|
||||||
|
if (config.DISALLOW_ROBOT) {
|
||||||
|
return c.text("User-agent: *\nDisallow: /");
|
||||||
|
} else {
|
||||||
|
c.status(404);
|
||||||
|
return c.text("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
||||||
272
src/router.types.d.ts
vendored
Normal file
272
src/router.types.d.ts
vendored
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
export type RouterType = {
|
||||||
|
"36kr": {
|
||||||
|
itemId: number;
|
||||||
|
publishTime: number;
|
||||||
|
templateMaterial: {
|
||||||
|
widgetTitle: string;
|
||||||
|
authorName: string;
|
||||||
|
statCollect: number;
|
||||||
|
widgetImage: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"qq-news": {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
abstract: string;
|
||||||
|
source: string;
|
||||||
|
hotEvent: {
|
||||||
|
hotScore: number;
|
||||||
|
};
|
||||||
|
timestamp: number;
|
||||||
|
miniProShareImage: string;
|
||||||
|
};
|
||||||
|
"netease-news": {
|
||||||
|
title: string;
|
||||||
|
imgsrc: string;
|
||||||
|
source: string;
|
||||||
|
docid: string;
|
||||||
|
ptime: string;
|
||||||
|
};
|
||||||
|
"zhihu-daily": {
|
||||||
|
id: number;
|
||||||
|
images: [string];
|
||||||
|
title: string;
|
||||||
|
hint: string;
|
||||||
|
url: string;
|
||||||
|
type: number;
|
||||||
|
};
|
||||||
|
"51cto": {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
cover: string;
|
||||||
|
abstract: string;
|
||||||
|
source_id: number;
|
||||||
|
pubdate: string;
|
||||||
|
};
|
||||||
|
discuz: {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
guid: string;
|
||||||
|
content?: string;
|
||||||
|
pubDate?: string;
|
||||||
|
author?: string;
|
||||||
|
};
|
||||||
|
bilibili: {
|
||||||
|
bvid: string;
|
||||||
|
title: string;
|
||||||
|
desc?: string;
|
||||||
|
pubdate: string;
|
||||||
|
pic?: string;
|
||||||
|
author?: string;
|
||||||
|
video_review?: number;
|
||||||
|
owner?: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
stat?: {
|
||||||
|
view: number;
|
||||||
|
};
|
||||||
|
short_link_v2?: string;
|
||||||
|
};
|
||||||
|
juejin: {
|
||||||
|
content: {
|
||||||
|
content_id: string;
|
||||||
|
title: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
author: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
content_counter: {
|
||||||
|
hot_rank: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
weibo: {
|
||||||
|
mid: string;
|
||||||
|
word: string;
|
||||||
|
word_scheme: string;
|
||||||
|
note: string;
|
||||||
|
category: string;
|
||||||
|
raw_hot: number;
|
||||||
|
onboard_time: number;
|
||||||
|
};
|
||||||
|
zhihu: {
|
||||||
|
target: {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
excerpt: string;
|
||||||
|
created: number;
|
||||||
|
};
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
thumbnail: string;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
detail_text: string;
|
||||||
|
};
|
||||||
|
douyin: {
|
||||||
|
sentence_id: string;
|
||||||
|
word: string;
|
||||||
|
hot_value: number;
|
||||||
|
event_time: number;
|
||||||
|
};
|
||||||
|
baidu: {
|
||||||
|
index: number;
|
||||||
|
word: string;
|
||||||
|
desc: string;
|
||||||
|
img: string;
|
||||||
|
hotScore: string;
|
||||||
|
show: string;
|
||||||
|
rawUrl: string;
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
miyoushe: {
|
||||||
|
post: {
|
||||||
|
post_id: string;
|
||||||
|
subject: string;
|
||||||
|
content: string;
|
||||||
|
cover: string;
|
||||||
|
created_at: number;
|
||||||
|
};
|
||||||
|
stat: {
|
||||||
|
view_num: number;
|
||||||
|
};
|
||||||
|
user: {
|
||||||
|
nickname: string;
|
||||||
|
};
|
||||||
|
image_list: [
|
||||||
|
{
|
||||||
|
url: string;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
weread: {
|
||||||
|
readingCount: number;
|
||||||
|
bookInfo: {
|
||||||
|
bookId: string;
|
||||||
|
title: string;
|
||||||
|
intro: string;
|
||||||
|
cover: string;
|
||||||
|
author: string;
|
||||||
|
publishTime: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
toutiao: {
|
||||||
|
ClusterIdStr: string;
|
||||||
|
Title: string;
|
||||||
|
HotValue: string;
|
||||||
|
Image: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
thepaper: {
|
||||||
|
contId: string;
|
||||||
|
name: string;
|
||||||
|
pic: string;
|
||||||
|
praiseTimes: string;
|
||||||
|
pubTimeLong: number;
|
||||||
|
};
|
||||||
|
sspai: {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
banner: string;
|
||||||
|
like_count: number;
|
||||||
|
released_time: number;
|
||||||
|
author: {
|
||||||
|
nickname: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
lol: {
|
||||||
|
sAuthor: string;
|
||||||
|
sIMG: string;
|
||||||
|
sTitle: string;
|
||||||
|
iTotalPlay: string;
|
||||||
|
iDocID: string;
|
||||||
|
sCreated: string;
|
||||||
|
};
|
||||||
|
ngabbs: {
|
||||||
|
tid: number;
|
||||||
|
subject: string;
|
||||||
|
author: string;
|
||||||
|
tpcurl: string;
|
||||||
|
replies: number;
|
||||||
|
postdate: number;
|
||||||
|
};
|
||||||
|
tieba: {
|
||||||
|
topic_id: number;
|
||||||
|
topic_name: string;
|
||||||
|
topic_desc: string;
|
||||||
|
topic_pic: string;
|
||||||
|
topic_url: string;
|
||||||
|
discuss_num: number;
|
||||||
|
create_time: number;
|
||||||
|
};
|
||||||
|
acfun: {
|
||||||
|
dougaId: string;
|
||||||
|
contentTitle: string;
|
||||||
|
userName: string;
|
||||||
|
contentDesc: string;
|
||||||
|
likeCount: number;
|
||||||
|
coverUrl: string;
|
||||||
|
contributeTime: number;
|
||||||
|
};
|
||||||
|
hellogithub: {
|
||||||
|
item_id: string;
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
description: string;
|
||||||
|
summary: string;
|
||||||
|
clicks_total: number;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
v2ex: {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
content: string;
|
||||||
|
id: number;
|
||||||
|
replies: number;
|
||||||
|
member: {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
earthquake: {
|
||||||
|
NEW_DID: string;
|
||||||
|
LOCATION_C: string;
|
||||||
|
M: string;
|
||||||
|
};
|
||||||
|
weatheralarm: {
|
||||||
|
alertid: string;
|
||||||
|
issuetime: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
pic: string;
|
||||||
|
};
|
||||||
|
huxiu: {
|
||||||
|
object_id: number;
|
||||||
|
content: string;
|
||||||
|
url: string;
|
||||||
|
user_info: {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
publish_time: string;
|
||||||
|
};
|
||||||
|
ifanr: {
|
||||||
|
buzz_original_url: string;
|
||||||
|
id: number;
|
||||||
|
post_content: string;
|
||||||
|
post_id: number;
|
||||||
|
post_title: string;
|
||||||
|
like_count: number;
|
||||||
|
comment_count: number;
|
||||||
|
created_at: number;
|
||||||
|
};
|
||||||
|
csdn: {
|
||||||
|
nickName: string;
|
||||||
|
articleTitle: string;
|
||||||
|
articleDetailUrl: string;
|
||||||
|
picList: [string];
|
||||||
|
hotRankScore: string;
|
||||||
|
period: string;
|
||||||
|
productId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
76
src/routes/36kr.ts
Normal file
76
src/routes/36kr.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { post } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "hot";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "36kr",
|
||||||
|
title: "36氪",
|
||||||
|
type: "热榜",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "热榜分类",
|
||||||
|
type: {
|
||||||
|
hot: "人气榜",
|
||||||
|
video: "视频榜",
|
||||||
|
comment: "热议榜",
|
||||||
|
collect: "收藏榜",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://m.36kr.com/hot-list-m",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const url = `https://gateway.36kr.com/api/mis/nav/home/nav/rank/${type}`;
|
||||||
|
const result = await post({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
partner_id: "wap",
|
||||||
|
param: {
|
||||||
|
siteId: 1,
|
||||||
|
platformId: 2,
|
||||||
|
},
|
||||||
|
timestamp: new Date().getTime(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const listType = {
|
||||||
|
hot: "hotRankList",
|
||||||
|
video: "videoList",
|
||||||
|
comment: "remarkList",
|
||||||
|
collect: "collectList",
|
||||||
|
};
|
||||||
|
const list =
|
||||||
|
result.data.data[(listType as Record<string, keyof typeof result.data.data>)[type || "hot"]];
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["36kr"]) => {
|
||||||
|
const item = v.templateMaterial;
|
||||||
|
return {
|
||||||
|
id: v.itemId,
|
||||||
|
title: item.widgetTitle,
|
||||||
|
cover: item.widgetImage,
|
||||||
|
author: item.authorName,
|
||||||
|
timestamp: getTime(v.publishTime),
|
||||||
|
hot: item.statCollect,
|
||||||
|
url: `https://www.36kr.com/p/${v.itemId}`,
|
||||||
|
mobileUrl: `https://m.36kr.com/p/${v.itemId}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
56
src/routes/51cto.ts
Normal file
56
src/routes/51cto.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { getToken, sign } from "../utils/getToken/51cto.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "51cto",
|
||||||
|
title: "51CTO",
|
||||||
|
type: "推荐榜",
|
||||||
|
link: "https://www.51cto.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://api-media.51cto.com/index/index/recommend`;
|
||||||
|
const params = {
|
||||||
|
page: 1,
|
||||||
|
page_size: 50,
|
||||||
|
limit_time: 0,
|
||||||
|
name_en: "",
|
||||||
|
};
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const token = (await getToken()) as string;
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
timestamp,
|
||||||
|
token,
|
||||||
|
sign: sign("index/index/recommend", params, timestamp, token),
|
||||||
|
},
|
||||||
|
noCache,
|
||||||
|
});
|
||||||
|
const list = result.data.data.data.list;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["51cto"]) => ({
|
||||||
|
id: v.source_id,
|
||||||
|
title: v.title,
|
||||||
|
cover: v.cover,
|
||||||
|
desc: v.abstract,
|
||||||
|
timestamp: getTime(v.pubdate),
|
||||||
|
url: v.url,
|
||||||
|
mobileUrl: v.url,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
66
src/routes/52pojie.ts
Normal file
66
src/routes/52pojie.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { web } from "../utils/getData.js";
|
||||||
|
import { extractRss, parseRSS } from "../utils/parseRSS.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "hot";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "52pojie",
|
||||||
|
title: "吾爱破解",
|
||||||
|
type: "榜单",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "榜单分类",
|
||||||
|
type: {
|
||||||
|
tech: "新鲜出炉",
|
||||||
|
newthread: "技术分享",
|
||||||
|
hot: "人气热门",
|
||||||
|
digest: "精华采撷",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://www.52pojie.cn/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const url = `https://www.52pojie.cn/forum.php?mod=guide&view=${type}&rss=1`;
|
||||||
|
const result = await web({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36",
|
||||||
|
});
|
||||||
|
const parseData = async () => {
|
||||||
|
if (typeof result?.data === "string") {
|
||||||
|
const rssContent = extractRss(result.data);
|
||||||
|
return await parseRSS(rssContent);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const list = await parseData();
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["discuz"]) => ({
|
||||||
|
id: v.guid,
|
||||||
|
title: v.title,
|
||||||
|
desc: v.content,
|
||||||
|
author: v.author,
|
||||||
|
timestamp: getTime(v.pubDate),
|
||||||
|
hot: null,
|
||||||
|
url: v.link,
|
||||||
|
mobileUrl: v.link,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
77
src/routes/acfun.ts
Normal file
77
src/routes/acfun.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "-1";
|
||||||
|
const range = c.req.query("range") || "DAY";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type, range }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "acfun",
|
||||||
|
title: "AcFun",
|
||||||
|
type: "排行榜",
|
||||||
|
description: "AcFun是一家弹幕视频网站,致力于为每一个人带来欢乐。",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "频道",
|
||||||
|
type: {
|
||||||
|
"-1": "全站综合",
|
||||||
|
"155": "番剧",
|
||||||
|
"1": "动画",
|
||||||
|
"60": "娱乐",
|
||||||
|
"201": "生活",
|
||||||
|
"58": "音乐",
|
||||||
|
"123": "舞蹈·偶像",
|
||||||
|
"59": "游戏",
|
||||||
|
"70": "科技",
|
||||||
|
"68": "影视",
|
||||||
|
"69": "体育",
|
||||||
|
"125": "鱼塘",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
range: {
|
||||||
|
name: "时间",
|
||||||
|
type: {
|
||||||
|
DAY: "今日",
|
||||||
|
THREE_DAYS: "三日",
|
||||||
|
WEEK: "本周",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://www.acfun.cn/rank/list/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type, range } = options;
|
||||||
|
const url = `https://www.acfun.cn/rest/pc-direct/rank/channel?channelId=${type === "-1" ? "" : type}&rankLimit=30&rankPeriod=${range}`;
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
Referer: `https://www.acfun.cn/rank/list/?cid=-1&pcid=${type}&range=${range}`,
|
||||||
|
},
|
||||||
|
noCache,
|
||||||
|
});
|
||||||
|
const list = result.data.rankList;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["acfun"]) => ({
|
||||||
|
id: v.dougaId,
|
||||||
|
title: v.contentTitle,
|
||||||
|
desc: v.contentDesc,
|
||||||
|
cover: v.coverUrl,
|
||||||
|
author: v.userName,
|
||||||
|
timestamp: getTime(v.contributeTime),
|
||||||
|
hot: v.likeCount,
|
||||||
|
url: `https://www.acfun.cn/v/ac${v.dougaId}`,
|
||||||
|
mobileUrl: `https://m.acfun.cn/v/?ac=${v.dougaId}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
64
src/routes/baidu.ts
Normal file
64
src/routes/baidu.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "realtime";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "baidu",
|
||||||
|
title: "百度",
|
||||||
|
type: "热搜榜",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "热搜类别",
|
||||||
|
type: {
|
||||||
|
realtime: "热搜",
|
||||||
|
novel: "小说",
|
||||||
|
movie: "电影",
|
||||||
|
teleplay: "电视剧",
|
||||||
|
car: "汽车",
|
||||||
|
game: "游戏",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://top.baidu.com/board",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const url = `https://top.baidu.com/board?tab=${type}`;
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/1.0 Mobile/12F69 Safari/605.1.15",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// 正则查找
|
||||||
|
const pattern = /<!--s-data:(.*?)-->/s;
|
||||||
|
const matchResult = result.data.match(pattern);
|
||||||
|
const jsonObject = JSON.parse(matchResult[1]).cards[0].content;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: jsonObject.map((v: RouterType["baidu"]) => ({
|
||||||
|
id: v.index,
|
||||||
|
title: v.word,
|
||||||
|
desc: v.desc,
|
||||||
|
cover: v.img,
|
||||||
|
author: v.show?.length ? v.show : "",
|
||||||
|
timestamp: null,
|
||||||
|
hot: Number(v.hotScore),
|
||||||
|
url: `https://www.baidu.com/s?wd=${encodeURIComponent(v.query)}`,
|
||||||
|
mobileUrl: v.rawUrl,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
105
src/routes/bilibili.ts
Normal file
105
src/routes/bilibili.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import getBiliWbi from "../utils/getToken/bilibili.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "0";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "bilibili",
|
||||||
|
title: "哔哩哔哩",
|
||||||
|
type: "热门榜",
|
||||||
|
description: "你所热爱的,就是你的生活",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "排行榜分区",
|
||||||
|
type: {
|
||||||
|
0: "全站",
|
||||||
|
1: "动画",
|
||||||
|
3: "音乐",
|
||||||
|
4: "游戏",
|
||||||
|
5: "娱乐",
|
||||||
|
36: "科技",
|
||||||
|
119: "鬼畜",
|
||||||
|
129: "舞蹈",
|
||||||
|
155: "时尚",
|
||||||
|
160: "生活",
|
||||||
|
168: "国创相关",
|
||||||
|
188: "数码",
|
||||||
|
181: "影视",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://www.bilibili.com/v/popular/rank/all",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const wbiData = await getBiliWbi();
|
||||||
|
const url = `https://api.bilibili.com/x/web-interface/ranking/v2?tid=${type}&type=all&${wbiData}`;
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
Referer: `https://www.bilibili.com/ranking/all`,
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
||||||
|
},
|
||||||
|
noCache,
|
||||||
|
});
|
||||||
|
// 是否触发风控
|
||||||
|
if (result.data?.data?.list?.length > 0) {
|
||||||
|
const list = result.data.data.list;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["bilibili"]) => ({
|
||||||
|
id: v.bvid,
|
||||||
|
title: v.title,
|
||||||
|
desc: v.desc || "该视频暂无简介",
|
||||||
|
cover: v.pic.replace(/http:/, "https:"),
|
||||||
|
author: v.owner.name,
|
||||||
|
timestamp: getTime(v.pubdate),
|
||||||
|
hot: v.stat.view,
|
||||||
|
url: v.short_link_v2 || `https://www.bilibili.com/video/${v.bvid}`,
|
||||||
|
mobileUrl: `https://m.bilibili.com/video/${v.bvid}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 采用备用接口
|
||||||
|
else {
|
||||||
|
const url = `https://api.bilibili.com/x/web-interface/ranking?jsonp=jsonp?rid=${type}&type=1&callback=__jp0`;
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
Referer: `https://www.bilibili.com/ranking/all`,
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
||||||
|
},
|
||||||
|
noCache,
|
||||||
|
});
|
||||||
|
const list = result.data.data.list;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["bilibili"]) => ({
|
||||||
|
id: v.bvid,
|
||||||
|
title: v.title,
|
||||||
|
desc: v.desc || "该视频暂无简介",
|
||||||
|
cover: v.pic.replace(/http:/, "https:"),
|
||||||
|
author: v.author,
|
||||||
|
timestamp: null,
|
||||||
|
hot: v.video_review,
|
||||||
|
url: `https://www.bilibili.com/video/${v.bvid}`,
|
||||||
|
mobileUrl: `https://m.bilibili.com/video/${v.bvid}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
41
src/routes/csdn.ts
Normal file
41
src/routes/csdn.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "csdn",
|
||||||
|
title: "CSDN",
|
||||||
|
type: "排行榜",
|
||||||
|
description: "专业开发者社区",
|
||||||
|
link: "https://www.csdn.net/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = "https://blog.csdn.net/phoenix/web/blog/hot-rank?page=0&pageSize=30";
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["csdn"]) => ({
|
||||||
|
id: v.productId,
|
||||||
|
title: v.articleTitle,
|
||||||
|
cover: v.picList?.[0] || null,
|
||||||
|
desc: null,
|
||||||
|
author: v.nickName,
|
||||||
|
timestamp: getTime(v.period),
|
||||||
|
hot: Number(v.hotRankScore),
|
||||||
|
url: v.articleDetailUrl,
|
||||||
|
mobileUrl: v.articleDetailUrl,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
56
src/routes/douban-group.ts
Normal file
56
src/routes/douban-group.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import { load } from "cheerio";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "douban-group",
|
||||||
|
title: "豆瓣讨论",
|
||||||
|
type: "讨论精选",
|
||||||
|
link: "https://www.douban.com/group/explore",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 数据处理
|
||||||
|
const getNumbers = (text: string | undefined) => {
|
||||||
|
if (!text) return 100000000;
|
||||||
|
const regex = /\d+/;
|
||||||
|
const match = text.match(regex);
|
||||||
|
if (match) {
|
||||||
|
return Number(match[0]);
|
||||||
|
} else {
|
||||||
|
return 100000000;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://www.douban.com/group/explore`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const $ = load(result.data);
|
||||||
|
const listDom = $(".article .channel-item");
|
||||||
|
const listData = listDom.toArray().map((item) => {
|
||||||
|
const dom = $(item);
|
||||||
|
const url = dom.find("h3 a").attr("href") || undefined;
|
||||||
|
return {
|
||||||
|
id: getNumbers(url),
|
||||||
|
title: dom.find("h3 a").text().trim(),
|
||||||
|
cover: dom.find(".pic-wrap img").attr("src"),
|
||||||
|
desc: dom.find(".block p").text().trim(),
|
||||||
|
timestamp: dom.find("span.pubtime").text().trim(),
|
||||||
|
hot: null,
|
||||||
|
url,
|
||||||
|
mobileUrl: `https://m.douban.com/group/topic/${getNumbers(url)}/`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: listData,
|
||||||
|
};
|
||||||
|
};
|
||||||
64
src/routes/douban-movie.ts
Normal file
64
src/routes/douban-movie.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import { load } from "cheerio";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "douban-movie",
|
||||||
|
title: "豆瓣电影",
|
||||||
|
type: "新片排行榜",
|
||||||
|
link: "https://movie.douban.com/chart",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 数据处理
|
||||||
|
const getNumbers = (text: string | undefined) => {
|
||||||
|
if (!text) return 10000000;
|
||||||
|
const regex = /\d+/;
|
||||||
|
const match = text.match(regex);
|
||||||
|
if (match) {
|
||||||
|
return Number(match[0]);
|
||||||
|
} else {
|
||||||
|
return 10000000;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://movie.douban.com/chart/`;
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const $ = load(result.data);
|
||||||
|
const listDom = $(".article tr.item");
|
||||||
|
const listData = listDom.toArray().map((item) => {
|
||||||
|
const dom = $(item);
|
||||||
|
const url = dom.find("a").attr("href") || undefined;
|
||||||
|
const score = dom.find(".rating_nums").text() ?? "0.0";
|
||||||
|
return {
|
||||||
|
id: getNumbers(url),
|
||||||
|
title: `【${score}】${dom.find("a").attr("title")}`,
|
||||||
|
cover: dom.find("img").attr("src"),
|
||||||
|
desc: dom.find("p.pl").text(),
|
||||||
|
timestamp: null,
|
||||||
|
hot: getNumbers(dom.find("span.pl").text()),
|
||||||
|
url,
|
||||||
|
mobileUrl: `https://m.douban.com/movie/subject/${getNumbers(url)}/`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: listData,
|
||||||
|
};
|
||||||
|
};
|
||||||
61
src/routes/douyin.ts
Normal file
61
src/routes/douyin.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "douyin",
|
||||||
|
title: "抖音",
|
||||||
|
type: "热榜",
|
||||||
|
description: "实时上升热点",
|
||||||
|
link: "https://www.douyin.com",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取抖音临时 Cookis
|
||||||
|
const getDyCookies = async () => {
|
||||||
|
try {
|
||||||
|
const cookisUrl = "https://www.douyin.com/passport/general/login_guiding_strategy/?aid=6383";
|
||||||
|
const { data } = await get({ url: cookisUrl, originaInfo: true });
|
||||||
|
const pattern = /passport_csrf_token=(.*); Path/s;
|
||||||
|
const matchResult = data.headers["set-cookie"][0].match(pattern);
|
||||||
|
const cookieData = matchResult[1];
|
||||||
|
return cookieData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取抖音 Cookie 出错" + error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url =
|
||||||
|
"https://www.douyin.com/aweme/v1/web/hot/search/list/?device_platform=webapp&aid=6383&channel=channel_pc_web&detail_list=1";
|
||||||
|
const cookie = await getDyCookies();
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
headers: {
|
||||||
|
Cookie: `passport_csrf_token=${cookie}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const list = result.data.data.word_list;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["douyin"]) => ({
|
||||||
|
id: v.sentence_id,
|
||||||
|
title: v.word,
|
||||||
|
timestamp: getTime(v.event_time),
|
||||||
|
hot: v.hot_value,
|
||||||
|
url: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`,
|
||||||
|
mobileUrl: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
79
src/routes/earthquake.ts
Normal file
79
src/routes/earthquake.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
const mappings = {
|
||||||
|
O_TIME: "发震时刻(UTC+8)",
|
||||||
|
LOCATION_C: "参考位置",
|
||||||
|
M: "震级(M)",
|
||||||
|
EPI_LAT: "纬度(°)",
|
||||||
|
EPI_LON: "经度(°)",
|
||||||
|
EPI_DEPTH: "深度(千米)",
|
||||||
|
SAVE_TIME: "录入时间",
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeMappings = {
|
||||||
|
1: "最近24小时地震信息",
|
||||||
|
2: "最近48小时地震信息",
|
||||||
|
3: "最近7天地震信息",
|
||||||
|
4: "最近30天地震信息",
|
||||||
|
5: "最近一年3.0级以上地震信息",
|
||||||
|
6: "最近一年地震信息",
|
||||||
|
7: "最近一年3.0级以下地震",
|
||||||
|
8: "最近一年4.0级以上地震信息",
|
||||||
|
9: "最近一年5.0级以上地震信息",
|
||||||
|
0: "最近一年6.0级以上地震信息",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "5";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "earthquake",
|
||||||
|
title: "中国地震台",
|
||||||
|
type: "地震速报",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "速报分类",
|
||||||
|
type: {
|
||||||
|
...typeMappings,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://news.ceic.ac.cn/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const url = `http://www.ceic.ac.cn/ajax/speedsearch?num=${type}`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const data = result.data.replace(/,"page":"(.*?)","num":/, ',"num":');
|
||||||
|
const list = JSON.parse(data.substring(1, data.length - 1)).shuju;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["earthquake"]) => {
|
||||||
|
const contentBuilder = [];
|
||||||
|
const { NEW_DID, LOCATION_C, M } = v;
|
||||||
|
for (const mappingsKey in mappings) {
|
||||||
|
contentBuilder.push(`${mappings[mappingsKey]}:${v[mappingsKey]}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: NEW_DID,
|
||||||
|
title: `${LOCATION_C}发生${M}级地震`,
|
||||||
|
desc: contentBuilder.join("\n"),
|
||||||
|
timestamp: getTime(v["O_TIME"]),
|
||||||
|
hot: null,
|
||||||
|
url: `https://news.ceic.ac.cn/${NEW_DID}.html`,
|
||||||
|
mobileUrl: `https://news.ceic.ac.cn/${NEW_DID}.html`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
55
src/routes/genshin.ts
Normal file
55
src/routes/genshin.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "1";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "genshin",
|
||||||
|
title: "原神",
|
||||||
|
type: "最新动态",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "榜单分类",
|
||||||
|
type: {
|
||||||
|
1: "公告",
|
||||||
|
2: "活动",
|
||||||
|
3: "资讯",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://www.miyoushe.com/ys/home/28",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const url = `https://bbs-api.miyoushe.com/post/wapi/getNewsList?gids=2&page_size=20&type=${type}`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data.list;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["miyoushe"]) => {
|
||||||
|
const data = v.post;
|
||||||
|
return {
|
||||||
|
id: data.post_id,
|
||||||
|
title: data.subject,
|
||||||
|
desc: data.content,
|
||||||
|
cover: data.cover,
|
||||||
|
author: v.user.nickname,
|
||||||
|
timestamp: getTime(data.created_at),
|
||||||
|
hot: v.stat.view_num,
|
||||||
|
url: `https://www.miyoushe.com/ys/article/${data.post_id}`,
|
||||||
|
mobileUrl: `https://m.miyoushe.com/ys/#/article/${data.post_id}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
51
src/routes/hellogithub.ts
Normal file
51
src/routes/hellogithub.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const sort = c.req.query("sort") || "featured";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ sort }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "hellogithub",
|
||||||
|
title: "HelloGitHub",
|
||||||
|
type: "热门仓库",
|
||||||
|
description: "分享 GitHub 上有趣、入门级的开源项目",
|
||||||
|
parame: {
|
||||||
|
sort: {
|
||||||
|
name: "排行榜分区",
|
||||||
|
type: {
|
||||||
|
featured: "精选",
|
||||||
|
all: "全部",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://hellogithub.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { sort } = options;
|
||||||
|
const url = `https://abroad.hellogithub.com/v1/?sort_by=${sort}&tid=&page=1`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["hellogithub"]) => ({
|
||||||
|
id: v.item_id,
|
||||||
|
title: v.title,
|
||||||
|
desc: v.summary,
|
||||||
|
author: v.author,
|
||||||
|
timestamp: getTime(v.updated_at),
|
||||||
|
hot: v.clicks_total,
|
||||||
|
url: `https://hellogithub.com/repository/${v.item_id}`,
|
||||||
|
mobileUrl: `https://hellogithub.com/repository/${v.item_id}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
51
src/routes/history.ts
Normal file
51
src/routes/history.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import { load } from "cheerio";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getCurrentDateTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
// 获取日期
|
||||||
|
const day = c.req.query("day") || getCurrentDateTime().day;
|
||||||
|
const month = c.req.query("month") || getCurrentDateTime().month;
|
||||||
|
const { fromCache, data, updateTime } = await getList({ month, day }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "history",
|
||||||
|
title: "历史上的今天",
|
||||||
|
type: `${month}-${day}`,
|
||||||
|
parame: {
|
||||||
|
month: "月份",
|
||||||
|
day: "日期",
|
||||||
|
},
|
||||||
|
link: "https://www.lssjt.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { month, day } = options;
|
||||||
|
const url = `https://www.lssjt.com/${month}/${day}/`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const $ = load(result.data);
|
||||||
|
const listDom = $("li.circler");
|
||||||
|
const listData = listDom.toArray().map((item, index) => {
|
||||||
|
const dom = $(item);
|
||||||
|
const href = dom.find("a").attr("href");
|
||||||
|
return {
|
||||||
|
id: index,
|
||||||
|
title: dom.find("a.txt").text().trim() || dom.find("a").attr("title"),
|
||||||
|
cover: dom.find("img").attr("data-original"),
|
||||||
|
timestamp: dom.find("div.text span").text().trim() || dom.find("div.t span").text().trim(),
|
||||||
|
hot: null,
|
||||||
|
url: href || undefined,
|
||||||
|
mobileUrl: href || undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: listData,
|
||||||
|
};
|
||||||
|
};
|
||||||
55
src/routes/honkai.ts
Normal file
55
src/routes/honkai.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "1";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "honkai",
|
||||||
|
title: "崩坏3",
|
||||||
|
type: "最新动态",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "榜单分类",
|
||||||
|
type: {
|
||||||
|
1: "公告",
|
||||||
|
2: "活动",
|
||||||
|
3: "资讯",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://www.miyoushe.com/bh3/home/6",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const url = `https://bbs-api.miyoushe.com/post/wapi/getNewsList?gids=1&page_size=20&type=${type}`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data.list;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["miyoushe"]) => {
|
||||||
|
const data = v.post;
|
||||||
|
return {
|
||||||
|
id: data.post_id,
|
||||||
|
title: data.subject,
|
||||||
|
desc: data.content,
|
||||||
|
cover: data.cover || v.image_list[0].url,
|
||||||
|
author: v.user.nickname,
|
||||||
|
timestamp: getTime(data.created_at),
|
||||||
|
hot: v.stat.view_num,
|
||||||
|
url: `https://www.miyoushe.com/bh3/article/${data.post_id}`,
|
||||||
|
mobileUrl: `https://m.miyoushe.com/bh3/#/article/${data.post_id}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
66
src/routes/hostloc.ts
Normal file
66
src/routes/hostloc.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { web } from "../utils/getData.js";
|
||||||
|
import { extractRss, parseRSS } from "../utils/parseRSS.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "hot";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "hostloc",
|
||||||
|
title: "全球主机交流",
|
||||||
|
type: "榜单",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "榜单分类",
|
||||||
|
type: {
|
||||||
|
hot: "最新热门",
|
||||||
|
digest: "最新精华",
|
||||||
|
new: "最新回复",
|
||||||
|
newthread: "最新发表",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://hostloc.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const url = `https://hostloc.com/forum.php?mod=guide&view=${type}&rss=1`;
|
||||||
|
const result = await web({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36",
|
||||||
|
});
|
||||||
|
const parseData = async () => {
|
||||||
|
if (typeof result?.data === "string") {
|
||||||
|
const rssContent = extractRss(result.data);
|
||||||
|
return await parseRSS(rssContent);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const list = await parseData();
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["discuz"]) => ({
|
||||||
|
id: v.guid,
|
||||||
|
title: v.title,
|
||||||
|
desc: v.content,
|
||||||
|
author: v.author,
|
||||||
|
timestamp: getTime(v.pubDate),
|
||||||
|
hot: null,
|
||||||
|
url: v.link,
|
||||||
|
mobileUrl: v.link,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
54
src/routes/huxiu.ts
Normal file
54
src/routes/huxiu.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "huxiu",
|
||||||
|
title: "虎嗅",
|
||||||
|
type: "24小时",
|
||||||
|
link: "https://www.huxiu.com/moment/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标题处理
|
||||||
|
const titleProcessing = (text: string) => {
|
||||||
|
const paragraphs = text.split("<br><br>");
|
||||||
|
const title = paragraphs.shift().replace(/。$/, "");
|
||||||
|
const intro = paragraphs.join("<br><br>");
|
||||||
|
return { title, intro };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://www.huxiu.com/moment/`;
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
});
|
||||||
|
// 正则查找
|
||||||
|
const pattern =
|
||||||
|
/<script>[\s\S]*?window\.__INITIAL_STATE__\s*=\s*(\{[\s\S]*?\});[\s\S]*?<\/script>/;
|
||||||
|
const matchResult = result.data.match(pattern);
|
||||||
|
const jsonObject = JSON.parse(matchResult[1]).moment.momentList.moment_list.datalist;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: jsonObject.map((v: RouterType["huxiu"]) => ({
|
||||||
|
id: v.object_id,
|
||||||
|
title: titleProcessing(v.content).title,
|
||||||
|
desc: titleProcessing(v.content).intro,
|
||||||
|
author: v.user_info.username,
|
||||||
|
timestamp: getTime(v.publish_time),
|
||||||
|
hot: null,
|
||||||
|
url: v.url || "https://www.huxiu.com/moment/",
|
||||||
|
mobileUrl: v.url || "https://m.huxiu.com/moment/",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
39
src/routes/ifanr.ts
Normal file
39
src/routes/ifanr.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "ifanr",
|
||||||
|
title: "爱范儿",
|
||||||
|
type: "快讯",
|
||||||
|
description: "15秒了解全球新鲜事",
|
||||||
|
link: "https://www.ifanr.com/digest/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = "https://sso.ifanr.com/api/v5/wp/buzz/?limit=20&offset=0";
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.objects;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["ifanr"]) => ({
|
||||||
|
id: v.id,
|
||||||
|
title: v.post_title,
|
||||||
|
desc: v.post_content,
|
||||||
|
timestamp: getTime(v.created_at),
|
||||||
|
hot: v.like_count || v.comment_count,
|
||||||
|
url: `https://www.ifanr.com/${v.id}` || v.buzz_original_url,
|
||||||
|
mobileUrl: `https://www.ifanr.com/digest/${v.id}` || v.buzz_original_url,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
58
src/routes/ithome-xijiayi.ts
Normal file
58
src/routes/ithome-xijiayi.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import { load } from "cheerio";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "ithome-xijiayi",
|
||||||
|
title: "IT之家「喜加一」",
|
||||||
|
type: "最新动态",
|
||||||
|
description: "最新最全的「喜加一」游戏动态尽在这里!",
|
||||||
|
link: "https://www.ithome.com/zt/xijiayi",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 链接处理
|
||||||
|
const replaceLink = (url: string, getId: boolean = false) => {
|
||||||
|
const match = url.match(/https:\/\/www\.ithome\.com\/0\/(\d+)\/(\d+)\.htm/);
|
||||||
|
if (match && match[1] && match[2]) {
|
||||||
|
return getId ? match[1] + match[2] : `https://m.ithome.com/html/${match[1]}${match[2]}.htm`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://www.ithome.com/zt/xijiayi`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const $ = load(result.data);
|
||||||
|
const listDom = $(".newslist li");
|
||||||
|
const listData = listDom.toArray().map((item) => {
|
||||||
|
const dom = $(item);
|
||||||
|
const href = dom.find("a").attr("href");
|
||||||
|
const time = dom.find("span.time").text().trim();
|
||||||
|
const match = time.match(/'([^']+)'/);
|
||||||
|
const dateTime = match ? match[1] : null;
|
||||||
|
return {
|
||||||
|
id: href ? Number(replaceLink(href, true)) : 100000,
|
||||||
|
title: dom.find(".newsbody h2").text().trim(),
|
||||||
|
desc: dom.find(".newsbody p").text().trim(),
|
||||||
|
cover: dom.find("img").attr("data-original"),
|
||||||
|
timestamp: getTime(dateTime),
|
||||||
|
hot: Number(dom.find(".comment").text().replace(/\D/g, "")),
|
||||||
|
url: href || undefined,
|
||||||
|
mobileUrl: href ? replaceLink(href) : undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: listData,
|
||||||
|
};
|
||||||
|
};
|
||||||
57
src/routes/ithome.ts
Normal file
57
src/routes/ithome.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import { load } from "cheerio";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "ithome",
|
||||||
|
title: "IT之家",
|
||||||
|
type: "热榜",
|
||||||
|
description: "爱科技,爱这里 - 前沿科技新闻网站",
|
||||||
|
link: "https://m.ithome.com/rankm/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 链接处理
|
||||||
|
const replaceLink = (url: string, getId: boolean = false) => {
|
||||||
|
const match = url.match(/[html|live]\/(\d+)\.htm/);
|
||||||
|
// 是否匹配成功
|
||||||
|
if (match && match[1]) {
|
||||||
|
return getId
|
||||||
|
? match[1]
|
||||||
|
: `https://www.ithome.com/0/${match[1].slice(0, 3)}/${match[1].slice(3)}.htm`;
|
||||||
|
}
|
||||||
|
// 返回原始 URL
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://m.ithome.com/rankm/`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const $ = load(result.data);
|
||||||
|
const listDom = $(".rank-box .placeholder");
|
||||||
|
const listData = listDom.toArray().map((item) => {
|
||||||
|
const dom = $(item);
|
||||||
|
const href = dom.find("a").attr("href");
|
||||||
|
return {
|
||||||
|
id: href ? Number(replaceLink(href, true)) : 100000,
|
||||||
|
title: dom.find(".plc-title").text().trim(),
|
||||||
|
cover: dom.find("img").attr("data-original"),
|
||||||
|
timestamp: dom.find("span.post-time").text().trim(),
|
||||||
|
hot: Number(dom.find(".review-num").text().replace(/\D/g, "")),
|
||||||
|
url: href ? replaceLink(href) : undefined,
|
||||||
|
mobileUrl: href || undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: listData,
|
||||||
|
};
|
||||||
|
};
|
||||||
59
src/routes/jianshu.ts
Normal file
59
src/routes/jianshu.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import { load } from "cheerio";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "jianshu",
|
||||||
|
title: "简书",
|
||||||
|
type: "热门推荐",
|
||||||
|
description: "一个优质的创作社区",
|
||||||
|
link: "https://www.jianshu.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取 ID
|
||||||
|
const getID = (url: string) => {
|
||||||
|
if (!url) return "undefined";
|
||||||
|
const match = url.match(/([^/]+)$/);
|
||||||
|
return match ? match[1] : "undefined";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://www.jianshu.com/`;
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
headers: {
|
||||||
|
Referer: "https://www.jianshu.com",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const $ = load(result.data);
|
||||||
|
const listDom = $("ul.note-list li");
|
||||||
|
const listData = listDom.toArray().map((item) => {
|
||||||
|
const dom = $(item);
|
||||||
|
const href = dom.find("a").attr("href") || "";
|
||||||
|
return {
|
||||||
|
id: getID(href),
|
||||||
|
title: dom.find("a.title").text()?.trim(),
|
||||||
|
cover: dom.find("img").attr("src"),
|
||||||
|
desc: dom.find("p.abstract").text()?.trim(),
|
||||||
|
author: dom.find("a.nickname").text()?.trim(),
|
||||||
|
hot: null,
|
||||||
|
timestamp: null,
|
||||||
|
url: `https://www.jianshu.com${href}`,
|
||||||
|
mobileUrl: `https://www.jianshu.com${href}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: listData,
|
||||||
|
};
|
||||||
|
};
|
||||||
37
src/routes/juejin.ts
Normal file
37
src/routes/juejin.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "juejin",
|
||||||
|
title: "稀土掘金",
|
||||||
|
type: "文章榜",
|
||||||
|
link: "https://juejin.cn/hot/articles",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["juejin"]) => ({
|
||||||
|
id: v.content.content_id,
|
||||||
|
title: v.content.title,
|
||||||
|
author: v.author.name,
|
||||||
|
hot: v.content_counter.hot_rank,
|
||||||
|
timestamp: null,
|
||||||
|
url: `https://juejin.cn/post/${v.content.content_id}`,
|
||||||
|
mobileUrl: `https://juejin.cn/post/${v.content.content_id}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
40
src/routes/lol.ts
Normal file
40
src/routes/lol.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "lol",
|
||||||
|
title: "英雄联盟",
|
||||||
|
type: "更新公告",
|
||||||
|
link: "https://lol.qq.com/gicp/news/423/2/1334/1.html",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url =
|
||||||
|
"https://apps.game.qq.com/cmc/zmMcnTargetContentList?r0=json&page=1&num=30&target=24&source=web_pc";
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data.result;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["lol"]) => ({
|
||||||
|
id: v.iDocID,
|
||||||
|
title: v.sTitle,
|
||||||
|
cover: `https:${v.sIMG}`,
|
||||||
|
author: v.sAuthor,
|
||||||
|
hot: Number(v.iTotalPlay),
|
||||||
|
timestamp: getTime(v.sCreated),
|
||||||
|
url: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(v.iDocID)}`,
|
||||||
|
mobileUrl: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(v.iDocID)}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
39
src/routes/netease-news.ts
Normal file
39
src/routes/netease-news.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "netease-news",
|
||||||
|
title: "网易新闻",
|
||||||
|
type: "热点榜",
|
||||||
|
link: "https://m.163.com/hot",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://m.163.com/fe/api/hot/news/flow`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data.list;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["netease-news"]) => ({
|
||||||
|
id: v.docid,
|
||||||
|
title: v.title,
|
||||||
|
cover: v.imgsrc,
|
||||||
|
author: v.source,
|
||||||
|
hot: null,
|
||||||
|
timestamp: getTime(v.ptime),
|
||||||
|
url: `https://www.163.com/dy/article/${v.docid}.html`,
|
||||||
|
mobileUrl: `https://m.163.com/dy/article/${v.docid}.html`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
57
src/routes/ngabbs.ts
Normal file
57
src/routes/ngabbs.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { post } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "ngabbs",
|
||||||
|
title: "NGA",
|
||||||
|
type: "论坛热帖",
|
||||||
|
description: "精英玩家俱乐部",
|
||||||
|
link: "https://ngabbs.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://ngabbs.com/nuke.php?__lib=load_topic&__act=load_topic_reply_ladder2&opt=1&all=1`;
|
||||||
|
const result = await post({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
headers: {
|
||||||
|
Accept: "*/*",
|
||||||
|
Host: "ngabbs.com",
|
||||||
|
Referer: "https://ngabbs.com/",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
"Content-Length": "11",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Accept-Language": "zh-Hans-CN;q=1",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",
|
||||||
|
"X-User-Agent": "NGA_skull/7.3.1(iPhone13,2;iOS 17.2.1)",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
__output: "14",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const list = result.data.result[0];
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["ngabbs"]) => ({
|
||||||
|
id: v.tid,
|
||||||
|
title: v.subject,
|
||||||
|
author: v.author,
|
||||||
|
hot: v.replies,
|
||||||
|
timestamp: getTime(v.postdate),
|
||||||
|
url: `https://bbs.nga.cn${v.tpcurl}`,
|
||||||
|
mobileUrl: `https://bbs.nga.cn${v.tpcurl}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
40
src/routes/qq-news.ts
Normal file
40
src/routes/qq-news.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "qq-news",
|
||||||
|
title: "腾讯新闻",
|
||||||
|
type: "热点榜",
|
||||||
|
link: "https://news.qq.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://r.inews.qq.com/gw/event/hot_ranking_list?page_size=50`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.idlist[0].newslist.slice(1);
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["qq-news"]) => ({
|
||||||
|
id: v.id,
|
||||||
|
title: v.title,
|
||||||
|
desc: v.abstract,
|
||||||
|
cover: v.miniProShareImage,
|
||||||
|
author: v.source,
|
||||||
|
hot: v.hotEvent.hotScore,
|
||||||
|
timestamp: getTime(v.timestamp),
|
||||||
|
url: `https://new.qq.com/rain/a/${v.id}`,
|
||||||
|
mobileUrl: `https://view.inews.qq.com/k/${v.id}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
48
src/routes/sspai.ts
Normal file
48
src/routes/sspai.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "热门文章";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "sspai",
|
||||||
|
title: "少数派",
|
||||||
|
type: "热榜",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "分类",
|
||||||
|
type: ["热门文章", "应用推荐", "生活方式", "效率技巧", "少数派播客"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://sspai.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const url = `https://sspai.com/api/v1/article/tag/page/get?limit=40&tag=${type}`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["sspai"]) => ({
|
||||||
|
id: v.id,
|
||||||
|
title: v.title,
|
||||||
|
desc: v.summary,
|
||||||
|
cover: v.banner,
|
||||||
|
author: v.author.nickname,
|
||||||
|
timestamp: getTime(v.released_time),
|
||||||
|
hot: v.like_count,
|
||||||
|
url: `https://sspai.com/post/${v.id}`,
|
||||||
|
mobileUrl: `https://sspai.com/post/${v.id}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
55
src/routes/starrail.ts
Normal file
55
src/routes/starrail.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "1";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "starrail",
|
||||||
|
title: "崩坏:星穹铁道",
|
||||||
|
type: "最新动态",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "榜单分类",
|
||||||
|
type: {
|
||||||
|
1: "公告",
|
||||||
|
2: "活动",
|
||||||
|
3: "资讯",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://www.miyoushe.com/sr/home/53",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const url = `https://bbs-api.miyoushe.com/post/wapi/getNewsList?gids=6&page_size=20&type=${type}`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data.list;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["miyoushe"]) => {
|
||||||
|
const data = v.post;
|
||||||
|
return {
|
||||||
|
id: data.post_id,
|
||||||
|
title: data.subject,
|
||||||
|
desc: data.content,
|
||||||
|
cover: data.cover,
|
||||||
|
author: v.user.nickname,
|
||||||
|
timestamp: getTime(data.created_at),
|
||||||
|
hot: v.stat.view_num,
|
||||||
|
url: `https://www.miyoushe.com/sr/article/${data.post_id}`,
|
||||||
|
mobileUrl: `https://m.miyoushe.com/sr/#/article/${data.post_id}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
38
src/routes/thepaper.ts
Normal file
38
src/routes/thepaper.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "thepaper",
|
||||||
|
title: "澎湃新闻",
|
||||||
|
type: "热榜",
|
||||||
|
link: "https://www.thepaper.cn/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://cache.thepaper.cn/contentapi/wwwIndex/rightSidebar`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data.hotNews;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["thepaper"]) => ({
|
||||||
|
id: v.contId,
|
||||||
|
title: v.name,
|
||||||
|
cover: v.pic,
|
||||||
|
hot: Number(v.praiseTimes),
|
||||||
|
timestamp: getTime(v.pubTimeLong),
|
||||||
|
url: `https://www.thepaper.cn/newsDetail_forward_${v.contId}`,
|
||||||
|
mobileUrl: `https://m.thepaper.cn/newsDetail_forward_${v.contId}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
40
src/routes/tieba.ts
Normal file
40
src/routes/tieba.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "tieba",
|
||||||
|
title: "百度贴吧",
|
||||||
|
type: "热议榜",
|
||||||
|
description: "全球领先的中文社区",
|
||||||
|
link: "https://tieba.baidu.com/hottopic/browse/topicList",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://tieba.baidu.com/hottopic/browse/topicList`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data.bang_topic.topic_list;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["tieba"]) => ({
|
||||||
|
id: v.topic_id,
|
||||||
|
title: v.topic_name,
|
||||||
|
desc: v.topic_desc,
|
||||||
|
cover: v.topic_pic,
|
||||||
|
hot: v.discuss_num,
|
||||||
|
timestamp: getTime(v.create_time),
|
||||||
|
url: v.topic_url,
|
||||||
|
mobileUrl: v.topic_url,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
38
src/routes/toutiao.ts
Normal file
38
src/routes/toutiao.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "toutiao",
|
||||||
|
title: "今日头条",
|
||||||
|
type: "热榜",
|
||||||
|
link: "https://www.toutiao.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["toutiao"]) => ({
|
||||||
|
id: v.ClusterIdStr,
|
||||||
|
title: v.Title,
|
||||||
|
cover: v.Image.url,
|
||||||
|
timestamp: getTime(v.ClusterIdStr),
|
||||||
|
hot: Number(v.HotValue),
|
||||||
|
url: `https://www.toutiao.com/trending/${v.ClusterIdStr}/`,
|
||||||
|
mobileUrl: `https://api.toutiaoapi.com/feoffline/amos_land/new/html/main/index.html?topic_id=${v.ClusterIdStr}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
49
src/routes/v2ex.ts
Normal file
49
src/routes/v2ex.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const type = c.req.query("type") || "hot";
|
||||||
|
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "v2ex",
|
||||||
|
title: "V2EX",
|
||||||
|
type: "主题榜",
|
||||||
|
parame: {
|
||||||
|
type: {
|
||||||
|
name: "榜单分类",
|
||||||
|
type: {
|
||||||
|
hot: "最热主题",
|
||||||
|
latest: "最新主题",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "https://www.v2ex.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { type } = options;
|
||||||
|
const url = `https://www.v2ex.com/api/topics/${type}.json`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["v2ex"]) => ({
|
||||||
|
id: v.id,
|
||||||
|
title: v.title,
|
||||||
|
desc: v.content,
|
||||||
|
author: v.member.username,
|
||||||
|
timestamp: null,
|
||||||
|
hot: v.replies,
|
||||||
|
url: v.url,
|
||||||
|
mobileUrl: v.url,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
48
src/routes/weatheralarm.ts
Normal file
48
src/routes/weatheralarm.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { RouterData, ListContext, Options } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||||
|
const province = c.req.query("province") || "";
|
||||||
|
const { fromCache, data, type, updateTime } = await getList({ province }, noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "weatheralarm",
|
||||||
|
title: "中央气象台",
|
||||||
|
type: type || "全国气象预警",
|
||||||
|
parame: {
|
||||||
|
province: {
|
||||||
|
name: "预警区域",
|
||||||
|
value: "省份名称( 例如:广东省 )",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "http://nmc.cn/publish/alarm.html",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (options: Options, noCache: boolean) => {
|
||||||
|
const { province } = options;
|
||||||
|
const url = `http://www.nmc.cn/rest/findAlarm?pageNo=1&pageSize=20&signaltype=&signallevel=&province=${encodeURIComponent(province)}`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data.page.list;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
type: province + "气象预警",
|
||||||
|
data: list.map((v: RouterType["weatheralarm"]) => ({
|
||||||
|
id: v.alertid,
|
||||||
|
title: v.title,
|
||||||
|
desc: v.issuetime + " " + v.title,
|
||||||
|
cover: v.pic,
|
||||||
|
timestamp: getTime(v.issuetime),
|
||||||
|
hot: null,
|
||||||
|
url: `http://nmc.cn${v.url}`,
|
||||||
|
mobileUrl: `http://nmc.cn${v.url}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
45
src/routes/weibo.ts
Normal file
45
src/routes/weibo.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "weibo",
|
||||||
|
title: "微博",
|
||||||
|
type: "热搜榜",
|
||||||
|
description: "实时热点,每分钟更新一次",
|
||||||
|
link: "https://s.weibo.com/top/summary/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://weibo.com/ajax/side/hotSearch`;
|
||||||
|
const result = await get({ url, noCache, ttl: 60 });
|
||||||
|
const list = result.data.data.realtime;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["weibo"]) => {
|
||||||
|
const key = v.word_scheme ? v.word_scheme : `#${v.word}`;
|
||||||
|
return {
|
||||||
|
id: v.mid,
|
||||||
|
title: v.word,
|
||||||
|
desc: v.note || key,
|
||||||
|
author: v.category,
|
||||||
|
timestamp: getTime(v.onboard_time),
|
||||||
|
hot: v.raw_hot,
|
||||||
|
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(key)}&t=31&band_rank=1&Refer=top`,
|
||||||
|
mobileUrl: `https://s.weibo.com/weibo?q=${encodeURIComponent(
|
||||||
|
key,
|
||||||
|
)}&t=31&band_rank=1&Refer=top`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
51
src/routes/weread.ts
Normal file
51
src/routes/weread.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import getWereadID from "../utils/getToken/weread.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "weread",
|
||||||
|
title: "微信读书",
|
||||||
|
type: "飙升榜",
|
||||||
|
link: "https://weread.qq.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://weread.qq.com/web/bookListInCategory/rising?rank=1`;
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const list = result.data.books;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["weread"]) => {
|
||||||
|
const data = v.bookInfo;
|
||||||
|
return {
|
||||||
|
id: data.bookId,
|
||||||
|
title: data.title,
|
||||||
|
author: data.author,
|
||||||
|
desc: data.intro,
|
||||||
|
cover: data.cover.replace("s_", "t9_"),
|
||||||
|
timestamp: getTime(data.publishTime),
|
||||||
|
hot: v.readingCount,
|
||||||
|
url: `https://weread.qq.com/web/bookDetail/${getWereadID(data.bookId)}`,
|
||||||
|
mobileUrl: `https://weread.qq.com/web/bookDetail/${getWereadID(data.bookId)}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
46
src/routes/zhihu-daily.ts
Normal file
46
src/routes/zhihu-daily.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "zhihu-daily",
|
||||||
|
title: "知乎日报",
|
||||||
|
type: "推荐榜",
|
||||||
|
description: "每天三次,每次七分钟",
|
||||||
|
link: "https://daily.zhihu.com/",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://daily.zhihu.com/api/4/news/latest`;
|
||||||
|
const result = await get({
|
||||||
|
url,
|
||||||
|
noCache,
|
||||||
|
headers: {
|
||||||
|
Referer: "https://daily.zhihu.com/api/4/news/latest",
|
||||||
|
Host: "daily.zhihu.com",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const list = result.data.stories.filter((el: RouterType["zhihu-daily"]) => el.type === 0);
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["zhihu-daily"]) => ({
|
||||||
|
id: v.id,
|
||||||
|
title: v.title,
|
||||||
|
cover: v.images[0],
|
||||||
|
author: v.hint,
|
||||||
|
hot: null,
|
||||||
|
timestamp: null,
|
||||||
|
url: v.url,
|
||||||
|
mobileUrl: v.url,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
42
src/routes/zhihu.ts
Normal file
42
src/routes/zhihu.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { RouterData } from "../types.js";
|
||||||
|
import type { RouterType } from "../router.types.js";
|
||||||
|
import { get } from "../utils/getData.js";
|
||||||
|
import { getTime } from "../utils/getTime.js";
|
||||||
|
|
||||||
|
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||||
|
const { fromCache, data, updateTime } = await getList(noCache);
|
||||||
|
const routeData: RouterData = {
|
||||||
|
name: "zhihu",
|
||||||
|
title: "知乎",
|
||||||
|
type: "热榜",
|
||||||
|
link: "https://www.zhihu.com/hot",
|
||||||
|
total: data?.length || 0,
|
||||||
|
updateTime,
|
||||||
|
fromCache,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return routeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getList = async (noCache: boolean) => {
|
||||||
|
const url = `https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=50&desktop=true`;
|
||||||
|
const result = await get({ url, noCache });
|
||||||
|
const list = result.data.data;
|
||||||
|
return {
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updateTime: result.updateTime,
|
||||||
|
data: list.map((v: RouterType["zhihu"]) => {
|
||||||
|
const data = v.target;
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
title: data.title,
|
||||||
|
desc: data.excerpt,
|
||||||
|
cover: v.children[0].thumbnail,
|
||||||
|
timestamp: getTime(data.created),
|
||||||
|
hot: parseInt(v.detail_text.replace(/[^\d]/g, "")) * 10000,
|
||||||
|
url: `https://www.zhihu.com/question/${data.id}`,
|
||||||
|
mobileUrl: `https://www.zhihu.com/question/${data.id}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
65
src/types.d.ts
vendored
Normal file
65
src/types.d.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import type { Context } from "hono";
|
||||||
|
|
||||||
|
// Context
|
||||||
|
export type ListContext = Context;
|
||||||
|
|
||||||
|
// 榜单数据
|
||||||
|
export type ListItem = {
|
||||||
|
id: number | string;
|
||||||
|
title: string;
|
||||||
|
cover?: string;
|
||||||
|
author?: string;
|
||||||
|
desc?: string;
|
||||||
|
hot: number | null;
|
||||||
|
timestamp: number | string | null;
|
||||||
|
url: string | undefined;
|
||||||
|
mobileUrl: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 路由数据
|
||||||
|
export type RouterData = {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
parame?: Record<string, string | object>;
|
||||||
|
total: number;
|
||||||
|
link?: string;
|
||||||
|
updateTime: string;
|
||||||
|
fromCache: boolean;
|
||||||
|
data: ListItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 请求类型
|
||||||
|
export type Get = {
|
||||||
|
url: string;
|
||||||
|
headers?: Record<string, string | string[]>;
|
||||||
|
params?: Record<string, string | number>;
|
||||||
|
timeout?: number;
|
||||||
|
noCache?: boolean;
|
||||||
|
ttl?: number;
|
||||||
|
originaInfo?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Post = {
|
||||||
|
url: string;
|
||||||
|
headers?: Record<string, string | string[]>;
|
||||||
|
body?: string | object | Buffer | undefined;
|
||||||
|
timeout?: number;
|
||||||
|
noCache?: boolean;
|
||||||
|
ttl?: number;
|
||||||
|
originaInfo?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Web = {
|
||||||
|
url: string;
|
||||||
|
timeout?: number;
|
||||||
|
noCache?: boolean;
|
||||||
|
ttl?: number;
|
||||||
|
userAgent?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 参数类型
|
||||||
|
export type Options = {
|
||||||
|
[key: string]: string | number | undefined;
|
||||||
|
};
|
||||||
37
src/utils/cache.ts
Normal file
37
src/utils/cache.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { config } from "../config.js";
|
||||||
|
import NodeCache from "node-cache";
|
||||||
|
import logger from "./logger.js";
|
||||||
|
|
||||||
|
// init
|
||||||
|
const cache = new NodeCache({
|
||||||
|
// 缓存过期时间( 秒 )
|
||||||
|
stdTTL: config.CACHE_TTL,
|
||||||
|
// 定期检查过期缓存( 秒 )
|
||||||
|
checkperiod: 600,
|
||||||
|
// 克隆变量
|
||||||
|
useClones: false,
|
||||||
|
// 最大键值对
|
||||||
|
maxKeys: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface GetCache<T> {
|
||||||
|
updateTime: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从缓存中获取数据
|
||||||
|
export const getCache = <T>(key: string): GetCache<T> | undefined => {
|
||||||
|
return cache.get(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将数据写入缓存
|
||||||
|
export const setCache = <T>(key: string, value: T, ttl: number = config.CACHE_TTL) => {
|
||||||
|
const success = cache.set(key, value, ttl);
|
||||||
|
if (logger) logger.info("数据缓存成功", { url: key });
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从缓存中删除数据
|
||||||
|
export const delCache = (key: string) => {
|
||||||
|
return cache.del(key);
|
||||||
|
};
|
||||||
153
src/utils/getData.ts
Normal file
153
src/utils/getData.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import type { Get, Post, Web } from "../types.ts";
|
||||||
|
import { config } from "../config.js";
|
||||||
|
import { getCache, setCache, delCache } from "./cache.js";
|
||||||
|
// import { Cluster } from "puppeteer-cluster";
|
||||||
|
import logger from "./logger.js";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
// 基础配置
|
||||||
|
const request = axios.create({
|
||||||
|
// 请求超时设置
|
||||||
|
timeout: config.REQUEST_TIMEOUT,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// puppeteer-cluster
|
||||||
|
// export const createCluster = async () => {
|
||||||
|
// return await Cluster.launch({
|
||||||
|
// concurrency: Cluster.CONCURRENCY_BROWSER,
|
||||||
|
// maxConcurrency: 5,
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Cluster
|
||||||
|
// const cluster = await createCluster();
|
||||||
|
const cluster = null;
|
||||||
|
|
||||||
|
// Cluster configuration
|
||||||
|
// cluster.task(async ({ page, data: { url, userAgent } }) => {
|
||||||
|
// if (userAgent) {
|
||||||
|
// await page.setUserAgent(userAgent);
|
||||||
|
// }
|
||||||
|
// await page.goto(url, { waitUntil: "networkidle0" });
|
||||||
|
// const pageContent = await page.content();
|
||||||
|
// return pageContent;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 请求拦截
|
||||||
|
request.interceptors.request.use(
|
||||||
|
(request) => {
|
||||||
|
if (!request.params) request.params = {};
|
||||||
|
// 发送请求
|
||||||
|
return request;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
logger.error("请求失败,请稍后重试");
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 响应拦截
|
||||||
|
request.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 继续传递错误
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET
|
||||||
|
export const get = async (options: Get) => {
|
||||||
|
const { url, headers, params, noCache, ttl = config.CACHE_TTL, originaInfo = false } = options;
|
||||||
|
logger.info("发起 GET 请求", options);
|
||||||
|
try {
|
||||||
|
// 检查缓存
|
||||||
|
if (noCache) delCache(url);
|
||||||
|
else {
|
||||||
|
const cachedData = getCache(url);
|
||||||
|
if (cachedData) {
|
||||||
|
logger.info("采用缓存", { url });
|
||||||
|
return { fromCache: true, data: cachedData.data, updateTime: cachedData.updateTime };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 缓存不存在时请求接口
|
||||||
|
logger.info("请求接口", { url });
|
||||||
|
const response = await request.get(url, { headers, params });
|
||||||
|
const responseData = response?.data || response;
|
||||||
|
// 存储新获取的数据到缓存
|
||||||
|
const updateTime = new Date().toISOString();
|
||||||
|
const data = originaInfo ? response : responseData;
|
||||||
|
setCache(url, { data, updateTime }, ttl);
|
||||||
|
// 返回数据
|
||||||
|
logger.info("接口调用成功", { status: response?.statusText });
|
||||||
|
return { fromCache: false, data, updateTime };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("GET 请求出错", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// POST
|
||||||
|
export const post = async (options: Post) => {
|
||||||
|
const { url, headers, body, noCache, ttl = config.CACHE_TTL, originaInfo = false } = options;
|
||||||
|
logger.info("发起 POST 请求", options);
|
||||||
|
try {
|
||||||
|
// 检查缓存
|
||||||
|
if (noCache) delCache(url);
|
||||||
|
else {
|
||||||
|
const cachedData = getCache(url);
|
||||||
|
if (cachedData) {
|
||||||
|
logger.info("采用缓存", { url });
|
||||||
|
return { fromCache: true, data: cachedData.data, updateTime: cachedData.updateTime };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 缓存不存在时请求接口
|
||||||
|
logger.info("请求接口", { url });
|
||||||
|
const response = await request.post(url, body, { headers });
|
||||||
|
const responseData = response?.data || response;
|
||||||
|
// 存储新获取的数据到缓存
|
||||||
|
const updateTime = new Date().toISOString();
|
||||||
|
const data = originaInfo ? response : responseData;
|
||||||
|
if (!noCache) {
|
||||||
|
setCache(url, { data, updateTime }, ttl);
|
||||||
|
}
|
||||||
|
// 返回数据
|
||||||
|
logger.info("接口调用成功", { status: response?.statusText });
|
||||||
|
return { fromCache: false, data, updateTime };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("POST 请求出错", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// puppeteer
|
||||||
|
export const web = async (options: Web) => {
|
||||||
|
const { url, noCache, ttl = config.CACHE_TTL, userAgent } = options;
|
||||||
|
logger.info("使用 Puppeteer 发起页面请求", options);
|
||||||
|
try {
|
||||||
|
// 检查缓存
|
||||||
|
if (noCache) {
|
||||||
|
delCache(url);
|
||||||
|
} else {
|
||||||
|
const cachedData = getCache(url);
|
||||||
|
if (cachedData) {
|
||||||
|
logger.info("采用缓存", { url });
|
||||||
|
return { fromCache: true, data: cachedData.data, updateTime: cachedData.updateTime };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 缓存不存在时使用 Puppeteer 请求页面
|
||||||
|
logger.info("启动浏览器请求页面", { url });
|
||||||
|
const pageContent = await cluster.execute({ url, userAgent });
|
||||||
|
// 存储新获取的数据到缓存
|
||||||
|
const updateTime = new Date().toISOString();
|
||||||
|
setCache(url, { data: pageContent, updateTime }, ttl);
|
||||||
|
// 返回数据
|
||||||
|
logger.info("页面内容获取成功");
|
||||||
|
return { fromCache: false, data: pageContent, updateTime };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Puppeteer 请求出错", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
42
src/utils/getRSS.ts
Normal file
42
src/utils/getRSS.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { RouterData, ListItem } from "../types.ts";
|
||||||
|
import { Feed } from "feed";
|
||||||
|
import logger from "./logger.js";
|
||||||
|
|
||||||
|
// 生成 RSS
|
||||||
|
const getRSS = (data: RouterData) => {
|
||||||
|
try {
|
||||||
|
// 基本信息
|
||||||
|
const feed = new Feed({
|
||||||
|
title: data.title,
|
||||||
|
description: data.title + data.type + (data?.description ? " - " + data?.description : ""),
|
||||||
|
id: data.name,
|
||||||
|
link: data.link,
|
||||||
|
language: "zh",
|
||||||
|
generator: "DailyHotApi",
|
||||||
|
copyright: "Copyright © 2020-present imsyy",
|
||||||
|
updated: new Date(data.updateTime),
|
||||||
|
});
|
||||||
|
// 获取数据
|
||||||
|
const listData = data.data;
|
||||||
|
listData.forEach((item: ListItem) => {
|
||||||
|
feed.addItem({
|
||||||
|
id: item.id?.toString(),
|
||||||
|
title: item.title,
|
||||||
|
date: new Date(data.updateTime),
|
||||||
|
link: item.url || "获取失败",
|
||||||
|
description: item?.desc,
|
||||||
|
author: [
|
||||||
|
{
|
||||||
|
name: item.author,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const rssData = feed.rss2();
|
||||||
|
return rssData;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("RSS 生成失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getRSS;
|
||||||
79
src/utils/getTime.ts
Normal file
79
src/utils/getTime.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
interface CurrentDateTime {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
hour: number;
|
||||||
|
minute: number;
|
||||||
|
second: number;
|
||||||
|
}
|
||||||
|
export const getTime = (timeInput: string | number): number | null => {
|
||||||
|
try {
|
||||||
|
let num: number;
|
||||||
|
|
||||||
|
// 处理字符串的情况
|
||||||
|
if (typeof timeInput === "string") {
|
||||||
|
// 尝试将字符串直接转换为数字
|
||||||
|
num = Number(timeInput);
|
||||||
|
|
||||||
|
if (isNaN(num)) {
|
||||||
|
// 将各种分隔符替换为标准格式
|
||||||
|
let standardizedInput = timeInput
|
||||||
|
.replace(/(\d{4})-(\d{2})-(\d{2})-(\d{2})/, "$1-$2-$3 $4") // "YYYY-MM-DD-HH" -> "YYYY-MM-DD HH"
|
||||||
|
.replace(/(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):?(\d{2})?:?(\d{2})?/, "$1-$2-$3 $4:$5:$6") // "YYYY-MM-DDTHH:mm:ss" -> "YYYY-MM-DD HH:mm:ss"
|
||||||
|
.replace(/(\d{4})[-/](\d{2})[-/](\d{2})/, "$1-$2-$3"); // "YYYY/MM/DD" or "YYYY-MM-DD" -> "YYYY-MM-DD"
|
||||||
|
|
||||||
|
// 减少解析过程中可能的多余空格
|
||||||
|
standardizedInput = standardizedInput.replace(/\s+/, " ").trim();
|
||||||
|
|
||||||
|
// 处理标准化后的日期时间字符串
|
||||||
|
const formatPatterns = [
|
||||||
|
"YYYY-MM-DD HH:mm:ss",
|
||||||
|
"YYYY-MM-DD HH:mm",
|
||||||
|
"YYYY-MM-DD HH",
|
||||||
|
"YYYY-MM-DD",
|
||||||
|
];
|
||||||
|
|
||||||
|
let parsedDate: dayjs.Dayjs | null = null;
|
||||||
|
for (const pattern of formatPatterns) {
|
||||||
|
parsedDate = dayjs(standardizedInput, pattern, true);
|
||||||
|
if (parsedDate.isValid()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedDate && parsedDate.isValid()) {
|
||||||
|
return parsedDate.valueOf();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
num = timeInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否为毫秒级时间戳
|
||||||
|
if (num > 946684800000) {
|
||||||
|
// 以2000年作为毫秒时间戳参考点
|
||||||
|
return num;
|
||||||
|
} else {
|
||||||
|
return num * 1000;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCurrentDateTime = (): CurrentDateTime => {
|
||||||
|
const now = dayjs();
|
||||||
|
|
||||||
|
return {
|
||||||
|
year: now.year(),
|
||||||
|
month: now.month() + 1,
|
||||||
|
day: now.date(),
|
||||||
|
hour: now.hour(),
|
||||||
|
minute: now.minute(),
|
||||||
|
second: now.second(),
|
||||||
|
};
|
||||||
|
};
|
||||||
29
src/utils/getToken/51cto.ts
Normal file
29
src/utils/getToken/51cto.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { getCache, setCache } from "../cache.js";
|
||||||
|
import { get } from "../getData.js";
|
||||||
|
import md5 from "md5";
|
||||||
|
|
||||||
|
export const getToken = async () => {
|
||||||
|
const cachedData = getCache("51cto-token");
|
||||||
|
if (cachedData && typeof cachedData === "object" && "token" in cachedData) {
|
||||||
|
const { token } = cachedData as { token: string };
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
const result = await get({
|
||||||
|
url: "https://api-media.51cto.com/api/token-get",
|
||||||
|
});
|
||||||
|
const token = result.data.data.data.token;
|
||||||
|
setCache("51cto-token", { token });
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sign = (
|
||||||
|
requestPath: string,
|
||||||
|
payload: Record<string, unknown> = {},
|
||||||
|
timestamp: number,
|
||||||
|
token: string,
|
||||||
|
) => {
|
||||||
|
payload.timestamp = timestamp;
|
||||||
|
payload.token = token;
|
||||||
|
const sortedParams = Object.keys(payload).sort();
|
||||||
|
return md5(md5(requestPath) + md5(sortedParams + md5(token) + timestamp));
|
||||||
|
};
|
||||||
85
src/utils/getToken/bilibili.ts
Normal file
85
src/utils/getToken/bilibili.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// 获取 Bilibili Web 端 WBI 签名鉴权
|
||||||
|
import { getCache, setCache } from "../cache.js";
|
||||||
|
import { get } from "../getData.js";
|
||||||
|
import md5 from "md5";
|
||||||
|
|
||||||
|
type EncodedKeys = {
|
||||||
|
img_key: string;
|
||||||
|
sub_key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface WbiParams {
|
||||||
|
[key: string]: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mixinKeyEncTab = [
|
||||||
|
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28,
|
||||||
|
14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54,
|
||||||
|
21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 对 imgKey 和 subKey 进行字符顺序打乱编码
|
||||||
|
const getMixinKey = (orig: string): string =>
|
||||||
|
mixinKeyEncTab
|
||||||
|
.map((n) => orig[n])
|
||||||
|
.join("")
|
||||||
|
.slice(0, 32);
|
||||||
|
|
||||||
|
// 为请求参数进行 wbi 签名
|
||||||
|
const encWbi = (params: WbiParams, img_key: string, sub_key: string): string => {
|
||||||
|
const mixin_key = getMixinKey(img_key + sub_key);
|
||||||
|
const curr_time = Math.round(Date.now() / 1000);
|
||||||
|
const chr_filter = /[!'()*]/g;
|
||||||
|
// 添加 wts 字段
|
||||||
|
Object.assign(params, { wts: curr_time });
|
||||||
|
// 按照 key 重排参数
|
||||||
|
const query = Object.keys(params)
|
||||||
|
.sort()
|
||||||
|
.map((key) => {
|
||||||
|
// 过滤 value 中的 "!'()*" 字符
|
||||||
|
const value = params[key].toString().replace(chr_filter, "");
|
||||||
|
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||||
|
})
|
||||||
|
.join("&");
|
||||||
|
// 计算 w_rid
|
||||||
|
const wbi_sign = md5(query + mixin_key);
|
||||||
|
return query + "&w_rid=" + wbi_sign;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取最新的 img_key 和 sub_key
|
||||||
|
const getWbiKeys = async (): Promise<EncodedKeys> => {
|
||||||
|
const result = await get({
|
||||||
|
url: "https://api.bilibili.com/x/web-interface/nav",
|
||||||
|
headers: {
|
||||||
|
// SESSDATA 字段
|
||||||
|
Cookie: "SESSDATA=xxxxxx",
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||||
|
Referer: "https://www.bilibili.com/",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const img_url: string = result.data.wbi_img?.img_url ?? "";
|
||||||
|
const sub_url: string = result.data.wbi_img?.sub_url ?? "";
|
||||||
|
return {
|
||||||
|
img_key: img_url.slice(img_url.lastIndexOf("/") + 1, img_url.lastIndexOf(".")),
|
||||||
|
sub_key: sub_url.slice(sub_url.lastIndexOf("/") + 1, sub_url.lastIndexOf(".")),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBiliWbi = async (): Promise<string> => {
|
||||||
|
const cachedData = getCache("bilibili-wbi");
|
||||||
|
console.log(cachedData);
|
||||||
|
if (cachedData && typeof cachedData === "object" && "wbi" in cachedData) {
|
||||||
|
const { wbi } = cachedData as { wbi: string };
|
||||||
|
return wbi;
|
||||||
|
}
|
||||||
|
const web_keys = await getWbiKeys();
|
||||||
|
const params = { foo: "114", bar: "514", baz: 1919810 };
|
||||||
|
const img_key = web_keys.img_key;
|
||||||
|
const sub_key = web_keys.sub_key;
|
||||||
|
const query = encWbi(params, img_key, sub_key);
|
||||||
|
setCache("bilibili-wbi", { wbi: query });
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBiliWbi;
|
||||||
68
src/utils/getToken/weread.ts
Normal file
68
src/utils/getToken/weread.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信读书的书籍 ID
|
||||||
|
* 感谢 @MCBBC 及 ChatGPT
|
||||||
|
*/
|
||||||
|
const getWereadID = (bookId: string) => {
|
||||||
|
try {
|
||||||
|
// 使用 MD5 哈希算法创建哈希对象
|
||||||
|
const hash = crypto.createHash("md5");
|
||||||
|
hash.update(bookId);
|
||||||
|
const str = hash.digest("hex");
|
||||||
|
// 取哈希结果的前三个字符作为初始值
|
||||||
|
let strSub = str.substring(0, 3);
|
||||||
|
// 判断书籍 ID 的类型并进行转换
|
||||||
|
let fa: (string | any[])[];
|
||||||
|
if (/^\d*$/.test(bookId)) {
|
||||||
|
// 如果书籍 ID 只包含数字,则将其拆分成长度为 9 的子字符串,并转换为十六进制表示
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < bookId.length; i += 9) {
|
||||||
|
const chunk = bookId.substring(i, i + 9);
|
||||||
|
chunks.push(parseInt(chunk).toString(16));
|
||||||
|
}
|
||||||
|
fa = ["3", chunks];
|
||||||
|
} else {
|
||||||
|
// 如果书籍 ID 包含其他字符,则将每个字符的 Unicode 编码转换为十六进制表示
|
||||||
|
let hexStr = "";
|
||||||
|
for (let i = 0; i < bookId.length; i++) {
|
||||||
|
hexStr += bookId.charCodeAt(i).toString(16);
|
||||||
|
}
|
||||||
|
fa = ["4", [hexStr]];
|
||||||
|
}
|
||||||
|
// 将类型添加到初始值中
|
||||||
|
strSub += fa[0];
|
||||||
|
// 将数字 2 和哈希结果的后两个字符添加到初始值中
|
||||||
|
strSub += "2" + str.substring(str.length - 2);
|
||||||
|
// 处理转换后的子字符串数组
|
||||||
|
for (let i = 0; i < fa[1].length; i++) {
|
||||||
|
const sub = fa[1][i];
|
||||||
|
const subLength = sub.length.toString(16);
|
||||||
|
// 如果长度只有一位数,则在前面添加 0
|
||||||
|
const subLengthPadded = subLength.length === 1 ? "0" + subLength : subLength;
|
||||||
|
// 将长度和子字符串添加到初始值中
|
||||||
|
strSub += subLengthPadded + sub;
|
||||||
|
// 如果不是最后一个子字符串,则添加分隔符 'g'
|
||||||
|
if (i < fa[1].length - 1) {
|
||||||
|
strSub += "g";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果初始值长度不足 20,从哈希结果中取足够的字符补齐
|
||||||
|
if (strSub.length < 20) {
|
||||||
|
strSub += str.substring(0, 20 - strSub.length);
|
||||||
|
}
|
||||||
|
// 使用 MD5 哈希算法创建新的哈希对象
|
||||||
|
const finalHash = crypto.createHash("md5");
|
||||||
|
finalHash.update(strSub);
|
||||||
|
const finalStr = finalHash.digest("hex");
|
||||||
|
// 取最终哈希结果的前三个字符并添加到初始值的末尾
|
||||||
|
strSub += finalStr.substring(0, 3);
|
||||||
|
return strSub;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("处理微信读书 ID 时出现错误:" + error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getWereadID;
|
||||||
81
src/utils/logger.ts
Normal file
81
src/utils/logger.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { config } from "../config.js";
|
||||||
|
import { createLogger, format, transports } from "winston";
|
||||||
|
import path from "path";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
let pathOption: (typeof transports.File)[] = [];
|
||||||
|
|
||||||
|
// 日志输出目录
|
||||||
|
if (config.USE_LOG_FILE) {
|
||||||
|
try {
|
||||||
|
pathOption = [
|
||||||
|
new transports.File({
|
||||||
|
filename: path.resolve("logs/error.log"),
|
||||||
|
level: "error",
|
||||||
|
maxsize: 1024 * 1024,
|
||||||
|
maxFiles: 1,
|
||||||
|
}),
|
||||||
|
new transports.File({
|
||||||
|
filename: path.resolve("logs/logger.log"),
|
||||||
|
maxsize: 1024 * 1024,
|
||||||
|
maxFiles: 1,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to initialize log files. Logging to a file will be skipped.", error);
|
||||||
|
pathOption = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义不同日志级别的彩色块
|
||||||
|
const levelColors: { [key: string]: string } = {
|
||||||
|
error: chalk.bgRed(" ERROR "),
|
||||||
|
warn: chalk.bgYellow(" WARN "),
|
||||||
|
info: chalk.bgBlue(" INFO "),
|
||||||
|
debug: chalk.bgGreen(" DEBUG "),
|
||||||
|
default: chalk.bgWhite(" LOG "),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义控制台日志输出格式
|
||||||
|
const consoleFormat = format.printf(({ level, message, timestamp, stack }) => {
|
||||||
|
// 获取原始日志级别
|
||||||
|
const originalLevel = Object.keys(levelColors).find((lvl) => level.includes(lvl)) || "default";
|
||||||
|
const colorLevel = levelColors[originalLevel] || levelColors.default;
|
||||||
|
|
||||||
|
let logMessage = `${colorLevel} [${timestamp}] ${message}`;
|
||||||
|
if (stack) {
|
||||||
|
logMessage += `\n${stack}`;
|
||||||
|
}
|
||||||
|
return logMessage;
|
||||||
|
});
|
||||||
|
|
||||||
|
// logger
|
||||||
|
const logger = createLogger({
|
||||||
|
// 最低的日志级别
|
||||||
|
level: "info",
|
||||||
|
// 定义日志的格式
|
||||||
|
format: format.combine(
|
||||||
|
format.timestamp({
|
||||||
|
format: "YYYY-MM-DD HH:mm:ss",
|
||||||
|
}),
|
||||||
|
format.errors({ stack: true }),
|
||||||
|
format.splat(),
|
||||||
|
format.json(),
|
||||||
|
),
|
||||||
|
transports: pathOption,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 控制台输出
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
try {
|
||||||
|
logger.add(
|
||||||
|
new transports.Console({
|
||||||
|
format: format.combine(format.colorize(), consoleFormat),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add console transport. Console logging will be skipped.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default logger;
|
||||||
41
src/utils/parseRSS.ts
Normal file
41
src/utils/parseRSS.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import RSSParser from "rss-parser";
|
||||||
|
import logger from "./logger.js";
|
||||||
|
|
||||||
|
export const extractRss = (content: string): string | null => {
|
||||||
|
// 匹配 <rss> 标签及内容
|
||||||
|
const rssRegex = /(<rss[\s\S]*?<\/rss>)/i;
|
||||||
|
const matches = content.match(rssRegex);
|
||||||
|
return matches ? matches[0] : null;
|
||||||
|
};
|
||||||
|
export const parseRSS = async (rssContent: string) => {
|
||||||
|
const parser = new RSSParser();
|
||||||
|
// 是否为网址
|
||||||
|
const isUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const feed = isUrl(rssContent)
|
||||||
|
? await parser.parseURL(rssContent)
|
||||||
|
: await parser.parseString(rssContent);
|
||||||
|
const items = feed.items.map((item) => ({
|
||||||
|
title: item.title, // 文章标题
|
||||||
|
link: item.link, // 文章链接
|
||||||
|
pubDate: item.pubDate, // 发布日期
|
||||||
|
author: item.creator ?? item.author, // 作者
|
||||||
|
content: item.content, // 内容
|
||||||
|
contentSnippet: item.contentSnippet, // 内容摘要
|
||||||
|
guid: item.guid, // 全局唯一标识符
|
||||||
|
categories: item.categories, // 分类
|
||||||
|
}));
|
||||||
|
// 返回解析数据
|
||||||
|
return items;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("解析 RSS 内容时出错:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
60
src/views/Error.tsx
Normal file
60
src/views/Error.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { FC } from "hono/jsx";
|
||||||
|
import { html } from "hono/html";
|
||||||
|
import Layout from "./Layout.js";
|
||||||
|
|
||||||
|
const Error: FC = (props) => {
|
||||||
|
return (
|
||||||
|
<Layout title="Error | DailyHot API">
|
||||||
|
<main className="error">
|
||||||
|
<div className="img">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 36 36">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M30 13.5a7.49 7.49 0 0 1-6.78-4.3H4V7h18.57a7.52 7.52 0 0 1-.07-1a7.52 7.52 0 0 1 .07-1H4a2 2 0 0 0-2 2v22a2 2 0 0 0 2 2h28a2 2 0 0 0 2-2V12.34a7.46 7.46 0 0 1-4 1.16m-13.2 6.33l-10 4.59v-2.64l6.51-3l-6.51-3v-2.61l10 4.59Zm6.6 5.57H17V23h6.4Z"
|
||||||
|
class="clr-i-solid--badged clr-i-solid-path-1--badged"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="30"
|
||||||
|
cy="6"
|
||||||
|
r="5"
|
||||||
|
fill="currentColor"
|
||||||
|
class="clr-i-solid--badged clr-i-solid-path-2--badged clr-i-badge"
|
||||||
|
/>
|
||||||
|
<path fill="none" d="M0 0h36v36H0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="title">
|
||||||
|
<h1 className="title-text">Looks like something went wrong</h1>
|
||||||
|
<span className="title-tip">程序执行出错</span>
|
||||||
|
{props?.error ? <p className="content">{props.error}</p> : null}
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button id="reload-button">
|
||||||
|
<svg
|
||||||
|
className="btn-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M17.65 6.35a7.95 7.95 0 0 0-6.48-2.31c-3.67.37-6.69 3.35-7.1 7.02C3.52 15.91 7.27 20 12 20a7.98 7.98 0 0 0 7.21-4.56c.32-.67-.16-1.44-.9-1.44c-.37 0-.72.2-.88.53a5.994 5.994 0 0 1-6.8 3.31c-2.22-.49-4.01-2.3-4.48-4.52A6.002 6.002 0 0 1 12 6c1.66 0 3.14.69 4.22 1.78l-1.51 1.51c-.63.63-.19 1.71.7 1.71H19c.55 0 1-.45 1-1V6.41c0-.89-1.08-1.34-1.71-.71z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="btn-text">刷新重试</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{html`
|
||||||
|
<script>
|
||||||
|
document.getElementById("reload-button").addEventListener("click", () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
`}
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Error;
|
||||||
66
src/views/Home.tsx
Normal file
66
src/views/Home.tsx
Normal file
File diff suppressed because one or more lines are too long
238
src/views/Layout.tsx
Normal file
238
src/views/Layout.tsx
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import type { FC } from "hono/jsx";
|
||||||
|
import { css, Style } from "hono/css";
|
||||||
|
|
||||||
|
const Layout: FC = (props) => {
|
||||||
|
const globalClass = css`
|
||||||
|
:-hono-global {
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
user-select: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
:root {
|
||||||
|
--text-color: #000;
|
||||||
|
--text-color-gray: #cbcbcb;
|
||||||
|
--text-color-hover: #fff;
|
||||||
|
--icon-color: #444;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--text-color: #fff;
|
||||||
|
--text-color-gray: #cbcbcb;
|
||||||
|
--text-color-hover: #3c3c3c;
|
||||||
|
--icon-color: #cbcbcb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--text-color-hover);
|
||||||
|
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei";
|
||||||
|
transition:
|
||||||
|
color 0.3s,
|
||||||
|
background-color 0.3s;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.img {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.img img,
|
||||||
|
.img svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.title .title-text {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.title .title-tip {
|
||||||
|
font-size: 20px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.title .content {
|
||||||
|
margin-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px dashed var(--text-color);
|
||||||
|
}
|
||||||
|
.control {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.control button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color);
|
||||||
|
border: var(--text-color) solid;
|
||||||
|
background-color: var(--text-color-hover);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 0 8px;
|
||||||
|
transition:
|
||||||
|
color 0.3s,
|
||||||
|
background-color 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.control button .btn-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.control button .btn-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.control button:hover {
|
||||||
|
border: var(--text-color) solid;
|
||||||
|
background: var(--text-color);
|
||||||
|
color: var(--text-color-hover);
|
||||||
|
}
|
||||||
|
.control button i {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.social {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.social .link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
.social .link::after {
|
||||||
|
content: "";
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
opacity: 0.4;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.social .link:last-child::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.social .link svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
footer .power,
|
||||||
|
footer .icp {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
footer a {
|
||||||
|
color: var(--text-color-gray);
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
footer a:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
return (
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>{props.title}</title>
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="description" content="今日热榜 API,一个聚合热门数据的 API 接口" />
|
||||||
|
<Style>{globalClass}</Style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{props.children}
|
||||||
|
<footer>
|
||||||
|
<div class="social">
|
||||||
|
<a href="https://github.com/imsyy/DailyHotApi" className="link" target="_blank">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.imsyy.top" className="link" target="_blank">
|
||||||
|
<svg
|
||||||
|
className="btn-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="mailto:one@imsyy.top" className="link">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="m20 8l-8 5l-8-5V6l8 5l8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="power">
|
||||||
|
Copyright ©
|
||||||
|
<a href="https://www.imsyy.top/" target="_blank">
|
||||||
|
無名
|
||||||
|
</a>
|
||||||
|
| Power by
|
||||||
|
<a href="https://github.com/honojs/hono/" target="_blank">
|
||||||
|
Hono
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="icp">
|
||||||
|
<a href="https://beian.miit.gov.cn/" target="_blank">
|
||||||
|
豫ICP备2022018134号-1
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
50
src/views/NotFound.tsx
Normal file
50
src/views/NotFound.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { FC } from "hono/jsx";
|
||||||
|
import { html } from "hono/html";
|
||||||
|
import Layout from "./Layout.js";
|
||||||
|
|
||||||
|
const NotFound: FC = () => {
|
||||||
|
return (
|
||||||
|
<Layout title="404 Not Found | DailyHot API">
|
||||||
|
<main className="not-found">
|
||||||
|
<div className="img">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 17q.425 0 .713-.288Q13 16.425 13 16t-.287-.713Q12.425 15 12 15t-.712.287Q11 15.575 11 16t.288.712Q11.575 17 12 17Zm0 5q-2.075 0-3.9-.788q-1.825-.787-3.175-2.137q-1.35-1.35-2.137-3.175Q2 14.075 2 12t.788-3.9q.787-1.825 2.137-3.175q1.35-1.35 3.175-2.138Q9.925 2 12 2t3.9.787q1.825.788 3.175 2.138q1.35 1.35 2.137 3.175Q22 9.925 22 12t-.788 3.9q-.787 1.825-2.137 3.175q-1.35 1.35-3.175 2.137Q14.075 22 12 22Zm0-9q.425 0 .713-.288Q13 12.425 13 12V8q0-.425-.287-.713Q12.425 7 12 7t-.712.287Q11 7.575 11 8v4q0 .425.288.712q.287.288.712.288Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="title">
|
||||||
|
<h1 className="title-text">404 Not Found</h1>
|
||||||
|
<span className="title-tip">请检查您的路径</span>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button id="home-button">
|
||||||
|
<svg
|
||||||
|
className="btn-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="btn-text">回到首页</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{html`
|
||||||
|
<script>
|
||||||
|
document.getElementById("home-button").addEventListener("click", () => {
|
||||||
|
window.location.href = "/";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
`}
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"strict": false,
|
||||||
|
"types": ["node"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx",
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "*.test.*", "./dist/**/*"]
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
const NodeCache = require("node-cache");
|
|
||||||
|
|
||||||
const cache = new NodeCache({
|
|
||||||
stdTTL: 1800, // 缓存默认过期时间(单位秒)
|
|
||||||
checkperiod: 60, // 定期检查过期缓存的时间(单位秒)
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从缓存中获取数据
|
|
||||||
* @param {string} key 缓存键值
|
|
||||||
* @return {Promise<any>} 数据
|
|
||||||
*/
|
|
||||||
const get = async (key) => {
|
|
||||||
return cache.get(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将数据写入缓存
|
|
||||||
* @param {string} key 缓存键值
|
|
||||||
* @param {any} value 数据
|
|
||||||
* @param {number} ttl 有效期,单位秒,默认为300秒
|
|
||||||
* @return {Promise<void>} 无返回值
|
|
||||||
*/
|
|
||||||
const set = async (key, value, ttl = 300) => {
|
|
||||||
return cache.set(key, value, ttl);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从缓存中删除数据
|
|
||||||
* @param {string} key 缓存键值
|
|
||||||
* @return {Promise<void>} 无返回值
|
|
||||||
*/
|
|
||||||
const del = async (key) => {
|
|
||||||
return cache.del(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
del,
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user