107 Commits

Author SHA1 Message Date
底层用户
755d4e22c1 Merge pull request #114 from Zeno2019/master
feat: 添加游资网(gameres)路由,并支持处理“X小时前”的时间格式
2025-08-19 09:08:29 +08:00
Zeno_
72d6d970e1 feat: 添加gameres路由类型定义 2025-08-19 02:03:23 +08:00
Zeno_
150a3d9c1e feat: 支持处理“X小时前”的时间格式并添加gameres路由 2025-08-18 21:19:32 +08:00
imsyy
259dd2c270 🐞 fix: 修复微博热榜 #110 2025-07-31 23:39:34 +08:00
底层用户
dad7e442da 🐞 fix: 修复知乎热榜 #105 2025-07-29 09:45:44 +08:00
底层用户
d5581815f4 Merge pull request #106 from ZHLHZHU/zhihu-auth
feat: 支持指定cookie获取知乎热榜
2025-05-14 09:06:09 +08:00
ZHLH
c6a8c13772 feat: 支持指定cookie获取知乎热榜 2025-05-12 00:56:26 -04:00
底层用户
46cb5ccec2 Merge pull request #104 from ZvonimirSun/master
feat: 为rss补充media:content信息
2025-05-11 13:42:52 +08:00
imsyy
41aeba290a Merge branch 'master' of github.com:imsyy/DailyHotApi 2025-05-11 00:01:43 +08:00
imsyy
20a5b537f9 feat: 水木社区 2025-05-11 00:00:53 +08:00
ZvonimirSun
2ed851bdc2 feat: 为rss补充media:content信息 2025-05-08 16:07:36 +08:00
imsyy
dddcb27487 🐞 fix: 修复知乎字段变更 #98 2025-04-02 14:32:18 +08:00
底层用户
410d88860d Merge pull request #95 from HelTi/master
提交合并
2025-02-10 21:24:07 +08:00
helti
22a14dd64e feat: 微信读书类目增加 2025-02-09 10:58:07 +08:00
helti
26f10d0c6a feat: bilibili接口问题排查优化 2025-02-08 14:17:28 +08:00
longqian
2a6f4db768 Merge branch 'master' into master 2025-02-07 23:22:16 +08:00
helti
94ba2478d1 feat: 掘金类目 2025-02-07 23:00:43 +08:00
helti
ec190229cc feat: GitHub请求优化 2025-02-07 09:57:44 +08:00
helti
6ce0f7d8b0 feat: 调整bilibili参数 2025-02-05 21:49:02 +08:00
底层用户
2c9526b7c9 Merge pull request #93 from RoversX/master
支持了 Github trending, hackernews 和 producthunt
2025-01-16 09:54:44 +08:00
helti
212b58ada9 fix: 导入路径 2025-01-15 20:04:42 +08:00
helti
16b9ad25d3 feat: github 2025-01-15 19:46:30 +08:00
helti
be9db7c625 Merge branch 'master' of github.com:HelTi/DailyHotApi 2025-01-14 18:40:18 +08:00
helti
e2bee69738 feat: 增加GitHub趋势排行 2025-01-14 17:28:20 +08:00
helti
b033e7574c update .gitignore 2025-01-14 17:28:02 +08:00
IMCSER
484b73c083 Support Github trending, hackernews and producthunt
Now support source from Github Trending, hackernews and producthunt
2025-01-13 22:13:51 +08:00
底层用户
0fa3108b92 feat: 新增 LinuxDo
 feat: 新增 LinuxDo
2025-01-09 16:15:46 +08:00
acanyo
c1b21f1964 feat: 新增 LinuxDo 2025-01-09 11:19:27 +08:00
helti
b31d4f59a2 feat: 修改pm3配置 2024-12-14 18:52:59 +08:00
helti
38f3f38403 feat: 增加 pm2部署和sh脚本 2024-12-14 18:11:44 +08:00
imsyy
c9ee406372 feat: 新增 极客公园 2024-12-09 18:25:20 +08:00
imsyy
bd507f9938 feat: 新增 数字尾巴 & 什么值得买 & 游研社 2024-12-09 18:13:20 +08:00
imsyy
a05578f0f4 🎈 perf: 优化 Redis 策略 2024-12-07 10:11:15 +08:00
imsyy
489723985b 🐞 fix: Fixed type errors 2024-12-07 09:45:18 +08:00
imsyy
51f27af0d6 feat: 支持 Redis 2024-12-06 18:15:44 +08:00
imsyy
161b6b612b feat: 新增 吾爱破解、果壳、快手 2024-12-05 15:28:28 +08:00
imsyy
f38d264366 🦄 refactor: 调整接口结构 2024-12-04 17:47:48 +08:00
imsyy
0cce0d6067 🐞 fix: 修复启动异常 2024-12-04 09:10:52 +08:00
imsyy
098b80865b 🐞 fix: 部分类型错误
- 新增 米游社 综合接口
2024-12-03 18:09:32 +08:00
imsyy
afb7c7d515 📃 docs: 更新说明 2024-11-27 17:25:52 +08:00
imsyy
7b610ef8cd 🐳 chore: 替换配置文件 2024-11-05 16:07:09 +08:00
imsyy
d27ef0d116 feat: 支持配置泛域名 2024-11-05 15:58:22 +08:00
imsyy
d0dfba27dc feat: 支持泛域名配置 2024-11-05 15:56:11 +08:00
imsyy
293c4d623e 🔧 build: 更新依赖 2024-11-05 15:15:34 +08:00
imsyy
097a1b3628 feat: 新增 酷安热榜 2024-11-05 15:12:07 +08:00
imsyy
ec50d5ced1 🐞 fix: 修复 知乎日报 异常 #80 2024-10-15 17:24:53 +08:00
imsyy
c713d34ba1 🐞 fix: 修复 start 脚本无法正常启动 2024-09-23 16:04:54 +08:00
imsyy
c4772962f4 🐞 fix: 修复构建时显示报错 #77 2024-09-20 10:29:42 +08:00
imsyy
36802252c0 Merge branch 'master' of github.com:imsyy/DailyHotApi 2024-07-30 11:50:31 +08:00
imsyy
7f5449089d 🔧 build: 更新版本号 2024-07-30 11:49:59 +08:00
底层用户
53c68a3d69 Merge pull request #72 from Kataick/fix-weibo-hot-field
fix(weibo): 修复微博热度字段
2024-07-26 18:30:24 +08:00
Kataick
447e0c6153 fix(weibo): 修复微博热度字段 2024-07-25 21:24:39 +08:00
imsyy
55f4e22693 feat: 新增 NodeSeek by @JianBing77 2024-07-22 18:07:03 +08:00
底层用户
c791e29a59 Merge pull request #71 from JianBing77/master
feat: 新增NodeSeek接口
2024-07-22 17:49:19 +08:00
JianBing77
91775a0763 feat: 新增NodeSeek接口 2024-07-19 11:41:15 +08:00
imsyy
a742f9df78 🐞 fix: 修复米游社系列接口出错 2024-07-09 09:35:27 +08:00
底层用户
f213d9810b Merge pull request #68 from Kataick/fix-zhihu-hot
fix(zhihu): fix hot error
2024-07-08 15:10:29 +08:00
底层用户
250a90cf2e Merge pull request #67 from Kataick/flx-douyin-url
fix(douyin): remove encodeURIComponent from url
2024-07-08 15:10:18 +08:00
Kataick
a4a1055604 fix(zhihu): hot error 2024-07-07 12:48:11 +08:00
Kataick
fd3d911b04 fix(douyin): remove encodeURIComponent from url 2024-07-06 13:24:54 +08:00
imsyy
bf80a2e22c feat: 新增 虎扑 & 新浪网 & 新浪新闻
- 修复历史上的今天 #63
2024-06-25 13:47:40 +08:00
imsyy
6988df58f1 🐞 fix: 修复历史上的今天 2024-06-24 16:02:06 +08:00
imsyy
bcff976a4d feat: 新增 CSDN 2024-06-17 14:06:22 +08:00
imsyy
62a8880ae4 feat: 输出美化 2024-06-13 17:51:44 +08:00
imsyy
e6f89c4868 🔧 build: 依赖更新 2024-06-13 16:53:59 +08:00
imsyy
fa80a29772 feat: 新增 历史上的今天 2024-06-13 14:09:16 +08:00
imsyy
b8f7c1ad23 🐞 fix: 修复错误包含 ts 文件 2024-06-13 10:12:06 +08:00
imsyy
d5217c3dff 🔧 build: 修正类型错误 2024-06-12 17:37:37 +08:00
imsyy
e1beb5b534 🐞 fix: 修复 hellogithub #60 2024-06-11 09:19:41 +08:00
imsyy
14bc5a1dce feat: 支持 Vercel 部署 2024-06-07 16:03:22 +08:00
imsyy
11addd20ca 🐳 chore: 修复工作流 2024-06-07 09:07:09 +08:00
imsyy
23375519ba 🐳 chore: 修复工作流 2024-06-06 17:50:06 +08:00
imsyy
2ba46c5e0e 🐳 chore: 修复工作流 2024-06-06 17:31:20 +08:00
imsyy
d4a52a6b24 🐞 fix: 修复 Docker 运行出错 2024-06-06 16:27:01 +08:00
imsyy
d73ca1170c feat: 新增 hostloc #43 2024-06-06 11:36:28 +08:00
imsyy
93d90eb653 🐳 chore: Publish npm 2024-06-06 09:39:09 +08:00
imsyy
a3bb42d26c feat: 新增 吾爱破解 2024-06-05 18:16:43 +08:00
imsyy
093312ea8c 🔧 build: fix document 2024-06-04 10:44:19 +08:00
imsyy
13f019a5aa 🐞 fix: 修复哔哩哔哩偶发性失败 #48 #54 2024-05-29 10:11:06 +08:00
imsyy
ce5d381093 Merge branch 'master' of github.com:imsyy/DailyHotApi 2024-05-16 18:11:05 +08:00
imsyy
679fdab87e feat: 新增 虎嗅 & 爱范儿 2024-05-16 18:10:34 +08:00
底层用户
8c8a86957b Merge pull request #49 from zhouzongyan/master
更新知乎日报的链接
2024-05-06 09:46:59 +08:00
allen.zhou
8e077640b1 更新知乎日报 2024-04-30 15:58:44 +08:00
allen.zhou
5555cc6f00 优化知乎拉取 2024-04-30 15:47:14 +08:00
imsyy
cdf2479044 feat: 新增一些接口 2024-04-16 15:06:02 +08:00
imsyy
067190b5a2 feat: 新增:IT之家「喜加一」 2024-04-15 18:26:52 +08:00
imsyy
abadb692e4 feat: 实现哔哩哔哩 wbi 签名鉴权 #48 2024-04-15 17:19:49 +08:00
imsyy
f62b4ff4dd 🐞 fix: 修复哔哩哔哩无法获取 2024-04-10 18:00:14 +08:00
imsyy
34ab73a3f1 🦄 refactor: Refactoring using hono 2024-04-08 16:35:58 +08:00
imsyy
7459858767 Merge branch 'master' of github.com:imsyy/DailyHotApi 2024-03-25 16:30:24 +08:00
imsyy
e38263ad40 📃 docs: 更新说明 2024-03-25 16:30:01 +08:00
底层用户
49854b33d6 Merge pull request #44 from jymusic0663/patch-1
修复首页nga接口不返回标题
2024-03-18 10:23:14 +08:00
榕城小林囝
2a018c0640 修复首页nga接口不返回标题 2024-03-08 13:39:37 +08:00
imsyy
b7260cf569 🐳 chore: 支持自动部署 2024-01-17 15:07:51 +08:00
imsyy
3343f2b5db 📃 docs: 更新文档 2024-01-02 11:22:38 +08:00
imsyy
2b91f3f32b feat: 新增部分接口 2024-01-02 11:20:50 +08:00
底层用户
fa8fb5a47f Merge pull request #38 from x-dr/master
feat:添加一些接口
2024-01-02 09:37:19 +08:00
x-dr
c0cbb3591b feat: 新增NGA论坛热帖 2023-12-27 19:36:38 +08:00
x-dr
4c54be315f feat: 新增网易云音乐飙升榜 2023-12-27 19:36:00 +08:00
x-dr
71a3621fd8 feat: 新增QQ音乐热歌榜 2023-12-27 19:35:25 +08:00
x-dr
e6a02c667f feat: 新增V2EX热帖 2023-12-27 19:34:50 +08:00
x-dr
07c0f6ed9b feat: 豆瓣小组精选榜单 2023-12-27 19:33:39 +08:00
imsyy
b8c16ad88a feat: 新增 Github Trending 榜单 2023-12-27 10:05:15 +08:00
底层用户
5df634058c Merge pull request #37 from x-dr/master
feat: add GitHub Trending Api
2023-12-27 09:59:26 +08:00
imsyy
029fed603b 🐞 fix: 修复英雄联盟更新公告 2023-12-26 15:58:29 +08:00
x-dr
69fb0640be feat: add GitHub Trending Api 2023-12-24 22:44:42 +08:00
imsyy
a4394588c1 feat: 优化 Docker 大小 2023-12-06 12:25:09 +08:00
132 changed files with 7762 additions and 5596 deletions

View File

@@ -1,15 +1,37 @@
node_modules # folders
npm-debug.log .devcontainer
Dockerfile*
docker-compose*
.dockerignore
.git
.github .github
.gitignore .husky
README.md .idea
LICENSE
.vscode .vscode
dist Dockerfile*
build LICENSE
images Procfile
script 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

5
.env
View File

@@ -1,5 +0,0 @@
# 服务端口
PORT=6688
# 允许的域名
ALLOWED_DOMAIN = '*'

29
.env.example Normal file
View File

@@ -0,0 +1,29 @@
# 服务端口
PORT=6688
# 允许的域名
ALLOWED_DOMAIN="*"
# 允许的主域名,填写格式为 imsyy.top
## 若填写该项,将忽略 ALLOWED_DOMAIN
ALLOWED_HOST=""
# ROBOT
DISALLOW_ROBOT=true
# Redis
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_PASSWORD=""
# 缓存时长( 秒
CACHE_TTL=3600
# 请求超时( 毫秒
REQUEST_TIMEOUT=6000
# 是否输出日志
USE_LOG_FILE=true
# RSS Mode
RSS_MODE=false

View File

@@ -1,19 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:vue/vue3-essential"],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["vue"],
"rules": {},
"globals": {
"require": true,
"module": true,
"process": true,
"__dirname": true
}
}

56
.github/workflows/docker.yml vendored Normal file
View 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 regular image
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
View 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 }}

11
.gitignore vendored
View File

@@ -10,12 +10,8 @@ lerna-debug.log*
node_modules node_modules
.DS_Store .DS_Store
dist dist
dist-ssr
coverage
*.local *.local
.env
/cypress/videos/
/cypress/screenshots/
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
@@ -27,4 +23,7 @@ coverage
*.sln *.sln
*.sw? *.sw?
test.md package-lock.json
# Sentry Config File
.env.sentry-build-plugin
.nvmrc

13
.hintrc
View File

@@ -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
View File

@@ -0,0 +1,2 @@
node_modules
dist/logs/

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
dist
node_modules
pnpm-lock.yaml
LICENSE.md
# tsconfig.json
# tsconfig.*.json

13
.prettierrc.js Normal file
View File

@@ -0,0 +1,13 @@
export default {
// see https://prettier.io/docs/en/options.html
// 优先使用单引号
singleQuote: false,
// 尾随逗号
trailingComma: "all",
// 缩进空格数
tabWidth: 2,
// 使用分号
semi: true,
// 换行符
printWidth: 100,
};

View File

@@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"singleQuote": false,
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"printWidth": 100
}

84
CODE_OF_CONDUCT.md Normal file
View 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>.

View File

@@ -1,13 +1,51 @@
FROM node:16 FROM node:20-alpine AS base
ENV NODE_ENV=docker
# 清理缓存
RUN rm -rf /var/cache/apk/*
# 构建阶段
FROM base AS builder
RUN npm install -g pnpm
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*json tsconfig.json pnpm-lock.yaml .env.example ./
COPY src ./src
COPY public ./public
RUN npm install # 复制环境变量
RUN [ ! -e ".env" ] && cp .env.example .env || true
COPY . . # 安装依赖
RUN pnpm install
RUN pnpm build
RUN pnpm prune --production
# 运行阶段
FROM base AS runner
# 创建用户和组
RUN addgroup --system --gid 114514 nodejs
RUN adduser --system --uid 114514 hono
# 创建日志目录
RUN mkdir -p /app/logs && chown -R hono:nodejs /app/logs
RUN ln -s /app/logs /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 EXPOSE 6688
CMD ["npm", "start"] # 运行
CMD ["node", "/app/dist/index.js"]

View File

@@ -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

324
README.md
View File

@@ -2,226 +2,212 @@
<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>
## 示例 ## 🚩 特性
- 极快响应,便于开发
- 支持 RSS 模式和 JSON 模式
- 支持多种部署方式
- 简明的路由目录,便于新增
## 👀 示例
> 这里是使用该 API 的示例站点 > 这里是使用该 API 的示例站点
> 示例站点可能由于访问量或者长久未维护而访问异常
> 若您也使用了本 API 搭建了网站,欢迎提交您的站点链接
- [今日热榜 - https://hot.imsyy.top/](https://hot.imsyy.top/) - [今日热榜 - https://hot.imsyy.top/](https://hot.imsyy.top/)
## 总览 ## 📊 接口总览
> 🟢 状态正常 <details>
> 🟠 可能失效 <summary>查看全部接口</summary>
> ❌ 无法使用
> 示例站点运行于海外服务器,部分国内站点可能存在访问异常,请以实际情况为准
| **站点** | **类别** | **调用名称** | **状态** | | **站点** | **类别** | **调用名称** | **状态** |
| ------------ | -------- | ------------------- | -------- | | ---------------- | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 哔哩哔哩 | 热门榜 | bilibili | 🟢 | | 哔哩哔哩 | 热门榜 | bilibili | ![https://api-hot.imsyy.top/bilibili](https://img.shields.io/website.svg?label=bilibili&url=https://api-hot.imsyy.top/bilibili&cacheSeconds=7200) |
| 微博 | 热搜榜 | weibo | 🟢 | | AcFun | 排行榜 | acfun | ![https://api-hot.imsyy.top/acfun](https://img.shields.io/website.svg?label=acfun&url=https://api-hot.imsyy.top/acfun&cacheSeconds=7200) |
| 知乎 | 热 | zhihu | 🟢 | | 微博 | 热搜榜 | weibo | ![https://api-hot.imsyy.top/weibo](https://img.shields.io/website.svg?label=weibo&url=https://api-hot.imsyy.top/weibo&cacheSeconds=7200) |
| 百度 | 热搜榜 | baidu | 🟢 | | 知乎 | 热榜 | zhihu | ![https://api-hot.imsyy.top/zhihu](https://img.shields.io/website.svg?label=zhihu&url=https://api-hot.imsyy.top/zhihu&cacheSeconds=7200) |
| 抖音 | 热点榜 | douyin / douyin_new | 🟢 | | 知乎日报 | 推荐榜 | zhihu-daily | ![https://api-hot.imsyy.top/zhihu-daily](https://img.shields.io/website.svg?label=zhihu-daily&url=https://api-hot.imsyy.top/zhihu-daily&cacheSeconds=7200) |
| 抖音 | 热歌榜 | douyin_music | 🟢 | | 百度 | 热搜榜 | baidu | ![https://api-hot.imsyy.top/baidu](https://img.shields.io/website.svg?label=baidu&url=https://api-hot.imsyy.top/baidu&cacheSeconds=7200) |
| 豆瓣 | 新片榜 | douban_new | 🟢 | | 抖音 | 热点 | douyin | ![https://api-hot.imsyy.top/douyin](https://img.shields.io/website.svg?label=douyin&url=https://api-hot.imsyy.top/douyin&cacheSeconds=7200) |
| 百度贴吧 | 热议榜 | tieba | 🟢 | | 快手 | 热点榜 | kuaishou | ![https://api-hot.imsyy.top/kuaishou](https://img.shields.io/website.svg?label=kuaishou&url=https://api-hot.imsyy.top/kuaishou&cacheSeconds=7200) |
| 少数派 | 热榜 | sspai | 🟢 | | 豆瓣电影 | 新片榜 | douban-movie | ![https://api-hot.imsyy.top/douban-movie](https://img.shields.io/website.svg?label=douban-movie&url=https://api-hot.imsyy.top/douban-movie&cacheSeconds=7200) |
| IT 之家 | 热榜 | ithome | 🟠 | | 豆瓣讨论小组 | 讨论精选 | douban-group | ![https://api-hot.imsyy.top/douban-group](https://img.shields.io/website.svg?label=douban-group&url=https://api-hot.imsyy.top/douban-group&cacheSeconds=7200) |
| 澎湃新闻 | 热 | thepaper | 🟢 | | 百度贴吧 | 热议榜 | tieba | ![https://api-hot.imsyy.top/tieba](https://img.shields.io/website.svg?label=tieba&url=https://api-hot.imsyy.top/tieba&cacheSeconds=7200) |
| 今日头条 | 热榜 | toutiao | 🟢 | | 少数派 | 热榜 | sspai | ![https://api-hot.imsyy.top/sspai](https://img.shields.io/website.svg?label=sspai&url=https://api-hot.imsyy.top/sspai&cacheSeconds=7200) |
| 36 氪 | 热榜 | 36kr | 🟢 | | IT之家 | 热榜 | ithome | ![https://api-hot.imsyy.top/ithome](https://img.shields.io/website.svg?label=ithome&url=https://api-hot.imsyy.top/ithome&cacheSeconds=7200) |
| 稀土掘金 | 热榜 | juejin | 🟢 | | IT之家「喜加一」 | 最新动态 | ithome-xijiayi | ![https://api-hot.imsyy.top/ithome-xijiayi](https://img.shields.io/website.svg?label=ithome-xijiayi&url=https://api-hot.imsyy.top/ithome-xijiayi&cacheSeconds=7200) |
| 腾讯新闻 | 热点榜 | newsqq | 🟢 | | 简书 | 热门推荐 | jianshu | ![https://api-hot.imsyy.top/jianshu](https://img.shields.io/website.svg?label=jianshu&url=https://api-hot.imsyy.top/jianshu&cacheSeconds=7200) |
| 网易新闻 | 热点榜 | netease | 🟢 | | 果壳 | 热门文章 | guokr | ![https://api-hot.imsyy.top/guokr](https://img.shields.io/website.svg?label=guokr&url=https://api-hot.imsyy.top/guokr&cacheSeconds=7200) |
| 英雄联盟 | 更新公告 | lol | 🟢 | | 澎湃新闻 | 热榜 | thepaper | ![https://api-hot.imsyy.top/thepaper](https://img.shields.io/website.svg?label=thepaper&url=https://api-hot.imsyy.top/thepaper&cacheSeconds=7200) |
| 原神 | 最新消息 | genshin | 🟢 | | 今日头条 | 热榜 | toutiao | ![https://api-hot.imsyy.top/toutiao](https://img.shields.io/website.svg?label=toutiao&url=https://api-hot.imsyy.top/toutiao&cacheSeconds=7200) |
| 微信读书 | 飙升榜 | weread | 🟢 | | 36 氪 | 热榜 | 36kr | ![https://api-hot.imsyy.top/36kr](https://img.shields.io/website.svg?label=36kr&url=https://api-hot.imsyy.top/36kr&cacheSeconds=7200) |
| 快手 | 热榜 | kuaishou | 🟢 | | 51CTO | 推荐榜 | 51cto | ![https://api-hot.imsyy.top/51cto](https://img.shields.io/website.svg?label=51cto&url=https://api-hot.imsyy.top/51cto&cacheSeconds=7200) |
| 历史上的今天 | 指定日期 | calendar | 🟢 | | CSDN | 排行榜 | csdn | ![https://api-hot.imsyy.top/csdn](https://img.shields.io/website.svg?label=csdn&url=https://api-hot.imsyy.top/csdn&cacheSeconds=7200) |
| NodeSeek | 最新动态 | nodeseek | ![https://api-hot.imsyy.top/nodeseek](https://img.shields.io/website.svg?label=nodeseek&url=https://api-hot.imsyy.top/nodeseek&cacheSeconds=7200) |
| 稀土掘金 | 热榜 | juejin | ![https://api-hot.imsyy.top/juejin](https://img.shields.io/website.svg?label=juejin&url=https://api-hot.imsyy.top/juejin&cacheSeconds=7200) |
| 腾讯新闻 | 热点榜 | qq-news | ![https://api-hot.imsyy.top/qq-news](https://img.shields.io/website.svg?label=qq-news&url=https://api-hot.imsyy.top/qq-news&cacheSeconds=7200) |
| 新浪网 | 热榜 | sina | ![https://api-hot.imsyy.top/sina](https://img.shields.io/website.svg?label=sina&url=https://api-hot.imsyy.top/sina&cacheSeconds=7200) |
| 新浪新闻 | 热点榜 | sina-news | ![https://api-hot.imsyy.top/sina-news](https://img.shields.io/website.svg?label=sina-news&url=https://api-hot.imsyy.top/sina-news&cacheSeconds=7200) |
| 网易新闻 | 热点榜 | netease-news | ![https://api-hot.imsyy.top/netease-news](https://img.shields.io/website.svg?label=netease-news&url=https://api-hot.imsyy.top/netease-news&cacheSeconds=7200) |
| 吾爱破解 | 榜单 | 52pojie | ![https://api-hot.imsyy.top/52pojie](https://img.shields.io/website.svg?label=52pojie&url=https://api-hot.imsyy.top/52pojie&cacheSeconds=7200) |
| 全球主机交流 | 榜单 | hostloc | ![https://api-hot.imsyy.top/hostloc](https://img.shields.io/website.svg?label=hostloc&url=https://api-hot.imsyy.top/hostloc&cacheSeconds=7200) |
| 虎嗅 | 24小时 | huxiu | ![https://api-hot.imsyy.top/huxiu](https://img.shields.io/website.svg?label=huxiu&url=https://api-hot.imsyy.top/huxiu&cacheSeconds=7200) |
| 酷安 | 热榜 | coolapk | ![https://api-hot.imsyy.top/coolapk](https://img.shields.io/website.svg?label=coolapk&url=https://api-hot.imsyy.top/coolapk&cacheSeconds=7200) |
| 虎扑 | 步行街热帖 | hupu | ![https://api-hot.imsyy.top/hupu](https://img.shields.io/website.svg?label=hupu&url=https://api-hot.imsyy.top/hupu&cacheSeconds=7200) |
| 爱范儿 | 快讯 | ifanr | ![https://api-hot.imsyy.top/ifanr](https://img.shields.io/website.svg?label=ifanr&url=https://api-hot.imsyy.top/ifanr&cacheSeconds=7200) |
| 英雄联盟 | 更新公告 | lol | ![https://api-hot.imsyy.top/lol](https://img.shields.io/website.svg?label=lol&url=https://api-hot.imsyy.top/lol&cacheSeconds=7200) |
| 米游社 | 最新消息 | miyoushe | ![https://api-hot.imsyy.top/miyoushe](https://img.shields.io/website.svg?label=miyoushe&url=https://api-hot.imsyy.top/miyoushe&cacheSeconds=7200) |
| 原神 | 最新消息 | genshin | ![https://api-hot.imsyy.top/genshin](https://img.shields.io/website.svg?label=genshin&url=https://api-hot.imsyy.top/genshin&cacheSeconds=7200) |
| 崩坏3 | 最新动态 | honkai | ![https://api-hot.imsyy.top/honkai](https://img.shields.io/website.svg?label=honkai&url=https://api-hot.imsyy.top/honkai&cacheSeconds=7200) |
| 崩坏:星穹铁道 | 最新动态 | starrail | ![https://api-hot.imsyy.top/starrail](https://img.shields.io/website.svg?label=starrail&url=https://api-hot.imsyy.top/starrail&cacheSeconds=7200) |
| 微信读书 | 飙升榜 | weread | ![https://api-hot.imsyy.top/weread](https://img.shields.io/website.svg?label=weread&url=https://api-hot.imsyy.top/weread&cacheSeconds=7200) |
| NGA | 热帖 | ngabbs | ![https://api-hot.imsyy.top/ngabbs](https://img.shields.io/website.svg?label=ngabbs&url=https://api-hot.imsyy.top/ngabbs&cacheSeconds=7200) |
| V2EX | 主题榜 | v2ex | ![https://api-hot.imsyy.top/v2ex](https://img.shields.io/website.svg?label=v2ex&url=https://api-hot.imsyy.top/v2ex&cacheSeconds=7200) |
| HelloGitHub | Trending | hellogithub | ![https://api-hot.imsyy.top/hellogithub](https://img.shields.io/website.svg?label=hellogithub&url=https://api-hot.imsyy.top/hellogithub&cacheSeconds=7200) |
| 中央气象台 | 全国气象预警 | weatheralarm | ![https://api-hot.imsyy.top/weatheralarm](https://img.shields.io/website.svg?label=weatheralarm&url=https://api-hot.imsyy.top/weatheralarm&cacheSeconds=7200) |
| 中国地震台 | 地震速报 | earthquake | ![https://api-hot.imsyy.top/earthquake](https://img.shields.io/website.svg?label=earthquake&url=https://api-hot.imsyy.top/earthquake&cacheSeconds=7200) |
| 历史上的今天 | 月-日 | history | ![https://api-hot.imsyy.top/history](https://img.shields.io/website.svg?label=history&url=https://api-hot.imsyy.top/history&cacheSeconds=7200) |
### 特殊接口说明 </details>
#### 获取全部接口信息 ## ⚙️ 使用
获取除了下方特殊接口外的全部接口列表 本项目支持 `Node.js` 调用,可在安装完成后调用 `serveHotApi` 来开启服务器
```http > 该方式无法使用部分需要 Puppeteer 环境的接口
GET https://example.com/all
```
#### 历史上的今天(指定日期)
将指定的月份和日期传入即可得到当天数据,请注意格式
```http
GET https://example.com/calendar/date?month=06&day=01
```
## 部署
```bash ```bash
// 安装依赖 pnpm add dailyhot-api
pnpm install
// 运行
pnpm start
``` ```
## Docker 部署 ```js
import serveHotApi from "dailyhot-api";
/**
* 启动服务器
* @param {Number} [port] - 端口号
* @returns {Promise<void>}
*/
serveHotApi(3000);
```
## ⚙️ 部署
具体使用说明可参考 [我的博客](https://blog.imsyy.top/posts/2024/0408),下方仅讲解基础操作:
### Docker 部署
> 安装及配置 Docker 将不在此处说明,请自行解决 > 安装及配置 Docker 将不在此处说明,请自行解决
### 本地构建 #### 本地构建
```bash ```bash
// 构建 # 构建
docker build -t dailyhot-api . docker build -t dailyhot-api .
// 运行
docker run -p 6688:6688 -d dailyhot-api # 运行
docker run --restart always -p 6688:6688 -d dailyhot-api
# 或使用 Docker Compose
docker-compose up -d
``` ```
### 在线部署 #### 在线部署
```bash ```bash
// 拉取 # 拉取
docker pull imsyy/dailyhot-api:1.0.4 docker pull imsyy/dailyhot-api:latest
// 运行
docker run -p 6688:6688 -d imsyy/dailyhot-api:1.0.4 # 运行
docker run --restart always -p 6688:6688 -d imsyy/dailyhot-api:latest
``` ```
## Vercel 部署 ### 手动部署
现已支持 Vercel 部署,无需服务器 最直接的方式,您可以按照以下步骤将 `DailyHotApi` 部署在您的电脑、服务器或者其他任何地方
### 操作方法 #### 安装
1. fork 本项目 ```bash
2.`Vercel` 官网点击 `New Project` git clone https://github.com/imsyy/DailyHotApi.git
3. 点击 `Import Git Repository` 并选择你 fork 的此项目并点击 `import` cd DailyHotApi
4. `PROJECT NAME`自己填,`FRAMEWORK PRESET``Other` 然后直接点 `Deploy` 接着等部署完成即可
## 调用
### 获取榜单数据
> 获取数据只需在域名后面加上上方列表中的调用名称即可
```http
GET https://api-hot.imsyy.top/bilibili/
``` ```
<details> 然后再执行安装依赖
<summary>调用示例</summary>
```json ```bash
{ npm install
"code": 200,
"message": "获取成功",
"title": "哔哩哔哩", // 榜单名称
"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> 复制 `/.env.example` 文件并重命名为 `/.env` 并修改配置
### 获取榜单最新数据 #### 开发
> 获取最新数据只需在原链接后面加上 `/new`,这样就会直接从服务端拉取最新数据,不会从本地缓存中读取 ```bash
npm run dev
```http
GET https://api-hot.imsyy.top/bilibili/new
``` ```
<details> 成功启动后程序会在控制台输出可访问的地址
<summary>调用示例</summary>
```json #### 编译运行
{
"code": 200, ```bash
"message": "获取成功", npm run build
"title": "哔哩哔哩", // 榜单名称 npm run start
"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> ### pm2 部署
## 其他 ```bash
npm i pm2 -g
sh ./deploy.sh
```
- 本项目为了避免频繁请求官方数据,默认对数据做了缓存处理,默认为 `30` 分钟,如需更改,请自行前往 `utils\cacheData.js` 文件修改 成功启动后程序会在控制台输出可访问的地址
### Vercel 部署
本项目支持通过 `Vercel` 进行一键部署,点击下方按钮或前往 [项目仓库](https://github.com/imsyy/DailyHotApi-Vercel) 进行手动部署
[![Deploy with Vercel](https://vercel.com/button)](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
[![Star History Chart](https://api.star-history.com/svg?repos=imsyy/DailyHotApi&type=Date)](https://star-history.com/#imsyy/DailyHotApi&Date)

35
deploy.sh Normal file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
# 日志文件
LOG_FILE="deploy.log"
# 输出时间戳的日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_FILE
}
# 错误处理函数
handle_error() {
log "错误: $1"
exit 1
}
# 开始拉去代码
log "开始拉取代码..."
git pull
# 开始部署
log "开始部署..."
# 安装依赖
log "正在安装依赖..."
npm install || handle_error "npm install 失败"
# 构建项目
log "正在构建项目..."
npm run build || handle_error "构建失败"
# 使用 pm2 重启或启动项目
log "正在启动/重启服务..."
pm2 restart daily-news || pm2 start ecosystem.config.cjs || handle_error "PM2 启动失败"
log "部署完成!"

15
docker-compose.yml Normal file
View 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

15
ecosystem.config.cjs Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
apps: [{
name: 'daily-news',
script: 'npm',
args: 'start',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'development',
PORT: 6688
}
}]
}

14
eslint.config.js Normal file
View File

@@ -0,0 +1,14 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
ignores: ["**/node_modules", "**/dist", "**/.gitignore", "**/logs", "**/docker-compose.yml"],
},
{ files: ["**/*.{js,mjs,cjs,ts}"] },
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
];

View File

@@ -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.info(`成功在 ${port} 端口上运行`);
});
};
// 检测端口是否被占用
const checkPort = (port) => {
return new Promise((resolve, reject) => {
const server = net
.createServer()
.once("error", (err) => {
if (err.code === "EADDRINUSE") {
console.info(`端口 ${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);

View File

@@ -1,32 +1,81 @@
{ {
"name": "dailyhot_api", "name": "dailyhot-api",
"version": "1.0.4", "version": "2.0.8",
"description": "一个今日热榜", "description": "An Api on Today's Hot list",
"main": "index.js", "keywords": [
"API",
"RSS"
],
"homepage": "https://github.com/imsyy/DailyHotApi#readme",
"bugs": {
"url": "https://github.com/imsyy/DailyHotApi/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/imsyy/DailyHotApi.git"
},
"license": "MIT",
"author": "imsyy",
"main": "dist/index.js",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"files": [
"LICENSE",
"README.md",
"dist/**/*",
"!dist/logs/**/*"
],
"scripts": { "scripts": {
"format": "prettier --write .", "format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "lint": "eslint .",
"start": "node index.js", "dev": "cross-env NODE_ENV=development tsx watch --no-cache src/index.ts",
"dev": "npx nodemon index.js", "dev:cache": "cross-env NODE_ENV=development tsx watch src/index.ts",
"prd": "pm2 start index.js", "build": "tsc --project tsconfig.json",
"build": "node index.js" "start": "cross-env NODE_ENV=development node dist/index.js"
}, },
"author": "imsyy", "type": "module",
"license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.3.4", "@hono/node-server": "^1.17.1",
"cheerio": "1.0.0-rc.12", "axios": "^1.11.0",
"dotenv": "^16.0.3", "chalk": "^5.4.1",
"eslint": "^8.48.0", "cheerio": "^1.1.2",
"eslint-plugin-vue": "^9.17.0", "dayjs": "^1.11.13",
"koa": "^2.14.1", "dotenv": "^17.2.1",
"koa-bodyparser": "^4.3.0", "feed": "^5.1.0",
"koa-router": "^12.0.0", "flatted": "^3.3.3",
"koa-static": "^5.0.0", "hono": "^4.8.9",
"koa-views": "^8.0.0", "iconv-lite": "^0.6.3",
"koa2-cors": "^2.0.6", "ioredis": "^5.6.1",
"md5": "^2.3.0",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"nodemon": "^2.0.22", "node-fetch": "^3.3.2",
"prettier": "^3.0.2" "rss-parser": "^3.13.0",
"user-agents": "^1.1.614",
"winston": "^3.17.0"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@types/md5": "^2.3.5",
"@types/node": "^22.16.5",
"@types/user-agents": "^1.0.4",
"@types/xml2js": "^0.4.14",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"cross-env": "^7.0.3",
"eslint": "^9.32.0",
"globals": "^16.3.0",
"prettier": "^3.6.2",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0"
},
"engines": {
"node": ">=20"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
} }
} }

3564
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,189 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>404 | DailyHot 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>

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/ico/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -1,195 +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>

View File

@@ -1,138 +0,0 @@
const Router = require("koa-router");
const krRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "36kr",
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;

View File

@@ -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 = { name: "baidu", 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;

View File

@@ -1,125 +0,0 @@
const Router = require("koa-router");
const bilibiliRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "bilibili",
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;

View File

@@ -1,125 +0,0 @@
const Router = require("koa-router");
const calendarRouter = new Router();
const axios = require("axios");
const { get, set } = 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;

View File

@@ -1,159 +0,0 @@
/*
* @author: MyFaith
* @date: 2023-09-06
* @customEditors: imsyy
* @lastEditTime: 2023-09-06
*/
const Router = require("koa-router");
const doubanNewRouter = new Router();
const axios = require("axios");
const cheerio = require("cheerio");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "douban",
title: "豆瓣",
subtitle: "新片榜",
};
// 缓存键名
const cacheKey = "doubanNewData";
// 调用时间
let updateTime = new Date().toISOString();
// 调用路径
const url = "https://movie.douban.com/chart/";
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 replaceTitle = (title, score) => {
return `[★${score}] ` + title.replace(/\n/g, "").replace(/ /g, "").replace(/\//g, " / ").trim();
};
// 数据处理
const getData = (data) => {
if (!data) return false;
const dataList = [];
const $ = cheerio.load(data);
try {
$(".article .item").map((idx, item) => {
const id = $(item).find("a").attr("href").split("/").at(-2) ?? "";
const score = $(item).find(".rating_nums").text() ?? "";
dataList.push({
title: replaceTitle($(item).find("a").text(), score),
desc: $(item).find("p").text(),
score,
comments: $(item).find("span.pl").text().match(/\d+/)?.[0] ?? "",
pic: $(item).find("img").attr("src") ?? "",
url: $(item).find("a").attr("href") ?? "",
mobileUrl: `https://m.douban.com/movie/subject/${id}`,
});
});
return dataList;
} catch (error) {
console.error("数据处理出错" + error);
return false;
}
};
// 豆瓣新片榜
doubanNewRouter.get("/douban_new", 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: "获取失败",
};
}
});
// 豆瓣新片榜 - 获取最新数据
doubanNewRouter.get("/douban_new/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,
updateTime,
total: newData.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: cachedData.length,
updateTime,
data: cachedData,
};
} else {
// 如果缓存中也没有数据,则返回错误信息
ctx.body = {
code: 500,
...routerInfo,
message: "获取失败",
};
}
}
});
doubanNewRouter.info = routerInfo;
module.exports = doubanNewRouter;

View File

@@ -1,183 +0,0 @@
const Router = require("koa-router");
const douyinRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "douyin",
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;

View File

@@ -1,155 +0,0 @@
/*
* @author: WangPeng
* @date: 2023-07-11 16:41:48
* @customEditors: imsyy
* @lastEditTime: 2023-07-11 16:03:12
*/
const Router = require("koa-router");
const douyinMusicRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "douyin",
title: "抖音",
subtitle: "热歌榜",
};
// 缓存键名
const cacheKey = "douyinMusicData";
// 调用时间
let updateTime = new Date().toISOString();
// 调用路径
const url = "https://aweme.snssdk.com/aweme/v1/chart/music/list/";
const HEADERS = {
"user-agent": "okhttp3",
};
const QUERIES = {
device_platform: "android",
version_name: "13.2.0",
version_code: "130200",
aid: "1128",
chart_id: "6853972723954146568",
count: "100",
};
// 数据处理
const getData = (data) => {
if (!data) return [];
try {
return data.map((v) => {
const item = v.music_info;
return {
id: item.id,
title: item.title,
album: item.album,
artist: item.author,
pic: item?.cover_large.url_list[0],
lyric: item.lyric_url,
url: item.play_url.uri,
mobileUrl: item.play_url.uri,
// h5Url: item.matched_song?.h5_url,
};
});
} catch (error) {
console.error("数据处理出错" + error);
return [];
}
};
// 抖音热歌榜
douyinMusicRouter.get("/douyin_music", 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: HEADERS,
params: QUERIES,
});
data = getData(response.data.music_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: "获取失败",
};
}
});
// 抖音热歌榜 - 获取最新数据
douyinMusicRouter.get("/douyin_music/new", async (ctx) => {
console.log("获取抖音热歌榜 - 最新数据");
try {
// 从服务器拉取最新数据
const response = await axios.get(url, {
headers: HEADERS,
params: QUERIES,
});
const newData = getData(response.data.word_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: "获取失败",
};
}
}
});
douyinMusicRouter.info = routerInfo;
module.exports = douyinMusicRouter;

View File

@@ -1,149 +0,0 @@
/*
* @author: WangPeng
* @date: 2023-07-10 16:56:01
* @customEditors: imsyy
* @lastEditTime: 2023-07-11 16:54:38
*/
const Router = require("koa-router");
const douyinNewRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "douyin",
title: "抖音",
subtitle: "热点榜",
};
// 缓存键名
const cacheKey = "douyinHotNewData";
// 调用时间
let updateTime = new Date().toISOString();
// 调用路径
const url = "https://aweme.snssdk.com/aweme/v1/hot/search/list/";
const HEADERS = {
"user-agent": "okhttp3",
};
const QUERIES = {
device_platform: "android",
version_name: "13.2.0",
version_code: "130200",
aid: "1128",
};
// 数据处理
const getData = (data) => {
if (!data) return [];
try {
const jsonObject = data.data.word_list;
return jsonObject.map((v) => {
return {
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)}`,
};
});
} catch (error) {
console.error("数据处理出错" + error);
return [];
}
};
// 抖音热点榜
douyinNewRouter.get("/douyin_new", 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: HEADERS,
params: QUERIES,
});
data = getData(response.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: "获取失败",
};
}
});
// 抖音热点榜 - 获取最新数据
douyinNewRouter.get("/douyin_new/new", async (ctx) => {
console.log("获取抖音热点榜 - 最新数据");
try {
// 从服务器拉取最新数据
const response = await axios.get(url, {
headers: HEADERS,
params: QUERIES,
});
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: "获取失败",
};
}
}
});
douyinNewRouter.info = routerInfo;
module.exports = douyinNewRouter;

View File

@@ -1,123 +0,0 @@
const Router = require("koa-router");
const genshinRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "genshin",
title: "原神",
subtitle: "最新信息",
};
// 缓存键名
const cacheKey = "genshinData";
// 调用时间
let updateTime = new Date().toISOString();
// 调用路径
const url =
"https://content-static.mihoyo.com/content/ysCn/getContentList?pageSize=50&pageNum=1&channelId=10";
// 数据处理
const getData = (data) => {
if (!data) return [];
return data.map((v) => {
return {
id: v.id,
title: v.title,
pic: v.ext[1]?.value[0]?.url,
start_time: v?.start_time,
url: `https://ys.mihoyo.com/main/news/detail/${v.id}`,
mobileUrl: `https://ys.mihoyo.com/main/m/news/detail/${v.id}`,
};
});
};
// 原神最新信息
genshinRouter.get("/genshin", 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: "获取失败",
};
}
});
// 原神最新信息 - 获取最新数据
genshinRouter.get("/genshin/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: "获取失败",
};
}
}
});
genshinRouter.info = routerInfo;
module.exports = genshinRouter;

View File

@@ -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;

View File

@@ -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 = {
name: "ithome",
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) => {
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: newData.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: cachedData.length,
updateTime,
data: cachedData,
};
} else {
// 如果缓存中也没有数据,则返回错误信息
ctx.body = {
code: 500,
...routerInfo,
message: "获取失败",
};
}
}
});
itHomeRouter.info = routerInfo;
module.exports = itHomeRouter;

View File

@@ -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 = {
name: "juejin",
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;

View File

@@ -1,162 +0,0 @@
/*
* @author: MCBBC
* @date: 2023-07-17
* @customEditors: imsyy
* @lastEditTime: 2023-07-17
*/
const Router = require("koa-router");
const kuaishouRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "kuaishou",
title: "快手",
subtitle: "热榜",
};
// 缓存键名
const cacheKey = "kuaishouData";
// 调用时间
let updateTime = new Date().toISOString();
// 调用路径
const url = "https://www.kuaishou.com/?isHome=1";
// Unicode 解码
const decodedString = (encodedString) => {
return encodedString.replace(/\\u([\d\w]{4})/gi, (match, grp) =>
String.fromCharCode(parseInt(grp, 16)),
);
};
// 数据处理
const getData = (data) => {
if (!data) return [];
const dataList = [];
try {
const pattern = /window.__APOLLO_STATE__=(.*);\(function\(\)/s;
const idPattern = /clientCacheKey=([A-Za-z0-9]+)/s;
const matchResult = data.match(pattern);
const jsonObject = JSON.parse(matchResult[1])["defaultClient"];
// 获取所有分类
const allItems = jsonObject['$ROOT_QUERY.visionHotRank({"page":"home"})']["items"];
// 遍历所有分类
allItems.forEach((v) => {
// 基础数据
const image = jsonObject[v.id]["poster"];
const id = image.match(idPattern)[1];
// 数据处理
dataList.push({
title: jsonObject[v.id]["name"],
pic: decodedString(image),
hot: jsonObject[v.id]["hotValue"],
url: `https://www.kuaishou.com/short-video/${id}`,
mobileUrl: `https://www.kuaishou.com/short-video/${id}`,
});
});
return dataList;
} catch (error) {
console.error("数据处理出错" + error);
return false;
}
};
// 快手热榜
kuaishouRouter.get("/kuaishou", 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,
...routerInfo,
message: "获取失败",
};
}
});
// 快手热榜 - 获取最新数据
kuaishouRouter.get("/kuaishou/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: "获取失败",
};
}
}
});
kuaishouRouter.info = routerInfo;
module.exports = kuaishouRouter;

View File

@@ -1,140 +0,0 @@
const Router = require("koa-router");
const lolRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "lol",
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;

View File

@@ -1,130 +0,0 @@
/*
* @author: MCBBC
* @date: 2023-07-17
* @customEditors: imsyy
* @lastEditTime: 2023-07-17
*/
const Router = require("koa-router");
const neteaseRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "netease",
title: "网易新闻",
subtitle: "热点榜",
};
// 缓存键名
const cacheKey = "neteaseData";
// 调用时间
let updateTime = new Date().toISOString();
// 调用路径
const url = "https://m.163.com/fe/api/hot/news/flow";
// 数据处理
const getData = (data) => {
if (!data) return [];
return data.map((v) => {
return {
id: v.skipID,
title: v.title,
desc: v._keyword,
pic: v.imgsrc,
owner: v.source,
url: `https://www.163.com/dy/article/${v.skipID}.html`,
mobileUrl: v.url,
};
});
};
// 网易新闻热榜
neteaseRouter.get("/netease", 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: "获取失败",
};
}
});
// 网易新闻热榜 - 获取最新数据
neteaseRouter.get("/netease/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: "获取失败",
};
}
}
});
neteaseRouter.info = routerInfo;
module.exports = neteaseRouter;

View File

@@ -1,124 +0,0 @@
const Router = require("koa-router");
const newsqqRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "newsqq",
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;

View File

@@ -1,124 +0,0 @@
const Router = require("koa-router");
const sspaiRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "sspai",
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;

View File

@@ -1,123 +0,0 @@
const Router = require("koa-router");
const thepaperRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "thepaper",
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;

View File

@@ -1,123 +0,0 @@
const Router = require("koa-router");
const tiebaRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "tieba",
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;

View File

@@ -1,122 +0,0 @@
const Router = require("koa-router");
const toutiaoRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "toutiao",
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;

View File

@@ -1,134 +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 = {
name: "weibo",
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;

View File

@@ -1,138 +0,0 @@
const Router = require("koa-router");
const wereadRouter = new Router();
const axios = require("axios");
const getWereadID = require("../utils/getWereadID");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
const routerInfo = {
name: "weread",
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;
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/bookDetail/${getWereadID(book.bookId)}`,
mobileUrl: `https://weread.qq.com/web/bookDetail/${getWereadID(book.bookId)}`,
};
});
};
// 微信读书
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;

View File

@@ -1,143 +0,0 @@
const Router = require("koa-router");
const zhihuRouter = new Router();
const axios = require("axios");
const { get, set, del } = require("../utils/cacheData");
// 接口信息
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;

66
src/app.tsx Normal file
View File

@@ -0,0 +1,66 @@
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: (origin) => {
// 是否指定域名
const isSame = config.ALLOWED_HOST && origin.endsWith(config.ALLOWED_HOST);
return isSame ? 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(`❌ [ERROR] ${err?.message}`);
return c.html(<Error error={err?.message} />, 500);
});
export default app;

56
src/config.ts Normal file
View File

@@ -0,0 +1,56 @@
import dotenv from "dotenv";
// 环境变量
dotenv.config();
export type Config = {
PORT: number;
DISALLOW_ROBOT: boolean;
CACHE_TTL: number;
REQUEST_TIMEOUT: number;
ALLOWED_DOMAIN: string;
ALLOWED_HOST: string;
USE_LOG_FILE: boolean;
RSS_MODE: boolean;
REDIS_HOST: string;
REDIS_PORT: number;
REDIS_PASSWORD: string;
ZHIHU_COOKIE: string;
};
// 验证并提取环境变量
const getEnvVariable = (key: string): string | undefined => {
const value = process.env[key];
if (value === undefined) return undefined;
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("REQUEST_TIMEOUT", 6000),
ALLOWED_DOMAIN: getEnvVariable("ALLOWED_DOMAIN") || "*",
ALLOWED_HOST: getEnvVariable("ALLOWED_HOST") || "imsyy.top",
USE_LOG_FILE: getBooleanEnvVariable("USE_LOG_FILE", true),
RSS_MODE: getBooleanEnvVariable("RSS_MODE", false),
REDIS_HOST: getEnvVariable("REDIS_HOST") || "127.0.0.1",
REDIS_PORT: getNumericEnvVariable("REDIS_PORT", 6379),
REDIS_PASSWORD: getEnvVariable("REDIS_PASSWORD") || "",
ZHIHU_COOKIE: getEnvVariable("ZHIHU_COOKIE") || "",
};

25
src/index.ts Normal file
View 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 successfully runs on port ${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;

116
src/registry.ts Normal file
View File

@@ -0,0 +1,116 @@
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> = [];
// 建立完整目录路径
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(`📂 The directory ${routersDirPath} does not exist or is not a directory`);
}
// 注册全部路由
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 generation failed" }, 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: undefined,
message: "This interface is temporarily offline",
};
}
return { name: path, path: `/${path}` };
}),
},
200,
),
);
export default app;

13
src/robots.txt.ts Normal file
View 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;

444
src/router.types.d.ts vendored Normal file
View File

@@ -0,0 +1,444 @@
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;
itemid: string;
desc: string;
scheme: string;
word: string;
word_scheme: string;
note: string;
flag_desc: string;
num: number;
onboard_time: number;
};
zhihu: {
target: {
id: number;
title: string;
excerpt: string;
created: number;
url: string;
};
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;
view_status: number;
images: string[];
};
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;
};
history: {
year: string;
title: string;
link: string;
desc: string;
cover: string;
pic_share: string;
};
hupu: {
tid: number;
title: string;
replies: number;
username: string;
time: string;
url: string;
};
sina: {
base: {
base: {
uniqueId: string;
url: string;
};
};
info: {
hotValue: string;
title: string;
};
};
"sina-news": {
id: string;
title: string;
media: string;
url: string;
create_date: string;
create_time: string;
top_num: string;
time: string;
};
coolapk: {
id: number;
ttitle: string;
shareUrl: string;
username: string;
tpic: string;
message: string;
replynum: number;
};
guokr: {
id: number;
title: string;
summary: string;
author: {
nickname: string;
};
date_modified: string;
small_image: string;
};
kuaishou: {
id: string;
name: string;
hotValue: string;
iconUrl: string;
poster: string;
photoIds: {
json: string[];
};
};
smzdm: {
content: string;
title: string;
article_id: string;
nickname: string;
jump_link: string;
pic_url: string;
collection_count: string;
time_sort: string;
};
yystv: {
id: string;
cover: string;
title: string;
preface: string;
author: string;
createtime: string;
};
dgtle: {
id: number;
content: string;
cover: string;
from: string;
title: string;
membernum: number;
created_at: number;
type: number;
};
geekpark: {
post: {
id: number;
nickname: string;
title: string;
abstract: string;
cover_url: string;
views: number;
published_timestamp: number;
authors: {
nickname: string;
}[];
};
};
linuxdo: {
id: string;
title: string;
url: string;
author: string;
desc: string;
timestamp: string;
};
hackernews: {
id: string;
title: string;
hot: number | undefined;
timestamp: number | undefined;
url: string;
mobileUrl: string;
};
github: {
id: string;
title: string;
desc?: string;
hot: number | undefined;
timestamp: number | undefined;
url: string;
mobileUrl: string;
};
producthunt: {
id: string;
title: string;
hot: number | undefined;
timestamp: number | undefined;
url: string;
mobileUrl: string;
};
newsmth: {
firstArticleId: string;
subject: string;
article: {
topicId: string;
postTime: number;
subject: string;
body: string;
account: {
name: string;
};
};
board: {
title: string;
name: string;
};
};
gameres: {
id: string;
title: string;
hot: number | undefined;
desc: string;
cover: string;
timestamp: number | undefined;
url: string;
mobileUrl: string;
};
};

75
src/routes/36kr.ts Normal file
View File

@@ -0,0 +1,75 @@
import type { RouterData, ListContext, Options, RouterResType } from "../types.js";
import type { RouterType } from "../router.types.js";
import { post } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
const typeMap: Record<string, string> = {
hot: "人气榜",
video: "视频榜",
comment: "热议榜",
collect: "收藏榜",
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "hot";
const listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "36kr",
title: "36氪",
type: typeMap[type],
params: {
type: {
name: "热榜分类",
type: typeMap,
},
},
link: "https://m.36kr.com/hot-list-m",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean): Promise<RouterResType> => {
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 {
...result,
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 || undefined,
url: `https://www.36kr.com/p/${v.itemId}`,
mobileUrl: `https://m.36kr.com/p/${v.itemId}`,
};
}),
};
};

54
src/routes/51cto.ts Normal file
View File

@@ -0,0 +1,54 @@
import type { RouterData, RouterResType } 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 listData = await getList(noCache);
const routeData: RouterData = {
name: "51cto",
title: "51CTO",
type: "推荐榜",
link: "https://www.51cto.com/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean): Promise<RouterResType> => {
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 {
...result,
data: list.map((v: RouterType["51cto"]) => ({
id: v.source_id,
title: v.title,
cover: v.cover,
desc: v.abstract,
timestamp: getTime(v.pubdate),
hot: undefined,
url: v.url,
mobileUrl: v.url,
})),
};
};

63
src/routes/52pojie.ts Normal file
View File

@@ -0,0 +1,63 @@
import type { RouterData, ListContext, Options, RouterResType } from "../types.js";
import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
import { parseRSS } from "../utils/parseRSS.js";
import iconv from "iconv-lite";
const typeMap: Record<string, string> = {
digest: "最新精华",
hot: "最新热门",
new: "最新回复",
newthread: "最新发表",
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "digest";
const listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "52pojie",
title: "吾爱破解",
type: typeMap[type],
params: {
type: {
name: "榜单分类",
type: typeMap,
},
},
link: "https://www.52pojie.cn/",
total: listData?.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean): Promise<RouterResType> => {
const { type } = options;
const url = `https://www.52pojie.cn/forum.php?mod=guide&view=${type}&rss=1`;
const result = await get({
url,
noCache,
responseType: "arraybuffer",
headers: {
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 utf8Data = iconv.decode(result.data, "gbk");
const list = await parseRSS(utf8Data);
return {
...result,
data: list.map((v, i) => ({
id: v.guid || i,
title: v.title || "",
desc: v.content?.trim() || "",
author: v.author,
timestamp: getTime(v.pubDate || 0),
hot: undefined,
url: v.link || "",
mobileUrl: v.link || "",
})),
};
};

78
src/routes/acfun.ts Normal file
View File

@@ -0,0 +1,78 @@
import type { RouterData, ListContext, Options, RouterResType } from "../types.js";
import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
const typeMap: Record<string, string> = {
"-1": "综合",
"155": "番剧",
"1": "动画",
"60": "娱乐",
"201": "生活",
"58": "音乐",
"123": "舞蹈·偶像",
"59": "游戏",
"70": "科技",
"68": "影视",
"69": "体育",
"125": "鱼塘",
};
const rangeMap: Record<string, string> = {
DAY: "今日",
THREE_DAYS: "三日",
WEEK: "本周",
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "-1";
const range = c.req.query("range") || "DAY";
const listData = await getList({ type, range }, noCache);
const routeData: RouterData = {
name: "acfun",
title: "AcFun",
type: `排行榜 · ${typeMap[type]}`,
description: "AcFun是一家弹幕视频网站致力于为每一个人带来欢乐。",
params: {
type: {
name: "频道",
type: typeMap,
},
range: {
name: "时间",
type: rangeMap,
},
},
link: "https://www.acfun.cn/rank/list/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean): Promise<RouterResType> => {
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 {
...result,
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}`,
})),
};
};

63
src/routes/baidu.ts Normal file
View File

@@ -0,0 +1,63 @@
import type { RouterData, ListContext, Options, RouterResType } from "../types.js";
import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js";
const typeMap: Record<string, string> = {
realtime: "热搜",
novel: "小说",
movie: "电影",
teleplay: "电视剧",
car: "汽车",
game: "游戏",
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "realtime";
const listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "baidu",
title: "百度",
type: typeMap[type],
params: {
type: {
name: "热搜类别",
type: typeMap,
},
},
link: "https://top.baidu.com/board",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean): Promise<RouterResType> => {
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 {
...result,
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: 0,
hot: Number(v.hotScore || 0),
url: `https://www.baidu.com/s?wd=${encodeURIComponent(v.query)}`,
mobileUrl: v.rawUrl,
})),
};
};

116
src/routes/bilibili.ts Normal file
View File

@@ -0,0 +1,116 @@
import type { RouterData, ListContext, Options, RouterResType } 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";
import logger from "../utils/logger.js";
const typeMap: Record<string, string> = {
"0": "全站",
"1": "动画",
"3": "音乐",
"4": "游戏",
"5": "娱乐",
"188": "科技",
"119": "鬼畜",
"129": "舞蹈",
"155": "时尚",
"160": "生活",
"168": "国创相关",
"181": "影视",
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "0";
const listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "bilibili",
title: "哔哩哔哩",
type: `热榜 · ${typeMap[type]}`,
description: "你所热爱的,就是你的生活",
params: {
type: {
name: "排行榜分区",
type: typeMap,
},
},
link: "https://www.bilibili.com/v/popular/rank/all",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean): Promise<RouterResType> => {
const { type } = options;
const wbiData = await getBiliWbi();
const url = `https://api.bilibili.com/x/web-interface/ranking/v2?rid=${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',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Sec-Ch-Ua': '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
},
noCache: false,
});
// 是否触发风控
if (result.data?.data?.list?.length > 0) {
logger.info('bilibili 新接口')
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 || 0,
url: v.short_link_v2 || `https://www.bilibili.com/video/${v.bvid}`,
mobileUrl: `https://m.bilibili.com/video/${v.bvid}`,
})),
};
}
// 采用备用接口
else {
logger.info('bilibili 备用接口')
const url = `https://api.bilibili.com/x/web-interface/ranking?jsonp=jsonp?rid=${type}&type=all&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 {
...result,
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: undefined,
hot: v.video_review,
url: `https://www.bilibili.com/video/${v.bvid}`,
mobileUrl: `https://m.bilibili.com/video/${v.bvid}`,
})),
};
}
};

41
src/routes/coolapk.ts Normal file
View 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 { genHeaders } from "../utils/getToken/coolapk.js";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const listData = await getList(noCache);
const routeData: RouterData = {
name: "coolapk",
title: "酷安",
type: "热榜",
link: "https://www.coolapk.com/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = `https://api.coolapk.com/v6/page/dataList?url=/feed/statList?cacheExpires=300&statType=day&sortField=detailnum&title=今日热门&title=今日热门&subTitle=&page=1`;
const result = await get({
url,
noCache,
headers: genHeaders(),
});
const list = result.data.data;
return {
...result,
data: list.map((v: RouterType["coolapk"]) => ({
id: v.id,
title: v.message,
cover: v.tpic,
author: v.username,
desc: v.ttitle,
timestamp: undefined,
hot: undefined,
url: v.shareUrl,
mobileUrl: v.shareUrl,
})),
};
};

38
src/routes/csdn.ts Normal file
View File

@@ -0,0 +1,38 @@
import type { RouterData, RouterResType } 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 listData = await getList(noCache);
const routeData: RouterData = {
name: "csdn",
title: "CSDN",
type: "排行榜",
description: "专业开发者社区",
link: "https://www.csdn.net/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean): Promise<RouterResType> => {
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 {
...result,
data: list.map((v: RouterType["csdn"]) => ({
id: v.productId,
title: v.articleTitle,
cover: v.picList?.[0] || undefined,
desc: undefined,
author: v.nickName,
timestamp: getTime(v.period),
hot: Number(v.hotRankScore),
url: v.articleDetailUrl,
mobileUrl: v.articleDetailUrl,
})),
};
};

39
src/routes/dgtle.ts Normal file
View 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 listData = await getList(noCache);
const routeData: RouterData = {
name: "dgtle",
title: "数字尾巴",
type: "热门文章",
description:
"致力于分享美好数字生活体验,囊括你闻所未闻的最丰富数码资讯,触所未触最抢鲜产品评测,随时随地感受尾巴们各式数字生活精彩图文、摄影感悟、旅行游记、爱物分享。",
link: "https://www.dgtle.com/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = `https://opser.api.dgtle.com/v2/news/index`;
const result = await get({ url, noCache });
const list = result.data?.items;
return {
...result,
data: list.map((v: RouterType["dgtle"]) => ({
id: v.id,
title: v.title || v.content,
desc: v.content,
cover: v.cover,
author: v.from,
hot: v.membernum,
timestamp: getTime(v.created_at),
url: `https://www.dgtle.com/news-${v.id}-${v.type}.html`,
mobileUrl: `https://m.dgtle.com/news-details/${v.id}`,
})),
};
};

View File

@@ -0,0 +1,54 @@
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 listData = await getList(noCache);
const routeData: RouterData = {
name: "douban-group",
title: "豆瓣讨论",
type: "讨论精选",
link: "https://www.douban.com/group/explore",
total: listData.data?.length || 0,
...listData,
};
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: getTime(dom.find("span.pubtime").text().trim()),
hot: 0,
url: url || `https://www.douban.com/group/topic/${getNumbers(url)}`,
mobileUrl: `https://m.douban.com/group/topic/${getNumbers(url)}/`,
};
});
return {
...result,
data: listData,
};
};

View File

@@ -0,0 +1,62 @@
import type { RouterData } from "../types.js";
import { load } from "cheerio";
import { get } from "../utils/getData.js";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const listData = await getList(noCache);
const routeData: RouterData = {
name: "douban-movie",
title: "豆瓣电影",
type: "新片榜",
link: "https://movie.douban.com/chart",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
// 数据处理
const getNumbers = (text: string | undefined): number => {
if (!text) return 0;
const regex = /\d+/;
const match = text.match(regex);
if (match) {
return Number(match[0]);
} else {
return 0;
}
};
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 scoreDom = dom.find(".rating_nums");
const score = scoreDom.length > 0 ? scoreDom.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: undefined,
hot: getNumbers(dom.find("span.pl").text()),
url: url || `https://movie.douban.com/subject/${getNumbers(url)}/`,
mobileUrl: `https://m.douban.com/movie/subject/${getNumbers(url)}/`,
};
});
return {
...result,
data: listData,
};
};

58
src/routes/douyin.ts Normal file
View File

@@ -0,0 +1,58 @@
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 listData = await getList(noCache);
const routeData: RouterData = {
name: "douyin",
title: "抖音",
type: "热榜",
description: "实时上升热点",
link: "https://www.douyin.com",
total: listData.data?.length || 0,
...listData,
};
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 undefined;
}
};
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 {
...result,
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/${v.sentence_id}`,
mobileUrl: `https://www.douyin.com/hot/${v.sentence_id}`,
})),
};
};

56
src/routes/earthquake.ts Normal file
View File

@@ -0,0 +1,56 @@
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";
const mappings: Record<string, string> = {
O_TIME: "发震时刻(UTC+8)",
LOCATION_C: "参考位置",
M: "震级(M)",
EPI_LAT: "纬度(°)",
EPI_LON: "经度(°)",
EPI_DEPTH: "深度(千米)",
SAVE_TIME: "录入时间",
};
export const handleRoute = async (_: undefined, noCache: boolean) => {
const listData = await getList(noCache);
const routeData: RouterData = {
name: "earthquake",
title: "中国地震台",
type: "地震速报",
link: "https://news.ceic.ac.cn/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = `https://news.ceic.ac.cn/speedsearch.html`;
const result = await get({ url, noCache });
const regex = /const newdata = (\[.*?\]);/s;
const match = result.data.match(regex);
const list = match && match[1] ? JSON.parse(match[1]) : [];
return {
...result,
data: list.map((v: RouterType["earthquake"]) => {
const contentBuilder = [];
const { NEW_DID, LOCATION_C, M } = v;
for (const mappingsKey in mappings) {
contentBuilder.push(
`${mappings[mappingsKey as keyof typeof mappings]}${v[mappingsKey as keyof typeof v]}`,
);
}
return {
id: NEW_DID,
title: `${LOCATION_C}发生${M}级地震`,
desc: contentBuilder.join("\n"),
timestamp: getTime(v["O_TIME" as keyof typeof v]),
hot: undefined,
url: `https://news.ceic.ac.cn/${NEW_DID}.html`,
mobileUrl: `https://news.ceic.ac.cn/${NEW_DID}.html`,
};
}),
};
};

66
src/routes/gameres.ts Normal file
View File

@@ -0,0 +1,66 @@
import type { RouterData } from "../types.js";
import { load } from "cheerio";
import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
import { RouterType } from "../router.types.js";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const listData = await getList(noCache);
const routeData: RouterData = {
name: "gameres",
title: "GameRes 游资网",
type: "最新资讯",
description:
"面向游戏从业者的游戏开发资讯,旨在为游戏制作人提供游戏研发类的程序技术、策划设计、艺术设计、原创设计等资讯内容。",
link: "https://www.gameres.com",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = `https://www.gameres.com`;
const result = await get({ url, noCache });
const $ = load(result.data);
const container = $('div[data-news-pane-id="100000"]');
const listDom = container.find("article.feed-item");
const listData = Array.from(listDom).map((el) => {
const dom = $(el);
const titleEl = dom.find(".feed-item-title-a").first();
const title = titleEl.text().trim();
const href = titleEl.attr("href");
const url = href?.startsWith("http") ? href : `https://www.gameres.com${href ?? ""}`;
const cover = dom.find(".thumb").attr("data-original") || "";
const desc = dom.find(".feed-item-right > p").first().text().trim();
const dateTime = dom.find(".mark-info").contents().first().text().trim();
const timestamp = getTime(dateTime);
// 热度(列表暂无评论数)
const hot = undefined;
return {
title,
desc,
cover,
timestamp,
hot,
url,
id: url,
mobileUrl: url,
} as RouterType["gameres"];
});
return {
...result,
data: listData,
};
};

41
src/routes/geekpark.ts Normal file
View 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 listData = await getList(noCache);
const routeData: RouterData = {
name: "geekpark",
title: "极客公园",
type: "热门文章",
description: "极客公园聚焦互联网领域,跟踪新鲜的科技新闻动态,关注极具创新精神的科技产品。",
link: "https://www.geekpark.net/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = `https://mainssl.geekpark.net/api/v2`;
const result = await get({ url, noCache });
const list = result.data?.homepage_posts;
return {
...result,
data: list.map((v: RouterType["geekpark"]) => {
const post = v.post;
return {
id: post.id,
title: post.title,
desc: post.abstract,
cover: post.cover_url,
author: post?.authors?.[0]?.nickname,
hot: post.views,
timestamp: getTime(post.published_timestamp),
url: `https://www.geekpark.net/news/${post.id}`,
mobileUrl: `https://www.geekpark.net/news/${post.id}`,
};
}),
};
};

52
src/routes/genshin.ts Normal file
View File

@@ -0,0 +1,52 @@
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 listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "genshin",
title: "原神",
type: "最新动态",
params: {
type: {
name: "榜单分类",
type: {
1: "公告",
2: "活动",
3: "资讯",
},
},
},
link: "https://www.miyoushe.com/ys/home/28",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean) => {
const { type } = options;
const url = `https://bbs-api-static.miyoushe.com/painter/wapi/getNewsList?client_type=4&gids=2&last_id=&page_size=20&type=${type}`;
const result = await get({ url, noCache });
const list = result.data.data.list;
return {
...result,
data: list.map((v: RouterType["miyoushe"]) => {
const data = v.post;
return {
id: data.post_id,
title: data.subject,
desc: data.content,
cover: data.cover || data?.images?.[0],
author: v.user?.nickname || undefined,
timestamp: getTime(data.created_at),
hot: data.view_status,
url: `https://www.miyoushe.com/ys/article/${data.post_id}`,
mobileUrl: `https://m.miyoushe.com/ys/#/article/${data.post_id}`,
};
}),
};
};

206
src/routes/github.ts Normal file
View File

@@ -0,0 +1,206 @@
// getTrending.ts
import fetch from "node-fetch";
import * as cheerio from "cheerio";
import { ListContext } from "../types";
import logger from "../utils/logger.js";
import { getCache, setCache } from "../utils/cache.js";
/**
* 定义 Trending 仓库信息的类型
*/
type RepoInfo = {
owner: string; // 仓库所属者
repo: string; // 仓库名称
url: string; // 仓库链接
description: string; // 仓库描述
language: string; // 编程语言
stars: string; // Stars (由于可能包含逗号或者其他符号,这里先用 string 存;实际可自行转 number)
forks: string; // Forks
todayStars?: string | number; // 今日 Star
};
type TrendingRepoInfo = {
data: RepoInfo[];
updateTime: string;
fromCache: boolean;
};
type TrendingType = "daily" | "weekly" | "monthly";
const typeMap: Record<TrendingType, string> = {
daily: "日榜",
weekly: "周榜",
monthly: "月榜",
};
function isTrendingType(value: string): value is TrendingType {
return ["daily", "weekly", "monthly"].includes(value as TrendingType);
}
export const handleRoute = async (c: ListContext) => {
const typeParam = c.req.query("type") || "daily";
const type = isTrendingType(typeParam) ? typeParam : "daily";
const listData = await getTrendingRepos(type);
const routeData = {
name: "github",
title: "github 趋势",
type: typeMap[type],
params: {
type: {
name: '排行榜分区',
type: typeMap,
},
},
link: `https://github.com/trending?since=${type}`,
total: listData?.data?.length || 0,
...{
...listData,
data: listData?.data?.map((v, index)=>{
return {
id:index,
title: v.repo,
desc: v.description,
hot: v.stars,
...v
}
})
}
};
return routeData;
};
/**
* 爬取 GitHub Trending 列表
* @param since 可选参数: 'daily' | 'weekly' | 'monthly',默认值为 'daily'
* @returns Promise<RepoInfo[]> 返回包含热门项目信息的数组
*/
export async function getTrendingRepos(
type: TrendingType | string = "daily",
ttl = 60 * 60 * 24,
): Promise<TrendingRepoInfo> {
const url = `https://github.com/trending?since=${type}`;
// 先从缓存中取
const cachedData = await getCache(url);
if (cachedData) {
logger.info("💾 [CHCHE] The request is cached");
return {
fromCache: true,
updateTime: cachedData.updateTime,
data: (cachedData?.data as RepoInfo[]) || [],
};
}
logger.info(`🌐 [GET] ${url}`);
// 更新请求头
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Cache-Control': 'max-age=0',
};
// 添加重试逻辑
const maxRetries = 3;
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
// 设置超时时间为 20 秒
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 20000);
const response = await fetch(url, {
headers,
signal: controller.signal
});
clearTimeout(timeout);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const html = await response.text();
// 1. 加载 HTML
const $ = cheerio.load(html);
// 2. 存储结果的数组
const results: RepoInfo[] = [];
// 3. 遍历每个 article.Box-row
$("article.Box-row").each((_, el) => {
const $el = $(el);
// 仓库标题和链接 (在 <h2> > <a> 里)
const $repoAnchor = $el.find("h2 a");
// 可能出现 "owner / repo" 这种文本
// eg: "owner / repo"
const fullNameText = $repoAnchor
.text()
.trim()
// 可能有多余空格,可以再做一次 split
// "owner / repo" => ["owner", "repo"]
.replace(/\r?\n/g, "") // 去掉换行
.replace(/\s+/g, " ") // 多空格处理
.split("/")
.map((s) => s.trim());
const owner = fullNameText[0] || "";
const repoName = fullNameText[1] || "";
// href 即仓库链接
const repoUrl = "https://github.com" + $repoAnchor.attr("href");
// 仓库描述 (<p class="col-9 color-fg-muted ...">)
const description = $el.find("p.col-9.color-fg-muted").text().trim();
// 语言 (<span itemprop="programmingLanguage">)
const language = $el.find('[itemprop="programmingLanguage"]').text().trim();
const starsText = $el.find('a[href$="/stargazers"]').text().trim();
const forksText = $el.find(`a[href$="/forks"]`).text().trim();
// 整合
results.push({
owner,
repo: repoName,
url: repoUrl || "",
description,
language,
stars: starsText,
forks: forksText,
});
});
const updateTime = new Date().toISOString();
const data = results;
await setCache(url, { data, updateTime }, ttl);
// 返回数据
logger.info(`✅ [${response?.status}] 请求成功!`);
return { fromCache: false, updateTime, data };
} catch (error: Error | unknown) {
lastError = error;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`❌ [ERROR] 第 ${i + 1} 请求失败: ${errorMessage}`);
// 如果是最后一次重试,则抛出错误
if (i === maxRetries - 1) {
logger.error("❌ [ERROR] 所有尝试请求失败!");
throw lastError;
}
// 等待一段时间后重试 (1秒、2秒、4秒...)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
continue;
}
}
throw new Error("请求失败!");
}

45
src/routes/guokr.ts Normal file
View 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 listData = await getList(noCache);
const routeData: RouterData = {
name: "guokr",
title: "果壳",
type: "热门文章",
description: "科技有意思",
link: "https://www.guokr.com/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = `https://www.guokr.com/beta/proxy/science_api/articles?limit=30`;
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/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
},
});
const list = result.data;
return {
...result,
data: list.map((v: RouterType["guokr"]) => ({
id: v.id,
title: v.title,
desc: v.summary,
cover: v.small_image,
author: v.author?.nickname,
hot: undefined,
timestamp: getTime(v.date_modified),
url: `https://www.guokr.com/article/${v.id}`,
mobileUrl: `https://m.guokr.com/article/${v.id}`,
})),
};
};

63
src/routes/hackernews.ts Normal file
View File

@@ -0,0 +1,63 @@
import type { RouterData } from "../types.js";
import { get } from "../utils/getData.js";
import { load } from "cheerio";
import type { RouterType } from "../router.types.js";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const listData = await getList(noCache);
const routeData: RouterData = {
name: "hackernews",
title: "Hacker News",
type: "Popular",
description: "News about hacking and startups",
link: "https://news.ycombinator.com/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const baseUrl = "https://news.ycombinator.com";
const result = await get({
url: baseUrl,
noCache,
headers: {
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
});
try {
const $ = load(result.data);
const stories: RouterType["hackernews"][] = [];
$(".athing").each((_, el) => {
const item = $(el);
const id = item.attr("id") || "";
const title = item.find(".titleline a").first().text().trim();
const url = item.find(".titleline a").first().attr("href");
// 获取分数并转换为数字
const scoreText = $(`#score_${id}`).text().match(/\d+/)?.[0];
const hot = scoreText ? parseInt(scoreText, 10) : undefined;
if (id && title) {
stories.push({
id,
title,
hot,
timestamp: undefined,
url: url || `${baseUrl}/item?id=${id}`,
mobileUrl: url || `${baseUrl}/item?id=${id}`,
});
}
});
return {
...result,
data: stories,
};
} catch (error) {
throw new Error(`Failed to parse HackerNews HTML: ${error}`);
}
};

48
src/routes/hellogithub.ts Normal file
View 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 sort = c.req.query("sort") || "featured";
const listData = await getList({ sort }, noCache);
const routeData: RouterData = {
name: "hellogithub",
title: "HelloGitHub",
type: "热门仓库",
description: "分享 GitHub 上有趣、入门级的开源项目",
params: {
sort: {
name: "排行榜分区",
type: {
featured: "精选",
all: "全部",
},
},
},
link: "https://hellogithub.com/",
total: listData.data?.length || 0,
...listData,
};
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 {
...result,
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}`,
})),
};
};

53
src/routes/history.ts Normal file
View File

@@ -0,0 +1,53 @@
import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.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(true).day;
const month = c.req.query("month") || getCurrentDateTime(true).month;
const listData = await getList({ month, day }, noCache);
const routeData: RouterData = {
name: "history",
title: "历史上的今天",
type: `${month}-${day}`,
params: {
month: "月份",
day: "日期",
},
link: "https://baike.baidu.com/calendar",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean) => {
const { month, day } = options;
const monthStr = month?.toString().padStart(2, "0");
const dayStr = day?.toString().padStart(2, "0");
const url = `https://baike.baidu.com/cms/home/eventsOnHistory/${monthStr}.json`;
const result = await get({
url,
noCache,
params: {
_: new Date().getTime(),
},
});
const list = monthStr ? result.data[monthStr][monthStr + dayStr] : [];
return {
...result,
data: list.map((v: RouterType["history"], index: number) => ({
id: index,
title: load(v.title).text().trim(),
cover: v.cover ? v.pic_share : undefined,
desc: load(v.desc).text().trim(),
year: v.year,
timestamp: undefined,
hot: undefined,
url: v.link,
mobileUrl: v.link,
})),
};
};

52
src/routes/honkai.ts Normal file
View File

@@ -0,0 +1,52 @@
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 listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "honkai",
title: "崩坏3",
type: "最新动态",
params: {
type: {
name: "榜单分类",
type: {
1: "公告",
2: "活动",
3: "资讯",
},
},
},
link: "https://www.miyoushe.com/bh3/home/6",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean) => {
const { type } = options;
const url = `https://bbs-api-static.miyoushe.com/painter/wapi/getNewsList?client_type=4&gids=1&last_id=&page_size=20&type=${type}`;
const result = await get({ url, noCache });
const list = result.data.data.list;
return {
...result,
data: list.map((v: RouterType["miyoushe"]) => {
const data = v.post;
return {
id: data.post_id,
title: data.subject,
desc: data.content,
cover: data.cover || data?.images?.[0],
author: v.user?.nickname || undefined,
timestamp: getTime(data.created_at),
hot: data.view_status,
url: `https://www.miyoushe.com/bh3/article/${data.post_id}`,
mobileUrl: `https://m.miyoushe.com/bh3/#/article/${data.post_id}`,
};
}),
};
};

58
src/routes/hostloc.ts Normal file
View File

@@ -0,0 +1,58 @@
import type { RouterData, ListContext, Options } from "../types.js";
import { get } from "../utils/getData.js";
import { parseRSS } from "../utils/parseRSS.js";
import { getTime } from "../utils/getTime.js";
const typeMap: Record<string, string> = {
hot: "最新热门",
digest: "最新精华",
new: "最新回复",
newthread: "最新发表",
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "hot";
const listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "hostloc",
title: "全球主机交流",
type: typeMap[type],
params: {
type: {
name: "榜单分类",
type: typeMap,
},
},
link: "https://hostloc.com/",
total: listData.data?.length || 0,
...listData,
};
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 get({
url,
noCache,
headers: {
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 list = await parseRSS(result.data);
return {
...result,
data: list.map((v, i) => ({
id: v.guid || i,
title: v.title || "",
desc: v.content || "",
author: v.author || "",
timestamp: getTime(v.pubDate || 0),
hot: undefined,
url: v.link || "",
mobileUrl: v.link || "",
})),
};
};

48
src/routes/hupu.ts Normal file
View 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";
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "1";
const listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "hupu",
title: "虎扑",
type: "步行街热帖",
params: {
type: {
name: "榜单分类",
type: {
1: "主干道",
6: "恋爱区",
11: "校园区",
12: "历史区",
612: "摄影区",
},
},
},
link: "https://bbs.hupu.com/all-gambia",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean) => {
const { type } = options;
const url = `https://m.hupu.com/api/v2/bbs/topicThreads?topicId=${type}&page=1`;
const result = await get({ url, noCache });
const list = result.data.data.topicThreads;
return {
...result,
data: list.map((v: RouterType["hupu"]) => ({
id: v.tid,
title: v.title,
author: v.username,
hot: v.replies,
timestamp: undefined,
url: `https://bbs.hupu.com/${v.tid}.html`,
mobileUrl: v.url,
})),
};
};

51
src/routes/huxiu.ts Normal file
View 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 { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const listData = await getList(noCache);
const routeData: RouterData = {
name: "huxiu",
title: "虎嗅",
type: "24小时",
link: "https://www.huxiu.com/moment/",
total: listData.data?.length || 0,
...listData,
};
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 {
...result,
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: undefined,
url: v.url || `https://www.huxiu.com/moment/${v.object_id}.html`,
mobileUrl: v.url || `https://m.huxiu.com/moment/${v.object_id}.html`,
})),
};
};

36
src/routes/ifanr.ts Normal file
View File

@@ -0,0 +1,36 @@
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 listData = await getList(noCache);
const routeData: RouterData = {
name: "ifanr",
title: "爱范儿",
type: "快讯",
description: "15秒了解全球新鲜事",
link: "https://www.ifanr.com/digest/",
total: listData.data?.length || 0,
...listData,
};
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 {
...result,
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: v.buzz_original_url || `https://www.ifanr.com/${v.post_id}`,
mobileUrl: v.buzz_original_url || `https://www.ifanr.com/digest/${v.post_id}`,
})),
};
};

View File

@@ -0,0 +1,55 @@
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 listData = await getList(noCache);
const routeData: RouterData = {
name: "ithome-xijiayi",
title: "IT之家「喜加一」",
type: "最新动态",
description: "最新最全的「喜加一」游戏动态尽在这里!",
link: "https://www.ithome.com/zt/xijiayi",
total: listData.data?.length || 0,
...listData,
};
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] : undefined;
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 || 0),
hot: Number(dom.find(".comment").text().replace(/\D/g, "")),
url: href || "",
mobileUrl: href ? replaceLink(href) : "",
};
});
return {
...result,
data: listData,
};
};

55
src/routes/ithome.ts Normal file
View File

@@ -0,0 +1,55 @@
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 listData = await getList(noCache);
const routeData: RouterData = {
name: "ithome",
title: "IT之家",
type: "热榜",
description: "爱科技,爱这里 - 前沿科技新闻网站",
link: "https://m.ithome.com/rankm/",
total: listData.data?.length || 0,
...listData,
};
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: getTime(dom.find("span.post-time").text().trim()),
hot: Number(dom.find(".review-num").text().replace(/\D/g, "")),
url: href ? replaceLink(href) : "",
mobileUrl: href ? replaceLink(href) : "",
};
});
return {
...result,
data: listData,
};
};

56
src/routes/jianshu.ts Normal file
View 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 listData = await getList(noCache);
const routeData: RouterData = {
name: "jianshu",
title: "简书",
type: "热门推荐",
description: "一个优质的创作社区",
link: "https://www.jianshu.com/",
total: listData.data?.length || 0,
...listData,
};
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: undefined,
timestamp: undefined,
url: `https://www.jianshu.com${href}`,
mobileUrl: `https://www.jianshu.com${href}`,
};
});
return {
...result,
data: listData,
};
};

76
src/routes/juejin.ts Normal file
View File

@@ -0,0 +1,76 @@
import type { ListContext, RouterData } from "../types.js";
import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js";
const headers = {
'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',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Sec-Ch-Ua': '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
}
const category_url = 'https://api.juejin.cn/tag_api/v1/query_category_briefs'
const getCategory = async()=>{
const res = await get({
url: category_url,
headers
})
const data = res?.data?.data || []
const typeObj: Record<string, string> = {}
typeObj['1'] = '综合'
data.forEach((c: { category_id: string; category_name: string }) => {
typeObj[c.category_id] = c.category_name
})
return typeObj
}
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || 1;
const listData = await getList(noCache, type);
const typeMaps = await getCategory()
const routeData: RouterData = {
name: "juejin",
title: "稀土掘金",
type: "文章榜",
params: {
type: {
name: "排行榜分区",
type: typeMaps,
},
},
link: "https://juejin.cn/hot/articles",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean, type: number | string = 1) => {
const url = `https://api.juejin.cn/content_api/v1/content/article_rank?category_id=${type}&type=hot`;
const result = await get({ url, noCache, headers });
const list = result.data.data;
return {
...result,
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: undefined,
url: `https://juejin.cn/post/${v.content.content_id}`,
mobileUrl: `https://juejin.cn/post/${v.content.content_id}`,
})),
};
};

59
src/routes/kuaishou.ts Normal file
View File

@@ -0,0 +1,59 @@
import type { RouterType } from "../router.types.js";
import type { ListItem, RouterData } from "../types.js";
import { get } from "../utils/getData.js";
import { parseChineseNumber } from "../utils/getNum.js";
import UserAgent from "user-agents";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const listData = await getList(noCache);
const routeData: RouterData = {
name: "kuaishou",
title: "快手",
type: "热榜",
description: "快手,拥抱每一种生活",
link: "https://www.kuaishou.com/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = `https://www.kuaishou.com/?isHome=1`;
const userAgent = new UserAgent({
deviceCategory: "desktop",
});
const result = await get({
url,
noCache,
headers: {
"User-Agent": userAgent.toString(),
},
});
const listData: ListItem[] = [];
// 获取主要内容
const pattern = /window.__APOLLO_STATE__=(.*);\(function\(\)/s;
const matchResult = result.data?.match(pattern);
const jsonObject = JSON.parse(matchResult[1])["defaultClient"];
// 获取所有分类
const allItems = jsonObject['$ROOT_QUERY.visionHotRank({"page":"home"})']["items"];
// 获取全部热榜
allItems?.forEach((item: { id: string }) => {
// 基础数据
const hotItem: RouterType["kuaishou"] = jsonObject[item.id];
const id = hotItem.photoIds?.json?.[0];
listData.push({
id: hotItem.id,
title: hotItem.name,
cover: decodeURIComponent(hotItem.poster),
hot: parseChineseNumber(hotItem.hotValue),
timestamp: undefined,
url: `https://www.kuaishou.com/short-video/${id}`,
mobileUrl: `https://www.kuaishou.com/short-video/${id}`,
});
});
return {
...result,
data: listData,
};
};

57
src/routes/linuxdo.ts Normal file
View File

@@ -0,0 +1,57 @@
import type { RouterData } from "../types.js";
import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
interface Topic {
id: number;
title: string;
excerpt: string;
last_poster_username: string;
created_at: string;
views: number;
like_count: number;
}
export const handleRoute = async (_: undefined, noCache: boolean) => {
const listData = await getList(noCache);
const routeData: RouterData = {
name: "linuxdo",
title: "Linux.do",
type: "热门文章",
description: "Linux 技术社区热搜",
link: "https://linux.do/hot",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = "https://linux.do/top/weekly.json";
const result = await get({
url,
noCache,
headers: {
"Accept": "application/json",
}
});
const topics = result.data.topic_list.topics as Topic[];
const list = topics.map((topic) => {
return {
id: topic.id,
title: topic.title,
desc: topic.excerpt,
author: topic.last_poster_username,
timestamp: getTime(topic.created_at),
url: `https://linux.do/t/${topic.id}`,
mobileUrl: `https://linux.do/t/${topic.id}`,
hot: topic.views || topic.like_count
};
});
return {
...result,
data: list
};
};

37
src/routes/lol.ts Normal file
View File

@@ -0,0 +1,37 @@
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 listData = await getList(noCache);
const routeData: RouterData = {
name: "lol",
title: "英雄联盟",
type: "更新公告",
link: "https://lol.qq.com/gicp/news/423/2/1334/1.html",
total: listData.data?.length || 0,
...listData,
};
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 {
...result,
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)}`,
})),
};
};

72
src/routes/miyoushe.ts Normal file
View File

@@ -0,0 +1,72 @@
import type { RouterData, ListContext, Options, RouterResType } from "../types.js";
import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
// 游戏分类
const gameMap: Record<string, string> = {
"1": "崩坏3",
"2": "原神",
"3": "崩坏学园2",
"4": "未定事件簿",
"5": "大别野",
"6": "崩坏:星穹铁道",
"7": "暂无",
"8": "绝区零",
};
// 榜单分类
const typeMap: Record<string, string> = {
"1": "公告",
"2": "活动",
"3": "资讯",
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const game = c.req.query("game") || "1";
const type = c.req.query("type") || "1";
const listData = await getList({ game, type }, noCache);
const routeData: RouterData = {
name: "miyoushe",
title: `米游社 · ${gameMap[game]}`,
type: `最新${typeMap[type]}`,
params: {
game: {
name: "游戏分类",
type: gameMap,
},
type: {
name: "榜单分类",
type: typeMap,
},
},
link: "https://www.miyoushe.com/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean): Promise<RouterResType> => {
const { game, type } = options;
const url = `https://bbs-api-static.miyoushe.com/painter/wapi/getNewsList?client_type=4&gids=${game}&last_id=&page_size=30&type=${type}`;
const result = await get({ url, noCache });
const list = result.data.data.list;
return {
...result,
data: list.map((v: RouterType["miyoushe"]) => {
const data = v.post;
return {
id: data.post_id,
title: data.subject,
desc: data.content,
cover: data.cover || data?.images?.[0],
author: v.user?.nickname || undefined,
timestamp: getTime(data.created_at),
hot: data.view_status || 0,
url: `https://www.miyoushe.com/ys/article/${data.post_id}`,
mobileUrl: `https://m.miyoushe.com/ys/#/article/${data.post_id}`,
};
}),
};
};

View File

@@ -0,0 +1,36 @@
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 listData = await getList(noCache);
const routeData: RouterData = {
name: "netease-news",
title: "网易新闻",
type: "热点榜",
link: "https://m.163.com/hot",
total: listData.data?.length || 0,
...listData,
};
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 {
...result,
data: list.map((v: RouterType["netease-news"]) => ({
id: v.docid,
title: v.title,
cover: v.imgsrc,
author: v.source,
hot: undefined,
timestamp: getTime(v.ptime),
url: `https://www.163.com/dy/article/${v.docid}.html`,
mobileUrl: `https://m.163.com/dy/article/${v.docid}.html`,
})),
};
};

42
src/routes/newsmth.ts Normal file
View 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 listData = await getList(noCache);
const routeData: RouterData = {
name: "newsmth",
title: "水木社区",
type: "热门话题",
description: "水木社区是一个源于清华的高知社群。",
link: "https://www.newsmth.net/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = `https://wap.newsmth.net/wap/api/hot/global`;
const result = await get({ url, noCache });
const list = result.data?.data?.topics;
return {
...result,
data: list.map((v: RouterType["newsmth"]) => {
const post = v.article;
const url = `https://wap.newsmth.net/article/${post.topicId}?title=${v.board?.title}&from=home`;
return {
id: v.firstArticleId,
title: post.subject,
desc: post.body,
cover: undefined,
author: post?.account?.name,
hot: undefined,
timestamp: getTime(post.postTime),
url,
mobileUrl: url,
};
}),
};
};

54
src/routes/ngabbs.ts Normal file
View File

@@ -0,0 +1,54 @@
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 listData = await getList(noCache);
const routeData: RouterData = {
name: "ngabbs",
title: "NGA",
type: "论坛热帖",
description: "精英玩家俱乐部",
link: "https://ngabbs.com/",
total: listData.data?.length || 0,
...listData,
};
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 {
...result,
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}`,
})),
};
};

44
src/routes/nodeseek.ts Normal file
View File

@@ -0,0 +1,44 @@
import type { RouterData } from "../types.js";
import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
import { parseRSS } from "../utils/parseRSS.js";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const listData = await getList(noCache);
const routeData: RouterData = {
name: "nodeseek",
title: "NodeSeek",
type: "最新",
params: {
type: {
name: "分类",
type: {
all: "所有",
},
},
},
link: "https://www.nodeseek.com/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = `https://rss.nodeseek.com/`;
const result = await get({ url, noCache });
const list = await parseRSS(result.data);
return {
...result,
data: list.map((v, i) => ({
id: v.guid || i,
title: v.title || "",
desc: v.content?.trim() || "",
author: v.author,
timestamp: getTime(v.pubDate || 0),
hot: undefined,
url: v.link || "",
mobileUrl: v.link || "",
})),
};
};

59
src/routes/nytimes.ts Normal file
View File

@@ -0,0 +1,59 @@
import type { RouterData, ListContext, Options, RouterResType } from "../types.js";
import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
import { parseRSS } from "../utils/parseRSS.js";
const areaMap: Record<string, string> = {
china: "中文网",
global: "全球版",
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const area = c.req.query("type") || "china";
const listData = await getList({ area }, noCache);
const routeData: RouterData = {
name: "nytimes",
title: "纽约时报",
type: areaMap[area],
params: {
area: {
name: "地区分类",
type: areaMap,
},
},
link: "https://www.nytimes.com/",
total: listData?.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean): Promise<RouterResType> => {
const { area } = options;
const url =
area === "china"
? "https://cn.nytimes.com/rss/"
: "https://rss.nytimes.com/services/xml/rss/nyt/World.xml";
const result = await get({
url,
noCache,
headers: {
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 list = await parseRSS(result.data);
return {
...result,
data: list.map((v, i) => ({
id: v.guid || i,
title: v.title || "",
desc: v.content?.trim() || "",
author: v.author,
timestamp: getTime(v.pubDate || 0),
hot: undefined,
url: v.link || "",
mobileUrl: v.link || "",
})),
};
};

60
src/routes/producthunt.ts Normal file
View File

@@ -0,0 +1,60 @@
import type { RouterData } from "../types.js";
import { get } from "../utils/getData.js";
import { load } from "cheerio";
import type { RouterType } from "../router.types.js";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const listData = await getList(noCache);
const routeData: RouterData = {
name: "producthunt",
title: "Product Hunt",
type: "Today",
description: "The best new products, every day",
link: "https://www.producthunt.com/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const baseUrl = "https://www.producthunt.com";
const result = await get({
url: baseUrl,
noCache,
headers: {
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
});
try {
const $ = load(result.data);
const stories: RouterType["producthunt"][] = [];
$("[data-test=homepage-section-0] [data-test^=post-item]").each((_, el) => {
const a = $(el).find("a").first();
const path = a.attr("href");
const title = $(el).find("a[data-test^=post-name]").text().trim();
const id = $(el).attr("data-test")?.replace("post-item-", "");
const vote = $(el).find("[data-test=vote-button]").text().trim();
if (path && id && title) {
stories.push({
id,
title,
hot: parseInt(vote) || undefined,
timestamp: undefined,
url: `${baseUrl}${path}`,
mobileUrl: `${baseUrl}${path}`,
});
}
});
return {
...result,
data: stories,
};
} catch (error) {
throw new Error(`Failed to parse Product Hunt HTML: ${error}`);
}
};

37
src/routes/qq-news.ts Normal file
View File

@@ -0,0 +1,37 @@
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 listData = await getList(noCache);
const routeData: RouterData = {
name: "qq-news",
title: "腾讯新闻",
type: "热点榜",
link: "https://news.qq.com/",
total: listData.data?.length || 0,
...listData,
};
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 {
...result,
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}`,
})),
};
};

134
src/routes/sina-news.ts Normal file
View File

@@ -0,0 +1,134 @@
import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js";
import { getTime, getCurrentDateTime } from "../utils/getTime.js";
import { get } from "../utils/getData.js";
// 榜单类别
const listType = {
"1": {
name: "总排行",
www: "news",
params: "www_www_all_suda_suda",
},
"2": {
name: "视频排行",
www: "news",
params: "video_news_all_by_vv",
},
"3": {
name: "图片排行",
www: "news",
params: "total_slide_suda",
},
"4": {
name: "国内新闻",
www: "news",
params: "news_china_suda",
},
"5": {
name: "国际新闻",
www: "news",
params: "news_world_suda",
},
"6": {
name: "社会新闻",
www: "news",
params: "news_society_suda",
},
"7": {
name: "体育新闻",
www: "sports",
params: "sports_suda",
},
"8": {
name: "财经新闻",
www: "finance",
params: "finance_0_suda",
},
"9": {
name: "娱乐新闻",
www: "ent",
params: "ent_suda",
},
"10": {
name: "科技新闻",
www: "tech",
params: "tech_news_suda",
},
"11": {
name: "军事新闻",
www: "news",
params: "news_mil_suda",
},
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "1";
const listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "sina-news",
title: "新浪新闻",
type: listType[type as keyof typeof listType].name,
params: {
type: {
name: "榜单分类",
type: Object.fromEntries(Object.entries(listType).map(([key, value]) => [key, value.name])),
},
},
link: "https://sinanews.sina.cn/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
// JSONP 处理
const parseData = (data: string) => {
// 移除前后多余空白
if (!data) throw new Error("Input data is empty or invalid");
// 提取 JSON 字符串部分
const prefix = "var data = ";
if (!data.startsWith(prefix))
throw new Error("Input data does not start with the expected prefix");
let jsonString = data.slice(prefix.length).trim();
// 确保字符串以 ';' 结尾并移除它
if (jsonString.endsWith(";")) {
jsonString = jsonString.slice(0, -1).trim();
} else {
throw new Error("Input data does not end with a semicolon");
}
// 格式是否正确
if (jsonString.startsWith("{") && jsonString.endsWith("}")) {
// 解析为 JSON 对象
try {
const jsonData = JSON.parse(jsonString);
return jsonData;
} catch (error) {
throw new Error("Failed to parse JSON: " + error);
}
} else {
throw new Error("Invalid JSON format");
}
};
const getList = async (options: Options, noCache: boolean) => {
const { type } = options;
// 必要数据
const { params, www } = listType[type as keyof typeof listType];
const { year, month, day } = getCurrentDateTime(true);
const url = `https://top.${www}.sina.com.cn/ws/GetTopDataList.php?top_type=day&top_cat=${params}&top_time=${year + month + day}&top_show_num=50`;
const result = await get({ url, noCache });
const list = parseData(result.data).data;
return {
...result,
data: list.map((v: RouterType["sina-news"]) => ({
id: v.id,
title: v.title,
author: v.media || undefined,
hot: parseFloat(v.top_num.replace(/,/g, "")),
timestamp: getTime(v.create_date + " " + v.create_time),
url: v.url,
mobileUrl: v.url,
})),
};
};

62
src/routes/sina.ts Normal file
View File

@@ -0,0 +1,62 @@
import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js";
import { parseChineseNumber } from "../utils/getNum.js";
import { get } from "../utils/getData.js";
const typeMap: Record<string, string> = {
all: "新浪热榜",
hotcmnt: "热议榜",
minivideo: "视频热榜",
ent: "娱乐热榜",
ai: "AI热榜",
auto: "汽车热榜",
mother: "育儿热榜",
fashion: "时尚热榜",
travel: "旅游热榜",
esg: "ESG热榜",
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "all";
const listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "sina",
title: "新浪网",
type: typeMap[type],
description: "热榜太多,一个就够",
params: {
type: {
name: "榜单分类",
type: typeMap,
},
},
link: "https://sinanews.sina.cn/",
total: listData.data?.length || 0,
...listData,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean) => {
const { type } = options;
const url = `https://newsapp.sina.cn/api/hotlist?newsId=HB-1-snhs%2Ftop_news_list-${type}`;
const result = await get({ url, noCache });
const list = result.data.data.hotList;
return {
...result,
data: list.map((v: RouterType["sina"]) => {
const base = v.base;
const info = v.info;
return {
id: base.base.uniqueId,
title: info.title,
desc: undefined,
author: undefined,
timestamp: undefined,
hot: parseChineseNumber(info.hotValue),
url: base.base.url,
mobileUrl: base.base.url,
};
}),
};
};

Some files were not shown because too many files have changed in this diff Show More