适配cloudflare部署
This commit is contained in:
1 parent
842297f58e
commit
efc85aee97
5 files changed
+238
-267
No files matched your search
+1
-1
@@ -14,7 +14,7 @@ RUN npm install
|
||||
COPY danmu_api/ .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000
|
||||
EXPOSE 9321
|
||||
|
||||
# 启动命令
|
||||
CMD ["node", "server.js"]
|
||||
+38
-262
@@ -1,271 +1,47 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const port = 9321;
|
||||
const http = require('http');
|
||||
const fetch = require('node-fetch'); // 使用 node-fetch 替换 undici
|
||||
const { Request, Response } = fetch;
|
||||
const worker = require('./worker.js');
|
||||
|
||||
// 模拟动漫数据
|
||||
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 server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
// Construct the full URL
|
||||
const fullUrl = `http://${req.headers.host}${req.url}`;
|
||||
|
||||
// Convert Node.js request body to a string for POST/PUT requests
|
||||
let body;
|
||||
if (req.method === 'POST' || req.method === 'PUT') {
|
||||
body = await new Promise((resolve) => {
|
||||
let data = '';
|
||||
req.on('data', chunk => data += chunk);
|
||||
req.on('end', () => resolve(data));
|
||||
});
|
||||
}
|
||||
];
|
||||
|
||||
// 模拟弹幕数据
|
||||
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());
|
||||
|
||||
// 首页路由:返回开源仓库说明
|
||||
app.get('/', (req, res) => {
|
||||
console.log('Accessed homepage with repository information');
|
||||
res.json({
|
||||
message: "Welcome to the Danmu API server",
|
||||
repository: "https://github.com/huangxd-/danmu_api.git",
|
||||
notice: "本项目仅为个人爱好开发,代码开源。如有任何侵权行为,请联系本人删除。"
|
||||
// Create a Web API-compatible Request object
|
||||
const webRequest = new Request(fullUrl, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: body || undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// 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
|
||||
// Call the worker's fetch handler
|
||||
const webResponse = await worker.default.fetch(webRequest, {}, {});
|
||||
|
||||
// Convert Web API Response to Node.js response
|
||||
res.statusCode = webResponse.status;
|
||||
webResponse.headers.forEach((value, key) => {
|
||||
res.setHeader(key, value);
|
||||
});
|
||||
const responseText = await webResponse.text();
|
||||
res.end(responseText);
|
||||
} catch (error) {
|
||||
console.error('Server error:', error);
|
||||
res.statusCode = 500;
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
// 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}`);
|
||||
server.listen(9321, '0.0.0.0', () => {
|
||||
console.log('Server running on http://0.0.0.0:9321');
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
// 全局状态(Cloudflare 和 Vercel 都可能重用实例)
|
||||
// ⚠️ 不是持久化存储,每次冷启动会丢失
|
||||
let 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;
|
||||
|
||||
function log(level, ...args) {
|
||||
const message = args
|
||||
.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : arg))
|
||||
.join(" ");
|
||||
const timestamp = new Date().toISOString();
|
||||
logBuffer.push({ timestamp, level, message });
|
||||
if (logBuffer.length > MAX_LOGS) logBuffer.shift();
|
||||
console[level](...args);
|
||||
}
|
||||
|
||||
function formatLogMessage(message) {
|
||||
try {
|
||||
const parsed = JSON.parse(message);
|
||||
return JSON.stringify(parsed, null, 2).replace(/\n/g, "\n ");
|
||||
} catch {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRequest(req) {
|
||||
const url = new URL(req.url);
|
||||
const path = url.pathname;
|
||||
const method = req.method;
|
||||
|
||||
// GET /api/users
|
||||
if (path === "/api/v2/search/anime" && method === "GET") {
|
||||
const queryTitle = url.searchParams.get("keyword");
|
||||
if (!queryTitle) {
|
||||
log("error", { error: "Keyword is required", received: url.searchParams });
|
||||
return jsonResponse(
|
||||
{
|
||||
errorCode: 400,
|
||||
success: false,
|
||||
errorMessage: "Keyword is required",
|
||||
animes: [],
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
const filteredAnimes = animes.filter((anime) =>
|
||||
anime.animeTitle.toLowerCase().includes(queryTitle.toLowerCase())
|
||||
);
|
||||
log("log", `Search anime with keyword: ${queryTitle}`);
|
||||
return jsonResponse({
|
||||
errorCode: 0,
|
||||
success: true,
|
||||
errorMessage: "",
|
||||
animes: filteredAnimes,
|
||||
});
|
||||
}
|
||||
|
||||
// GET /api/v2/bangumi/:animeId
|
||||
if (path.startsWith("/api/v2/bangumi/") && method === "GET") {
|
||||
const animeId = parseInt(path.split("/").pop());
|
||||
const anime = animes.find((a) => a.animeId === animeId);
|
||||
if (!anime) {
|
||||
log("error", `Anime with ID ${animeId} not found`);
|
||||
return jsonResponse(
|
||||
{ errorCode: 404, success: false, errorMessage: "Anime not found", bangumi: null },
|
||||
404
|
||||
);
|
||||
}
|
||||
log("log", `Fetched details for anime ID: ${animeId}`);
|
||||
return jsonResponse({
|
||||
errorCode: 0,
|
||||
success: true,
|
||||
errorMessage: "",
|
||||
bangumi: {
|
||||
animeId: anime.animeId,
|
||||
bangumiId: anime.bangumiId,
|
||||
animeTitle: anime.animeTitle,
|
||||
imageUrl: anime.imageUrl,
|
||||
isOnAir: true,
|
||||
airDay: 1,
|
||||
isFavorited: anime.isFavorited,
|
||||
rating: anime.rating,
|
||||
type: anime.type,
|
||||
typeDescription: anime.typeDescription,
|
||||
seasons: [
|
||||
{
|
||||
id: `season-${anime.animeId}`,
|
||||
airDate: anime.startDate,
|
||||
name: "Season 1",
|
||||
episodeCount: anime.episodeCount,
|
||||
},
|
||||
],
|
||||
episodes: [
|
||||
{
|
||||
seasonId: `season-${anime.animeId}`,
|
||||
episodeId: 1,
|
||||
episodeTitle: "Episode 1",
|
||||
episodeNumber: "01",
|
||||
airDate: anime.startDate,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// GET /api/v2/comment/:commentId
|
||||
if (path.startsWith("/api/v2/comment/") && method === "GET") {
|
||||
const commentId = parseInt(path.split("/").pop());
|
||||
const comment = comments.find((c) => c.cid === commentId);
|
||||
if (!comment) {
|
||||
log("error", `Comment with ID ${commentId} not found`);
|
||||
return jsonResponse({ count: 0, comments: [] }, 404);
|
||||
}
|
||||
log("log", `Fetched comment ID: ${commentId}`);
|
||||
return jsonResponse({ count: 1, comments: [comment] });
|
||||
}
|
||||
|
||||
// GET /api/logs
|
||||
if (path === "/api/logs" && method === "GET") {
|
||||
const logText = logBuffer
|
||||
.map(
|
||||
(log) =>
|
||||
`[${log.timestamp}] ${log.level}: ${formatLogMessage(log.message)}`
|
||||
)
|
||||
.join("\n");
|
||||
return new Response(logText, { headers: { "Content-Type": "text/plain" } });
|
||||
}
|
||||
|
||||
return jsonResponse({ message: "Not found" }, 404);
|
||||
}
|
||||
|
||||
function jsonResponse(data, status = 200) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// --- Cloudflare Workers 入口 ---
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return handleRequest(request);
|
||||
},
|
||||
};
|
||||
|
||||
// --- Vercel 入口 ---
|
||||
export async function vercelHandler(req, res) {
|
||||
const cfReq = new Request(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body:
|
||||
req.method === "POST" || req.method === "PUT"
|
||||
? JSON.stringify(req.body)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const response = await handleRequest(cfReq);
|
||||
|
||||
res.status(response.status);
|
||||
response.headers.forEach((value, key) => res.setHeader(key, value));
|
||||
const text = await response.text();
|
||||
res.send(text);
|
||||
}
|
||||
+1
-1
@@ -9,6 +9,6 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.21.2"
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -2,14 +2,14 @@
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "danmu_api/server.js",
|
||||
"src": "danmu_api/worker.js",
|
||||
"use": "@vercel/node"
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "danmu_api/server.js"
|
||||
"src": "/api/(.*)",
|
||||
"dest": "/danmu_api/worker.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user