From 7dc0bcff9f1ce5f34a8aaff3208f209179c82703 Mon Sep 17 00:00:00 2001 From: huangxd- <649924757@qq.com> Date: Tue, 9 Sep 2025 22:45:12 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E4=B8=AA=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + README.md | 1 - danmu_api/Dockerfile | 20 ++++ danmu_api/README.md | 120 +++++++++++++++++++ danmu_api/package.json | 14 +++ danmu_api/server.js | 261 +++++++++++++++++++++++++++++++++++++++++ danmu_api/vercel.json | 15 +++ 7 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 .gitignore delete mode 100644 README.md create mode 100644 danmu_api/Dockerfile create mode 100644 danmu_api/README.md create mode 100644 danmu_api/package.json create mode 100644 danmu_api/server.js create mode 100644 danmu_api/vercel.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81190df --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +danmu_api/package-lock.json +danmu_api/node_modules \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 22dc12f..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# danmu_api \ No newline at end of file diff --git a/danmu_api/Dockerfile b/danmu_api/Dockerfile new file mode 100644 index 0000000..b088d3f --- /dev/null +++ b/danmu_api/Dockerfile @@ -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"] \ No newline at end of file diff --git a/danmu_api/README.md b/danmu_api/README.md new file mode 100644 index 0000000..d08329e --- /dev/null +++ b/danmu_api/README.md @@ -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 \ No newline at end of file diff --git a/danmu_api/package.json b/danmu_api/package.json new file mode 100644 index 0000000..a2cf57d --- /dev/null +++ b/danmu_api/package.json @@ -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" + } +} diff --git a/danmu_api/server.js b/danmu_api/server.js new file mode 100644 index 0000000..305754d --- /dev/null +++ b/danmu_api/server.js @@ -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}`); +}); \ No newline at end of file diff --git a/danmu_api/vercel.json b/danmu_api/vercel.json new file mode 100644 index 0000000..96a67e8 --- /dev/null +++ b/danmu_api/vercel.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "builds": [ + { + "src": "danmu_api/server.js", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "danmu_api/server.js" + } + ] +} \ No newline at end of file