From a4c2af4629fd92b652c06551a8613f449256c806 Mon Sep 17 00:00:00 2001
From: huangxd- <649924757@qq.com>
Date: Mon, 15 Sep 2025 23:45:45 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=BE=83=E7=AE=80=E5=8D=95?=
=?UTF-8?q?=E7=9A=84match=E6=8E=A5=E5=8F=A3=EF=BC=8C=E7=9B=AE=E5=89=8D?=
=?UTF-8?q?=E5=B7=B2=E6=94=AF=E6=8C=81=E8=87=AA=E5=8A=A8=E5=8C=B9=E9=85=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 33 ++++++++--
danmu_api/worker.js | 131 ++++++++++++++++++++++++++++++++++++---
danmu_api/worker.test.js | 82 ++++++++++++++++--------
edgeone.json | 25 ++++++++
vercel.json | 5 +-
5 files changed, 234 insertions(+), 42 deletions(-)
create mode 100644 edgeone.json
diff --git a/README.md b/README.md
index d80a6a7..39c2909 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
本项目仅为个人爱好开发,代码开源。如有任何侵权行为,请联系本人删除。
-纯为爱发电,自用顺便分享,如果真要打赏,可以支付宝发口令红包到我的tg私信机器人 https://t.me/ddjdd_bot ,感谢😊,有问题提issue或私信机器人都ok。
+有问题提issue或私信机器人都ok。https://t.me/ddjdd_bot
## 功能
- **API 接口**:
@@ -68,7 +68,7 @@
3. **测试 API**:
使用 `http://{ip}:9321/{TOKEN}` 访问上述 API 接口。
-## Docker 一键启动
+## Docker 一键启动 【推荐】
1. **拉取镜像**:
```bash
docker pull logvar/danmu-api:latest
@@ -80,10 +80,23 @@
```
- 使用`-e TOKEN=your_token_here`设置`TOKEN`环境变量。
+ ```yaml
+ services:
+ danmu-api:
+ image: logvar/danmu-api:latest
+ container_name: danmu-api
+ ports:
+ - "9321:9321"
+ environment:
+ - TOKEN=your_token_here # 请将your_token_here 替换为实际的 Token 值
+ restart: unless-stopped # 可选配置,容器退出时自动重启(非必需,可根据需求删除)
+ ```
+ - 或使用docker compose部署。
+
3. **测试 API**:
使用 `http://{ip}:9321/{TOKEN}` 访问上述 API 接口。
-## 部署到 Vercel
+## 部署到 Vercel 【推荐】
### 一键部署
点击以下按钮即可将项目快速部署到 Vercel:
@@ -95,6 +108,10 @@
1. 转到你的项目设置。
2. 在“Environment Variables”部分添加 `TOKEN` 变量,输入你的 API 令牌值。
3. 保存更改并重新部署。
+- 示例请求:`https://{your_domian}.vercel.app/87654321/api/v2/search/anime?keyword=子夜归`
+
+### 优化点
+Settings > Functions > Advanced Setting > Function Region 切换为 Hong Kong,能提高访问速度,体验更优
## 部署到 Cloudflare
@@ -109,6 +126,7 @@
2. 转到“Settings” > “Variables”。
3. 添加 `TOKEN` 环境变量,输入你的 API 令牌值。
4. 保存并部署。
+- 示例请求:`https://{your_domian}.workers.dev/87654321/api/v2/search/anime?keyword=子夜归`
### 手动部署
创建一个worker,将`danmu_api/worker.js`里的代码直接拷贝到你创建的`worker.js`里,然后点击部署。
@@ -126,6 +144,13 @@
+## 环境变量列表
+| 变量名称 | 描述 |
+| ----------- | ----------- |
+| TOKEN | 用户token |
+| OTHER_SERVER | 兜底第三方弹幕服务器,如 https://api.danmu.icu |
+| VOD_SERVER | vod查询站点,如 https://www.caiji.cyou |
+
## 项目结构
```
danmu_api/
@@ -133,7 +158,6 @@ danmu_api/
│ └── workflows/
│ └── docker-image.yml
├── danmu_api/
-│ ├── README.md # 项目文档
│ ├── server.js # 本地node启动脚本
│ ├── worker.js # 主 API 服务器代码
│ ├── worker.test.js # 测试文件
@@ -141,6 +165,7 @@ danmu_api/
├── Dockerfile
├── package.json
├── vercel.json
+├── README.md
```
## 注意事项
diff --git a/danmu_api/worker.js b/danmu_api/worker.js
index 20dcb3d..f95e6f3 100644
--- a/danmu_api/worker.js
+++ b/danmu_api/worker.js
@@ -1,6 +1,6 @@
// 全局状态(Cloudflare 和 Vercel 都可能重用实例)
// ⚠️ 不是持久化存储,每次冷启动会丢失
-const VERSION = "1.0.0";
+const VERSION = "1.0.1";
let animes = [];
let episodeIds = [];
let episodeNum = 10001; // 全局变量,用于自增 ID
@@ -2441,6 +2441,107 @@ async function searchAnime(url) {
});
}
+// Extracted function for POST /api/v2/match
+async function matchAnime(url, req) {
+ try {
+ // 获取请求体
+ const body = await req.json();
+
+ // 验证请求体是否有效
+ if (!body) {
+ log("error", "Request body is empty");
+ return jsonResponse(
+ { errorCode: 400, success: false, errorMessage: "Empty request body" },
+ 400
+ );
+ }
+
+ // 处理请求体中的数据
+ // 假设请求体包含一个字段,比如 { query: "anime name" }
+ const { fileName } = body;
+ if (!fileName) {
+ log("error", "Missing fileName parameter in request body");
+ return jsonResponse(
+ { errorCode: 400, success: false, errorMessage: "Missing fileName parameter" },
+ 400
+ );
+ }
+
+ // 这里可以继续处理 query,比如调用其他服务或数据库查询
+ log("info", `Processing anime match for query: ${fileName}`);
+
+ const regex = /^(.+?)\s+S(\d+)E(\d+)$/;
+ const match = fileName.match(regex);
+
+ let title = match ? match[1] : fileName;
+ let season = match ? parseInt(match[2]) : null;
+ let episode = match ? parseInt(match[3]) : null;
+
+ log("info", "Parsed title, season, episode", { title, season, episode });
+
+ let originSearchUrl = new URL(req.url.replace("/match", `/search/anime?keyword=${title}`));
+ const searchRes = await searchAnime(originSearchUrl);
+ const searchData = await searchRes.json();
+ log("info", `searchData: ${searchData.animes}`);
+
+ let resAnime;
+ let resEpisode;
+ for (const anime of searchData.animes) {
+ if (anime.animeTitle.includes(title)) {
+ let originBangumiUrl = new URL(req.url.replace("/match", `bangumi/${anime.bangumiId}`));
+ const bangumiRes = await getBangumi(originBangumiUrl.pathname);
+ const bangumiData = await bangumiRes.json();
+ log("info", bangumiData);
+ if (season && episode && bangumiData.bangumi.episodes.length >= episode) {
+ resEpisode = bangumiData.bangumi.episodes[episode-1];
+ resAnime = anime;
+ break;
+ } else if (bangumiData.bangumi.episodes.length > 0) {
+ resEpisode = bangumiData.bangumi.episodes[0];
+ resAnime = anime;
+ break;
+ }
+ }
+ }
+
+ let resData = {
+ "errorCode": 0,
+ "success": true,
+ "errorMessage": "",
+ "isMatched": false,
+ "matches": []
+ };
+
+ if (resEpisode) {
+ resData["isMatched"] = true;
+ resData["matches"] = [
+ {
+ "episodeId": resEpisode.episodeId,
+ "animeId": resAnime.animeId,
+ "animeTitle": resAnime.animeTitle,
+ "episodeTitle": resEpisode.episodeTitle,
+ "type": resAnime.type,
+ "typeDescription": resAnime.typeDescription,
+ "shift": 0,
+ "imageUrl": resAnime.imageUrl
+ }
+ ]
+ }
+
+ log("info", `resMatchData: ${resData}`);
+
+ // 示例返回
+ return jsonResponse(resData);
+ } catch (error) {
+ // 处理 JSON 解析错误或其他异常
+ log("error", `Failed to parse request body: ${error.message}`);
+ return jsonResponse(
+ { errorCode: 400, success: false, errorMessage: "Invalid JSON body" },
+ 400
+ );
+ }
+}
+
// Extracted function for GET /api/v2/bangumi/:animeId
async function getBangumi(path) {
const animeId = parseInt(path.split("/").pop());
@@ -2552,25 +2653,29 @@ async function handleRequest(req, env) {
let path = url.pathname;
const method = req.method;
- // GET /
- if (path === "/" && method === "GET") {
+ function handleHomepage() {
log("log", "Accessed homepage with repository information");
return jsonResponse({
message: "Welcome to the LogVar Danmu API server",
version: VERSION,
repository: "https://github.com/huangxd-/danmu_api.git",
description: "一个人人都能部署的基于 js 的弹幕 API 服务器,支持爱优腾芒哔人弹幕直接获取,兼容弹弹play的搜索、详情查询和弹幕获取接口,并提供日志记录,支持vercel/cloudflare/docker/claw等部署方式,不用提前下载弹幕,没有nas或小鸡也能一键部署。",
- notice: "本项目仅为个人爱好开发,代码开源。如有任何侵权行为,请联系本人删除。纯为爱发电,自用顺便分享,如果真要打赏,可以支付宝发口令红包到我的tg私信机器人 https://t.me/ddjdd_bot ,感谢😊,有问题提issue或私信机器人都ok。"
+ notice: "本项目仅为个人爱好开发,代码开源。如有任何侵权行为,请联系本人删除。有问题提issue或私信机器人都ok。https://t.me/ddjdd_bot"
});
}
+ // GET /
+ if (path === "/" && method === "GET") {
+ return handleHomepage();
+ }
+
if (path === "/favicon.ico" || path === "/robots.txt") {
return new Response(null, { status: 204 });
}
// --- 校验 token ---
const parts = path.split("/").filter(Boolean); // 去掉空段
- if (parts.length < 2 || parts[0] !== token) {
+ if (parts.length < 1 || parts[0] !== token) {
log("error", `Invalid or missing token in path: ${path}`);
return jsonResponse(
{ errorCode: 401, success: false, errorMessage: "Unauthorized" },
@@ -2580,11 +2685,23 @@ async function handleRequest(req, env) {
// 移除 token 部分,剩下的才是真正的路径
path = "/" + parts.slice(1).join("/");
+ console.log(path);
+
+ // GET /
+ if (path === "/" && method === "GET") {
+ return handleHomepage();
+ }
+
// GET /api/v2/search/anime
if (path === "/api/v2/search/anime" && method === "GET") {
return searchAnime(url);
}
+ // GET /api/v2/search/anime
+ if (path === "/api/v2/match" && method === "POST") {
+ return matchAnime(url, req);
+ }
+
// GET /api/v2/bangumi/:animeId
if (path.startsWith("/api/v2/bangumi/") && method === "GET") {
return getBangumi(path);
@@ -2636,5 +2753,5 @@ export async function vercelHandler(req, res) {
}
// 为了测试导出 handleRequest
-export { handleRequest, searchAnime, getBangumi, getComment, fetchTencentVideo, fetchIqiyi, fetchMangoTV,
- fetchBilibili, fetchYouku, fetchOtherServer };
\ No newline at end of file
+export { handleRequest, searchAnime, matchAnime, getBangumi, getComment, fetchTencentVideo, fetchIqiyi, fetchMangoTV,
+ fetchBilibili, fetchYouku, fetchOtherServer, httpGet, httpPost };
\ No newline at end of file
diff --git a/danmu_api/worker.test.js b/danmu_api/worker.test.js
index 04199dc..2fd3f6f 100644
--- a/danmu_api/worker.test.js
+++ b/danmu_api/worker.test.js
@@ -1,7 +1,7 @@
const test = require('node:test');
const assert = require('node:assert').strict;
-const { handleRequest, searchAnime, getBangumi, getComment, fetchTencentVideo, fetchIqiyi, fetchMangoTV,
- fetchBilibili, fetchYouku, fetchOtherServer} = require('./worker');
+const { handleRequest, searchAnime, matchAnime, getBangumi, getComment, fetchTencentVideo, fetchIqiyi, fetchMangoTV,
+ fetchBilibili, fetchYouku, fetchOtherServer, httpGet, httpPost } = require('./worker');
// Mock Request class for testing
class MockRequest {
@@ -9,6 +9,7 @@ class MockRequest {
this.url = url;
this.method = options.method || 'GET';
this.headers = new Map(Object.entries(options.headers || {}));
+ this.json = options.body ? async () => options.body : undefined; // 模拟 POST 请求的 body
}
}
@@ -66,33 +67,60 @@ test('worker.js API endpoints', async (t) => {
// assert(res.length > 2, `Expected res.length > 2, but got ${res.length}`);
// });
- await t.test('GET realistic danmu', async () => {
- // tencent
- // const keyword = "子夜归";
- // iqiyi
- // const keyword = "赴山海";
- // mango
- // const keyword = "锦月如歌";
- // bilibili
- // const keyword = "国王排名";
- // youku
- // const keyword = "黑白局";
- // renren
- const keyword = "瑞克和莫蒂";
+ // await t.test('GET realistic danmu', async () => {
+ // // tencent
+ // // const keyword = "子夜归";
+ // // iqiyi
+ // // const keyword = "赴山海";
+ // // mango
+ // // const keyword = "锦月如歌";
+ // // bilibili
+ // // const keyword = "国王排名";
+ // // youku
+ // // const keyword = "黑白局";
+ // // renren
+ // const keyword = "瑞克和莫蒂";
+ //
+ // const searchUrl = new URL(`${urlPrefix}/${token}/api/v2/search/anime?keyword=${keyword}`);
+ // const searchRes = await searchAnime(searchUrl);
+ // const searchData = await searchRes.json();
+ // assert(searchData.animes.length > 0, `Expected searchData.animes.length > 0, but got ${searchData.animes.length}`);
+ //
+ // const bangumiUrl = new URL(`${urlPrefix}/${token}/api/v2/bangumi/${searchData.animes[0].animeId}`);
+ // const bangumiRes = await getBangumi(bangumiUrl.pathname);
+ // const bangumiData = await bangumiRes.json();
+ // assert(bangumiData.bangumi.episodes.length > 0, `Expected bangumiData.bangumi.episodes.length > 0, but got ${bangumiData.bangumi.episodes.length}`);
+ //
+ // const commentUrl = new URL(`${urlPrefix}/${token}/api/v2/comment/${bangumiData.bangumi.episodes[0].episodeId}?withRelated=true&chConvert=1`);
+ // const commentRes = await getComment(commentUrl.pathname);
+ // const commentData = await commentRes.json();
+ // assert(commentData.count > 0, `Expected commentData.count > 0, but got ${commentData.count}`);
+ // });
- const searchUrl = new URL(`${urlPrefix}/${token}/api/v2/search/anime?keyword=${keyword}`);
- const searchRes = await searchAnime(searchUrl);
- const searchData = await searchRes.json();
- assert(searchData.animes.length > 0, `Expected searchData.animes.length > 0, but got ${searchData.animes.length}`);
+ // 测试 POST /api/v2/match 接口
+ await t.test('POST /api/v2/match for matching anime', async () => {
+ // 构造请求体
+ const requestBody = {
+ "fileName": "生万物 S01E28",
+ "fileHash": "1234567890",
+ "fileSize": 0,
+ "videoDuration": 0,
+ "matchMode": "fileNameOnly"
+ };
- const bangumiUrl = new URL(`${urlPrefix}/${token}/api/v2/bangumi/${searchData.animes[0].animeId}`);
- const bangumiRes = await getBangumi(bangumiUrl.pathname);
- const bangumiData = await bangumiRes.json();
- assert(bangumiData.bangumi.episodes.length > 0, `Expected bangumiData.bangumi.episodes.length > 0, but got ${bangumiData.bangumi.episodes.length}`);
+ // 模拟 POST 请求
+ const matchUrl = `${urlPrefix}/${token}/api/v2/match`; // 注意路径与 handleRequest 中匹配
+ const req = new MockRequest(matchUrl, { method: 'POST', body: requestBody });
- const commentUrl = new URL(`${urlPrefix}/${token}/api/v2/comment/${bangumiData.bangumi.episodes[0].episodeId}?withRelated=true&chConvert=1`);
- const commentRes = await getComment(commentUrl.pathname);
- const commentData = await commentRes.json();
- assert(commentData.count > 0, `Expected commentData.count > 0, but got ${commentData.count}`);
+ // 调用 handleRequest 来处理 POST 请求
+ const res = await handleRequest(req);
+
+ // 解析响应
+ const responseBody = await parseResponse(res);
+ console.log(responseBody);
+
+ // 验证响应状态
+ assert.equal(res.status, 200);
+ assert.deepEqual(responseBody.success, true);
});
});
\ No newline at end of file
diff --git a/edgeone.json b/edgeone.json
new file mode 100644
index 0000000..9878e66
--- /dev/null
+++ b/edgeone.json
@@ -0,0 +1,25 @@
+{
+ "version": 1,
+ "routes": [
+ {
+ "pattern": "/",
+ "function": "index"
+ },
+ {
+ "pattern": "/api/*",
+ "function": "index"
+ },
+ {
+ "pattern": "/*/api/*",
+ "function": "index"
+ }
+ ],
+ "functions": {
+ "index": {
+ "src": "danmu_api/worker.js"
+ }
+ },
+ "env": {
+ "TOKEN": "87654321"
+ }
+}
\ No newline at end of file
diff --git a/vercel.json b/vercel.json
index 3064c23..69cfd87 100644
--- a/vercel.json
+++ b/vercel.json
@@ -19,8 +19,5 @@
"src": "/([^/]+)/api/(.*)",
"dest": "/danmu_api/worker.js"
}
- ],
- "env": {
- "TOKEN": "87654321"
- }
+ ]
}
\ No newline at end of file