增加较简单的match接口,目前已支持自动匹配
This commit is contained in:
1 parent
aab9b692b0
commit
a4c2af4629
5 files changed
+234
-42
No files matched your search
@@ -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 @@
|
||||
<img src="https://i.mji.rip/2025/09/14/9fdf945fb247994518042691f60d7849.jpeg" style="width:400px" />
|
||||
<img src="https://i.mji.rip/2025/09/14/dbacc0cf9c8a839f16b8960de1f38f11.jpeg" style="width:400px" />
|
||||
|
||||
## 环境变量列表
|
||||
| 变量名称 | 描述 |
|
||||
| ----------- | ----------- |
|
||||
| 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
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
+124
-7
@@ -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 };
|
||||
export { handleRequest, searchAnime, matchAnime, getBangumi, getComment, fetchTencentVideo, fetchIqiyi, fetchMangoTV,
|
||||
fetchBilibili, fetchYouku, fetchOtherServer, httpGet, httpPost };
|
||||
+55
-27
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -19,8 +19,5 @@
|
||||
"src": "/([^/]+)/api/(.*)",
|
||||
"dest": "/danmu_api/worker.js"
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
"TOKEN": "87654321"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user