第一个版本

This commit is contained in:
huangxd- committed 2025-09-09 22:45:12 +08:00
1 parent 56623673a1
commit 7dc0bcff9f
7 files changed
+433 -1

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
.idea
danmu_api/package-lock.json
danmu_api/node_modules
-1
View File
@@ -1 +0,0 @@
# danmu_api
+20
View File
@@ -0,0 +1,20 @@
# 使用官方 Node.js 18 轻量版镜像作为基础镜像
FROM node:18-alpine
# 设置工作目录
WORKDIR /app/danmu_api
# 复制 package.json 和 package-lock.json(如果存在)
COPY danmu_api/package*.json ./
# 安装项目依赖
RUN npm install
# 复制所有源代码
COPY danmu_api/ .
# 暴露端口
EXPOSE 3000
# 启动命令
CMD ["node", "server.js"]
+120
View File
@@ -0,0 +1,120 @@
# 简单 Node.js API 服务器
一个基于 Node.js 和 Express 的简单 API 服务器,支持弹弹play的动漫搜索、详情查询和弹幕评论功能,以及日志记录。
## 功能
- **API 接口**:
- `GET /api/v2/search/anime?keyword=${queryTitle}`:根据关键字搜索动漫。
- `GET /api/v2/bangumi/:animeId`:获取指定动漫的详细信息。
- `GET /api/v2/comment/:commentId?withRelated=true&chConvert=1`:获取指定弹幕评论,支持返回相关评论和字符转换。
- `GET /api/logs`:获取最近的日志(最多 500 行,格式为 `[时间戳] 级别: 消息`)。
- **日志记录**:捕获 `console.log`(info 级别)和 `console.error`(error 级别),JSON 内容格式化输出。
- **部署支持**:支持本地运行、Docker 容器化和 Vercel 部署。
## 前置条件
- Node.js(v18 或更高版本)
- npm
- Docker(可选,用于容器化部署)
- Vercel CLI(用于手动 Vercel 部署)
## 本地运行
1. **克隆仓库**:
```bash
git clone <仓库地址>
cd <项目目录>
```
2. **进入 danmu_api 目录**:
```bash
cd danmu_api
```
3. **安装依赖**:
```bash
npm install
```
4. **启动服务器**:
```bash
npm start
```
服务器将在 `http://{ip}:9321` 运行。
5. **测试 API**:
使用 Postman 或 curl 测试:
- `GET http://{ip}:9321/api/v2/search/anime?keyword=Anime%20A`
- `GET http://{ip}:9321/api/v2/bangumi/1`
- `GET http://{ip}:9321/api/v2/comment/1?withRelated=true&chConvert=1`
- `GET http://{ip}:9321/api/logs`
## 使用 Docker 运行
1. **构建 Docker 镜像**:
```bash
docker build -t danmu-api-server .
```
2. **运行容器**:
```bash
docker run -p 9321:9321 danmu-api-server
```
3. **测试 API**:
使用 `http://{ip}:9321` 访问上述 API 接口。
## 部署到 Vercel
### 一键部署
点击以下按钮即可将项目快速部署到 Vercel:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/your-username/simple-api-server&project-name=simple-api-server&repository-name=simple-api-server)
**注意**:请将按钮链接中的 `https://github.com/your-username/simple-api-server` 替换为你的实际 Git 仓库地址(例如 `https://github.com/your-username/your-repo`)。编辑 `danmu_api/README.md` 并更新链接后,推送到仓库,点击按钮即可自动克隆和部署。
### 手动部署
1. **安装 Vercel CLI**:
```bash
npm install -g vercel
```
2. **登录 Vercel**:
```bash
vercel login
```
3. **部署项目**:
在项目根目录运行:
```bash
vercel
```
- 按提示操作:
- 确认部署(输入 `y`)。
- 选择你的 Vercel 账户或团队(scope)。
- 项目根目录选择 `./`(包含 `danmu_api` 子目录)。
- 保留默认设置,除非需要特定更改。
- Vercel 会自动检测 `danmu_api/server.js` 和 `danmu_api/vercel.json` 并完成部署。
4. **验证部署**:
部署完成后,Vercel 会提供一个 URL(例如 `https://your-project.vercel.app`)。使用此 URL 测试 API:
- `GET https://your-project.vercel.app/api/v2/search/anime?keyword=Anime%20A`
- `GET https://your-project.vercel.app/api/v2/bangumi/1`
- `GET https://your-project.vercel.app/api/v2/comment/1?withRelated=true&chConvert=1`
- `GET https://your-project.vercel.app/api/logs`
## 项目结构
```
├── danmu_api/
│ ├── server.js # 主 API 服务器代码
│ ├── package.json # 项目依赖和脚本
│ ├── Dockerfile # Docker 配置
│ ├── vercel.json # Vercel 部署配置
│ ├── README.md # 项目文档
```
## 注意事项
- 日志存储在内存中,服务器重启后会清空。
- `/api/logs` 中的 JSON 日志会格式化显示,带缩进以提高可读性。
- 确保 `danmu_api/package.json` 中包含 `express` 依赖。
- 一键部署需要将项目推送到公开的 Git 仓库(如 GitHub),并更新按钮中的仓库地址。
## 许可证
MIT
+14
View File
@@ -0,0 +1,14 @@
{
"name": "danmu-api-server",
"version": "1.0.0",
"description": "A danmu Node.js Express API server",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.21.2"
}
}
+261
View File
@@ -0,0 +1,261 @@
const express = require('express');
const app = express();
const port = 9321;
// 模拟动漫数据
const animes = [
{
animeId: 1,
bangumiId: "bgm001",
animeTitle: "Anime A",
type: "tvseries",
typeDescription: "TV Series",
imageUrl: "https://example.com/anime-a.jpg",
startDate: "2025-01-01T00:00:00.000Z",
episodeCount: 12,
rating: 8.5,
isFavorited: true
},
{
animeId: 2,
bangumiId: "bgm002",
animeTitle: "Anime B",
type: "tvseries",
typeDescription: "TV Series",
imageUrl: "https://example.com/anime-b.jpg",
startDate: "2025-02-01T00:00:00.000Z",
episodeCount: 24,
rating: 7.8,
isFavorited: false
}
];
// 模拟弹幕数据
const comments = [
{ cid: 1, p: "00:01.500,1,25,16777215,1694208000", m: "Great episode!" },
{ cid: 2, p: "00:02.000,1,25,16777215,1694208001", m: "Love this anime!" }
];
// 日志存储,最多保存 500 行
const logBuffer = [];
const MAX_LOGS = 500;
// 格式化日志消息,处理 JSON 字符串
function formatLogMessage(message) {
try {
// 尝试解析消息是否为 JSON
const parsed = JSON.parse(message);
// 如果是 JSON,格式化输出(带缩进)
return JSON.stringify(parsed, null, 2).replace(/\n/g, '\n ');
} catch (e) {
// 不是 JSON,直接返回原消息
return message;
}
}
// 重写 console.log 和 console.error 以捕获日志
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
console.log = (...args) => {
const logMessage = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
const timestamp = new Date().toISOString();
logBuffer.push({ timestamp, level: 'info', message: logMessage });
// 保持日志不超过 MAX_LOGS 行
if (logBuffer.length > MAX_LOGS) {
logBuffer.shift();
}
originalConsoleLog.apply(console, args);
};
console.error = (...args) => {
const logMessage = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
const timestamp = new Date().toISOString();
logBuffer.push({ timestamp, level: 'error', message: logMessage });
// 保持日志不超过 MAX_LOGS 行
if (logBuffer.length > MAX_LOGS) {
logBuffer.shift();
}
originalConsoleError.apply(console, args);
};
// Middleware to parse JSON
app.use(express.json());
// API 1: 搜索动漫
app.get('/api/v2/search/anime', (req, res) => {
const queryTitle = req.query.keyword;
if (!queryTitle) {
console.error({ error: 'Keyword is required', received: req.query });
return res.status(400).json({
errorCode: 400,
success: false,
errorMessage: "Keyword is required",
animes: []
});
}
const filteredAnimes = animes.filter(anime =>
anime.animeTitle.toLowerCase().includes(queryTitle.toLowerCase())
);
console.log(`Search anime with keyword: ${queryTitle}`);
res.json({
errorCode: 0,
success: true,
errorMessage: "",
animes: filteredAnimes
});
});
// API 2: 获取动漫详情
app.get('/api/v2/bangumi/:animeId', (req, res) => {
const animeId = parseInt(req.params.animeId);
const anime = animes.find(a => a.animeId === animeId);
if (!anime) {
console.error(`Anime with ID ${animeId} not found`);
return res.status(404).json({
errorCode: 404,
success: false,
errorMessage: "Anime not found",
bangumi: null
});
}
console.log(`Fetched details for anime ID: ${animeId}`);
res.json({
errorCode: 0,
success: true,
errorMessage: "",
bangumi: {
animeId: anime.animeId,
bangumiId: anime.bangumiId,
animeTitle: anime.animeTitle,
imageUrl: anime.imageUrl,
searchKeyword: anime.animeTitle,
isOnAir: true,
airDay: 1,
isFavorited: anime.isFavorited,
isRestricted: false,
rating: anime.rating,
type: anime.type,
typeDescription: anime.typeDescription,
titles: [
{ language: "en", title: anime.animeTitle },
{ language: "ja", title: anime.animeTitle + " (JP)" }
],
seasons: [
{
id: `season-${anime.animeId}`,
airDate: anime.startDate,
name: "Season 1",
episodeCount: anime.episodeCount,
summary: `Summary for ${anime.animeTitle}`
}
],
episodes: [
{
seasonId: `season-${anime.animeId}`,
episodeId: 1,
episodeTitle: "Episode 1",
episodeNumber: "01",
lastWatched: anime.startDate,
airDate: anime.startDate
}
],
summary: `Summary for ${anime.animeTitle}`,
intro: `Introduction to ${anime.animeTitle}`,
metadata: ["Genre: Action", "Studio: Studio A"],
bangumiUrl: `https://example.com/bangumi/${anime.bangumiId}`,
userRating: anime.rating,
favoriteStatus: anime.isFavorited ? "favorited" : "none",
comment: "",
ratingDetails: {
story: anime.rating,
animation: anime.rating,
music: anime.rating
},
relateds: animes.filter(a => a.animeId !== animeId).map(a => ({
animeId: a.animeId,
bangumiId: a.bangumiId,
animeTitle: a.animeTitle,
imageUrl: a.imageUrl,
searchKeyword: a.animeTitle,
isOnAir: true,
airDay: 1,
isFavorited: a.isFavorited,
isRestricted: false,
rating: a.rating
})),
similars: animes.filter(a => a.animeId !== animeId).map(a => ({
animeId: a.animeId,
bangumiId: a.bangumiId,
animeTitle: a.animeTitle,
imageUrl: a.imageUrl,
searchKeyword: a.animeTitle,
isOnAir: true,
airDay: 1,
isFavorited: a.isFavorited,
isRestricted: false,
rating: a.rating
})),
tags: [
{ id: 1, name: "Action", count: 100 },
{ id: 2, name: "Adventure", count: 80 }
],
onlineDatabases: [
{ name: "Database A", url: "https://example.com/db/a" }
],
trailers: [
{
id: 1,
url: `https://example.com/trailer/${anime.animeId}`,
title: "Trailer 1",
imageUrl: `https://example.com/trailer-${anime.animeId}.jpg`,
date: anime.startDate
}
]
}
});
});
// API 3: 获取弹幕评论
app.get('/api/v2/comment/:commentId', (req, res) => {
const commentId = parseInt(req.params.commentId);
const withRelated = req.query.withRelated === 'true';
const chConvert = req.query.chConvert === '1';
const comment = comments.find(c => c.cid === commentId);
if (!comment) {
console.error(`Comment with ID ${commentId} not found`);
return res.status(404).json({
count: 0,
comments: []
});
}
console.log(`Fetched comment ID: ${commentId}, withRelated: ${withRelated}, chConvert: ${chConvert}`);
let resultComments = [comment];
if (withRelated) {
resultComments = comments; // 返回所有评论作为相关评论
}
if (chConvert) {
resultComments = resultComments.map(c => ({
...c,
m: c.m.toUpperCase() // 模拟字符转换
}));
}
res.json({
count: resultComments.length,
comments: resultComments
});
});
// API 4: 获取最近的日志(最多 500 行,纯文本格式)
app.get('/api/logs', (req, res) => {
const logText = logBuffer
.map(log => `[${log.timestamp}] ${log.level}: ${formatLogMessage(log.message)}`)
.join('\n');
res.set('Content-Type', 'text/plain');
res.send(logText);
});
// 启动服务器
app.listen(port, () => {
console.log(`Server running at http://0.0.0.0:${port}`);
});
+15
View File
@@ -0,0 +1,15 @@
{
"version": 2,
"builds": [
{
"src": "danmu_api/server.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "danmu_api/server.js"
}
]
}