feat: 后端改为deno

This commit is contained in:
ethan.chen
2025-11-05 11:43:47 +08:00
parent ba3435e1a0
commit f67a3835fa
27 changed files with 1449 additions and 1404 deletions

28
backend-deno/config.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* 配置文件 (Deno 版本)
*/
export const config = {
// 服务器配置
port: parseInt(Deno.env.get("PORT") || "5000"),
host: "127.0.0.1",
// 歌词配置
lyrics: {
cacheDir: "./lyrics",
apiUrl: "http://123.57.93.143:28883",
apiKey: "fzt_tom",
},
// WebSocket 配置
websocket: {
heartbeatInterval: 30000, // 30秒
maxConnections: 100,
},
// 音乐控制配置
music: {
updateInterval: 1000, // 1秒
retryAttempts: 3,
},
};

12
backend-deno/deno.json Normal file
View File

@@ -0,0 +1,12 @@
{
"imports": {
"hono": "https://deno.land/x/hono@v3.12.8/mod.ts",
"ws": "https://deno.land/std@0.208.0/ws/mod.ts",
"cors": "https://deno.land/x/hono@v3.12.8/middleware/cors/index.ts",
"logger": "https://deno.land/x/hono@v3.12.8/middleware/logger/index.ts"
},
"tasks": {
"start": "deno run --allow-net --allow-run --allow-read --allow-write --allow-env main.ts",
"dev": "deno run --allow-net --allow-run --allow-read --allow-write --watch --allow-env main.ts"
}
}

42
backend-deno/deno.lock generated Normal file
View File

@@ -0,0 +1,42 @@
{
"version": "5",
"remote": {
"https://deno.land/x/hono@v3.12.8/client/client.ts": "9b3bddfd400d069d58320c3f475447d5e61f2c343adad402d5d36fa17f00b481",
"https://deno.land/x/hono@v3.12.8/client/index.ts": "30def535310a37bede261f1b23d11a9758983b8e9d60a6c56309cee5f6746ab2",
"https://deno.land/x/hono@v3.12.8/client/utils.ts": "053273c002963b549d38268a1b423ac8ca211a8028bdab1ed0b781a62aa5e661",
"https://deno.land/x/hono@v3.12.8/compose.ts": "37d6e33b7db80e4c43a0629b12fd3a1e3406e7d9e62a4bfad4b30426ea7ae4f1",
"https://deno.land/x/hono@v3.12.8/context.ts": "bb3309ea57fa617714a16a99ab4da02ee4e36419770da6cb0f581fbbef9f541d",
"https://deno.land/x/hono@v3.12.8/helper/cookie/index.ts": "a05ce7e3bafe1f8c7f45d04abf79822716011be75904fe1aad2e99126fd985b9",
"https://deno.land/x/hono@v3.12.8/helper/html/index.ts": "701ed12b808e8c253247edc02c2c4b32261ae899e9c98f1427f7f6eaa8b7d1ce",
"https://deno.land/x/hono@v3.12.8/hono-base.ts": "9a0d2afd56c48eccf93cbe6fd6bb72b26ce4281f279a98f6fd76010dcd4001bf",
"https://deno.land/x/hono@v3.12.8/hono.ts": "2cc4c292e541463a4d6f83edbcea58048d203e9564ae62ec430a3d466b49a865",
"https://deno.land/x/hono@v3.12.8/http-exception.ts": "6071df078b5f76d279684d52fe82a590f447a64ffe1b75eb5064d0c8a8d2d676",
"https://deno.land/x/hono@v3.12.8/middleware/cors/index.ts": "69e208e2bfd6345f14892e59ea817818373b3ebd74163177d687cd47ddbe1f6c",
"https://deno.land/x/hono@v3.12.8/middleware/logger/index.ts": "4baf9217b61f5e9e937c3e4e6cd87508c83603fcee77c33edba0a6ae2cc41ccd",
"https://deno.land/x/hono@v3.12.8/mod.ts": "90114a97be9111b348129ad0143e764a64921f60dd03b8f3da529db98a0d3a82",
"https://deno.land/x/hono@v3.12.8/request.ts": "5c2ab4b9961615afdc87e950ce16cc57e98f7849da56ea5fc1a828ef0dbb4aaf",
"https://deno.land/x/hono@v3.12.8/router.ts": "880316f561918fc197481755aac2165fdbe2f530b925c5357a9f98d6e2cc85c7",
"https://deno.land/x/hono@v3.12.8/router/linear-router/index.ts": "8a2a7144c50b1f4a92d9ee99c2c396716af144c868e10608255f969695efccd0",
"https://deno.land/x/hono@v3.12.8/router/linear-router/router.ts": "1366b3fada26e33c96b0f228aa7962b9c9801c4f484b3991b256aed2c005c668",
"https://deno.land/x/hono@v3.12.8/router/pattern-router/index.ts": "304a66c50e243872037ed41c7dd79ed0c89d815e17e172e7ad7cdc4bc07d3383",
"https://deno.land/x/hono@v3.12.8/router/pattern-router/router.ts": "a9a5a2a182cce8c3ae82139892cc0502be7dd8f579f31e76d0302b19b338e548",
"https://deno.land/x/hono@v3.12.8/router/reg-exp-router/index.ts": "52755829213941756159b7a963097bafde5cc4fc22b13b1c7c9184dc0512d1db",
"https://deno.land/x/hono@v3.12.8/router/reg-exp-router/node.ts": "5b3fb80411db04c65df066e69fedb2c8c0844753c2633d703336de84d569252c",
"https://deno.land/x/hono@v3.12.8/router/reg-exp-router/router.ts": "c89a37b14c64518256d2fc216d9db7645e658235648762c3ad6e3fc6872a941c",
"https://deno.land/x/hono@v3.12.8/router/reg-exp-router/trie.ts": "852ce7207e6701e47fa30889a0d2b8bfcd56d8862c97e7bc9831e0a64bd8835f",
"https://deno.land/x/hono@v3.12.8/router/smart-router/index.ts": "74f9b4fe15ea535900f2b9b048581915f12fe94e531dd2b0032f5610e61c3bef",
"https://deno.land/x/hono@v3.12.8/router/smart-router/router.ts": "f1848a2a1eefa316a11853ae12e749552747771fb8a11fe713ae04ea6461140b",
"https://deno.land/x/hono@v3.12.8/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41",
"https://deno.land/x/hono@v3.12.8/router/trie-router/node.ts": "326830a78f3ba3f8a7171041c71d541b10fba2fb398067b09bcd35251bb9679f",
"https://deno.land/x/hono@v3.12.8/router/trie-router/router.ts": "54ced78d35676302c8fcdda4204f7bdf5a7cc907fbf9967c75674b1e394f830d",
"https://deno.land/x/hono@v3.12.8/utils/body.ts": "fe92c854fa0d1b36d1de3351a0041d06bd56c24a18778ae94511ccd9f806c0a2",
"https://deno.land/x/hono@v3.12.8/utils/buffer.ts": "9066a973e64498cb262c7e932f47eed525a51677b17f90893862b7279dc0773e",
"https://deno.land/x/hono@v3.12.8/utils/cookie.ts": "19920ba6756944aae1ad8585c3ddeaa9df479733f59d05359db096f7361e5e4b",
"https://deno.land/x/hono@v3.12.8/utils/crypto.ts": "bda0e141bbe46d3a4a20f8fbcb6380d473b617123d9fdfa93e4499410b537acc",
"https://deno.land/x/hono@v3.12.8/utils/html.ts": "e800e72e2940104e963707edb41a438937693dda8d21bd37a57620e35d36c647",
"https://deno.land/x/hono@v3.12.8/utils/stream.ts": "fe5f539a79c476e6af39e2549fa06054ad3c7ff3cbfa2c4c752e879cdd3365d5",
"https://deno.land/x/hono@v3.12.8/utils/url.ts": "71475d787e9919443837e0674bc623c2a683d0a7fc41fc1938ff47e1aa775de4",
"https://deno.land/x/hono@v3.12.8/validator/index.ts": "6c986e8b91dcf857ecc8164a506ae8eea8665792a4ff7215471df669c632ae7c",
"https://deno.land/x/hono@v3.12.8/validator/validator.ts": "97fead75b1e3e460d0895d68eab2be6d774f6d1428e2efb8b4df8ba8bb422c3e"
}
}

79
backend-deno/main.ts Normal file
View File

@@ -0,0 +1,79 @@
/**
* lyroc 后端主程序 (Deno + Hono 版本)
* 负责初始化应用和启动服务器
*/
import { Hono } from "hono";
import { ConnectionManager } from "./modules/websocket_manager.ts";
import {
startBackgroundTask,
stopBackgroundTask,
} from "./modules/background_tasks.ts";
import { registerRoutes } from "./modules/routes.ts";
import { initDB } from "./modules/lyrics.ts";
// 创建 Hono 应用
const app = new Hono();
// 创建 WebSocket 连接管理器
const manager = new ConnectionManager();
// 注册路由
registerRoutes(app, manager);
// 启动时初始化
async function startup() {
console.log("正在启动 API 服务...");
try {
// 初始化数据库
await initDB();
console.log("数据库初始化完成");
// 启动后台任务
startBackgroundTask(manager);
console.log("后台任务已启动");
} catch (error) {
console.error(`启动失败: ${error}`);
}
}
// 关闭时清理
function shutdown() {
console.log("正在关闭服务...");
try {
// 停止后台任务
stopBackgroundTask();
console.log("后台任务已停止");
// 关闭所有 WebSocket 连接
manager.closeAllConnections();
console.log("所有连接已关闭");
} catch (error) {
console.error(`关闭时出错: ${error}`);
}
}
// 处理进程信号
Deno.addSignalListener("SIGINT", () => {
console.log("收到 SIGINT 信号,正在关闭...");
shutdown();
Deno.exit(0);
});
Deno.addSignalListener("SIGTERM", () => {
console.log("收到 SIGTERM 信号,正在关闭...");
shutdown();
Deno.exit(0);
});
// 启动应用
await startup();
// 获取端口号
const port = parseInt(Deno.env.get("PORT") || "5005");
console.log(`启动后端服务器在端口: ${port}`);
// 启动服务器
Deno.serve({ port }, app.fetch);

View File

@@ -0,0 +1,230 @@
/**
* Apple Music 控制和状态获取模块 (Deno 版本)
* 负责与 Apple Music 应用交互的所有功能
*/
export interface MusicStatus {
status: "playing" | "paused" | "stopped" | "notrunning" | "error";
track_name?: string;
artist?: string;
position?: number;
duration?: number;
error?: string;
}
export interface ControlResult {
status: "success" | "error";
message?: string;
}
/**
* 运行 AppleScript 并返回结果
*/
async function runAppleScript(script: string): Promise<string> {
const process = new Deno.Command("osascript", {
args: ["-e", script],
stdout: "piped",
stderr: "piped",
});
try {
const { code, stdout, stderr } = await process.output();
const output = new TextDecoder().decode(stdout).trim();
const error = new TextDecoder().decode(stderr).trim();
if (code !== 0) {
console.warn(`AppleScript 执行失败: ${error}`);
return "";
}
return output;
} catch (error) {
console.error(`执行 AppleScript 时发生错误: ${error}`);
return "";
}
}
/**
* 获取 Apple Music 播放状态 - 适配 macOS Tahoe
*/
export async function getMusicStatus(): Promise<MusicStatus> {
// 先获取播放状态
const statusScript = `
tell application "Music"
if it is running then
set playerState to (get player state) as string
return playerState
else
return "notrunning"
end if
end tell
`;
try {
const playerState = await runAppleScript(statusScript);
if (!playerState) {
console.error("AppleScript 执行失败,返回了空值");
return { status: "error", error: "AppleScript execution failed" };
}
if (playerState === "notrunning") {
return { status: "notrunning" };
}
if (playerState === "stopped") {
return { status: "stopped" };
}
// 尝试获取歌曲信息
try {
const infoScript = `
tell application "Music"
if it is running then
try
set trackName to ""
set artistName to ""
set pos to 0
set dur to 0
if exists current track then
set t to current track
try
set trackName to (name of t) as text
end try
try
set artistName to (artist of t) as text
end try
try
set dur to (duration of t) as real
end try
end if
try
set pos to (player position) as real
end try
return "success|||" & trackName & "|||" & artistName & "|||" & (pos as text) & "|||" & (dur as text)
on error errMsg
return "error|||" & errMsg
end try
else
return "notrunning"
end if
end tell
`;
const infoResult = await runAppleScript(infoScript);
if (infoResult && infoResult.startsWith("success|||")) {
const parts = infoResult.split("|||");
if (parts.length >= 5) {
return {
status: playerState as "playing" | "paused",
track_name: parts[1],
artist: parts[2],
position: parseFloat(parts[3]),
duration: parseFloat(parts[4]),
};
}
}
} catch (error) {
console.warn(`获取歌曲信息失败: ${error}`);
}
// 如果无法获取歌曲信息,至少返回播放状态
return {
status: playerState as "playing" | "paused",
track_name: "无法获取歌曲信息",
artist: "无法获取艺术家信息",
position: 0,
duration: 0,
};
} catch (error) {
console.error(`获取音乐状态时发生异常: ${error}`);
return { status: "error", error: String(error) };
}
}
/**
* 控制 Apple Music 播放
*/
export async function controlMusic(
action: string,
position?: number
): Promise<ControlResult> {
let script = "";
try {
switch (action) {
case "playpause":
script = `
tell application "Music"
if it is running then
playpause
return "ok"
else
return "notrunning"
end if
end tell
`;
break;
case "previous":
script = `
tell application "Music"
if it is running then
previous track
return "ok"
else
return "notrunning"
end if
end tell
`;
break;
case "next":
script = `
tell application "Music"
if it is running then
next track
return "ok"
else
return "notrunning"
end if
end tell
`;
break;
case "seek":
if (position !== undefined) {
script = `
tell application "Music"
if it is running then
set player position to ${position}
return "ok"
else
return "notrunning"
end if
end tell
`;
} else {
return { status: "error", message: "位置参数不能为空" };
}
break;
default:
return { status: "error", message: "未知的命令" };
}
const result = await runAppleScript(script);
if (result === "ok") {
return { status: "success" };
} else {
return { status: "error", message: result };
}
} catch (error) {
return { status: "error", message: String(error) };
}
}

View File

@@ -0,0 +1,94 @@
/**
* 后台任务管理模块 (Deno 版本)
* 负责定期获取音乐状态和歌词更新
*/
import { ConnectionManager } from "./websocket_manager.ts";
import { getMusicStatus, MusicStatus } from "./apple_music.ts";
import { getLyricsData, LyricsData } from "./lyrics.ts";
let backgroundTaskId: number | null = null;
/**
* 启动后台任务
*/
export function startBackgroundTask(manager: ConnectionManager): void {
if (backgroundTaskId) {
console.log("后台任务已在运行");
return;
}
console.log("启动后台任务...");
backgroundTaskId = setInterval(async () => {
await updateMusicInfo(manager);
}, 1000); // 每秒更新一次
console.log("后台任务已启动");
}
/**
* 停止后台任务
*/
export function stopBackgroundTask(): void {
if (backgroundTaskId) {
clearInterval(backgroundTaskId);
backgroundTaskId = null;
console.log("后台任务已停止");
}
}
/**
* 更新音乐信息并发送给客户端
*/
export async function updateMusicInfo(
manager: ConnectionManager
): Promise<void> {
try {
// 获取音乐状态
const musicStatus = await getMusicStatus();
if (musicStatus.status === "error") {
console.warn(`获取音乐状态失败: ${musicStatus.error}`);
return;
}
// 获取歌词数据
const lyricsData = await getLyricsData(musicStatus);
// 合并数据
const updateData = {
type: "music_update",
timestamp: Date.now(),
music: musicStatus,
lyrics: lyricsData,
};
// 发送给所有连接的客户端
manager.broadcast(updateData);
} catch (error) {
console.error(`更新音乐信息时出错: ${error}`);
}
}
/**
* 获取歌词并发送更新
*/
export async function fetchAndUpdateLyrics(
status: MusicStatus,
manager: ConnectionManager
): Promise<void> {
try {
const lyricsData = await getLyricsData(status);
const updateData = {
type: "lyrics_update",
timestamp: Date.now(),
lyrics: lyricsData,
};
manager.broadcast(updateData);
} catch (error) {
console.error(`获取歌词并发送更新时出错: ${error}`);
}
}

View File

@@ -0,0 +1,305 @@
/**
* 歌词管理模块 (Deno 版本)
* 负责歌词的获取、解析和缓存
*/
import { MusicStatus } from "./apple_music.ts";
export interface LyricsData {
current_lyric_time: number | null;
current_lyric: string | null;
next_lyric: string | null;
next_next_lyric: string | null;
track_name: string | null;
artist_name: string | null;
}
interface LyricsLine {
time: number;
text: string;
}
interface LyricsCache {
track_name: string | null;
artist: string | null;
lyrics_text: string | null;
source: string | null;
deleted: boolean;
}
// 全局歌词缓存
let lyricsCache: LyricsCache = {
track_name: null,
artist: null,
lyrics_text: null,
source: null,
deleted: false,
};
const DB_PATH = "./lyrics.db";
/**
* 初始化数据库
*/
export async function initDB(): Promise<void> {
try {
// 确保歌词目录存在
await Deno.mkdir("./lyrics", { recursive: true });
console.log(`数据库初始化完成: ${DB_PATH}`);
} catch (error) {
console.error(`数据库初始化失败: ${error}`);
}
}
/**
* 清空歌词缓存
*/
export function clearLyricsCache(): void {
lyricsCache = {
track_name: null,
artist: null,
lyrics_text: null,
source: null,
deleted: false,
};
}
/**
* 从数据库加载歌词
*/
async function loadLyricsFromDB(
trackName: string,
artist: string
): Promise<string | null> {
try {
const filePath = `./lyrics/${encodeURIComponent(
trackName
)}_${encodeURIComponent(artist)}.lrc`;
const content = await Deno.readTextFile(filePath);
return content;
} catch (error) {
return null;
}
}
/**
* 保存歌词到数据库
*/
async function saveLyrics(
trackName: string,
artist: string,
lyricsText: string
): Promise<void> {
try {
const filePath = `./lyrics/${encodeURIComponent(
trackName
)}_${encodeURIComponent(artist)}.lrc`;
await Deno.writeTextFile(filePath, lyricsText);
} catch (error) {
console.error(`保存歌词失败: ${error}`);
}
}
/**
* 从网易云音乐 API 搜索歌词
*/
async function searchLyrics(
trackName: string,
artistName: string
): Promise<string | null> {
try {
const searchVariants = [
{ title: trackName, artist: artistName },
{ title: trackName, artist: "" },
{ title: trackName.replace(/[vV]er\.\s*\d*/g, ""), artist: artistName },
{ title: trackName.replace(/\([^)]*\)/g, "").trim(), artist: artistName },
];
for (const variant of searchVariants) {
try {
const encodedTitle = encodeURIComponent(variant.title);
const encodedArtist = encodeURIComponent(variant.artist);
let url = `http://123.57.93.143:28883/lyrics?title=${encodedTitle}`;
if (encodedArtist) {
url += `&artist=${encodedArtist}`;
}
const response = await fetch(url, {
headers: {
Authorization: "fzt_tom",
"User-Agent": "MacLyric/1.0",
},
});
if (response.ok) {
const lyricsText = await response.text();
if (lyricsText) {
return lyricsText;
}
}
} catch (error) {
console.warn(`搜索歌词变体失败: ${error}`);
continue;
}
}
return null;
} catch (error) {
console.error(`搜索歌词时出错: ${error}`);
return null;
}
}
/**
* 解析 LRC 格式歌词
*/
function parseLrcLyrics(lyricsText: string): LyricsLine[] {
const lines = lyricsText.split("\n");
const lyricsLines: LyricsLine[] = [];
for (const line of lines) {
const match = line.match(/^\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)$/);
if (match) {
const minutes = parseInt(match[1]);
const seconds = parseInt(match[2]);
const milliseconds = parseInt(match[3].padEnd(3, "0"));
const text = match[4].trim();
const time = minutes * 60 + seconds + milliseconds / 1000;
lyricsLines.push({ time, text });
}
}
return lyricsLines.sort((a, b) => a.time - b.time);
}
/**
* 格式化歌词数据
*/
function formatLrcLyrics(
lyricsText: string,
currentPosition?: number
): LyricsData {
const lyricsLines = parseLrcLyrics(lyricsText);
if (lyricsLines.length === 0) {
return {
current_lyric_time: null,
current_lyric: null,
next_lyric: null,
next_next_lyric: null,
track_name: null,
artist_name: null,
};
}
if (currentPosition === undefined) {
return {
current_lyric_time: lyricsLines[0].time,
current_lyric: lyricsLines[0].text,
next_lyric: lyricsLines.length > 1 ? lyricsLines[1].text : null,
next_next_lyric: lyricsLines.length > 2 ? lyricsLines[2].text : null,
track_name: null,
artist_name: null,
};
}
let currentIndex = -1;
for (let i = 0; i < lyricsLines.length; i++) {
if (lyricsLines[i].time <= currentPosition) {
currentIndex = i;
} else {
break;
}
}
const currentLyric = currentIndex >= 0 ? lyricsLines[currentIndex] : null;
const nextLyric =
currentIndex + 1 < lyricsLines.length
? lyricsLines[currentIndex + 1]
: null;
const nextNextLyric =
currentIndex + 2 < lyricsLines.length
? lyricsLines[currentIndex + 2]
: null;
return {
current_lyric_time: currentLyric?.time || null,
current_lyric: currentLyric?.text || null,
next_lyric: nextLyric?.text || null,
next_next_lyric: nextNextLyric?.text || null,
track_name: null,
artist_name: null,
};
}
/**
* 获取当前歌词数据
*/
export async function getLyricsData(status: MusicStatus): Promise<LyricsData> {
// 如果没有播放,清空缓存并返回空
if (status.status !== "playing" && status.status !== "paused") {
clearLyricsCache();
return {
current_lyric_time: null,
current_lyric: null,
next_lyric: null,
next_next_lyric: null,
track_name: null,
artist_name: null,
};
}
const trackName = status.track_name;
const artist = status.artist;
const position = status.position || 0;
// 检查是否切换了歌曲
if (lyricsCache.track_name !== trackName || lyricsCache.artist !== artist) {
console.log(`歌曲切换,重新获取歌词: ${trackName} - ${artist}`);
// 清空缓存
lyricsCache.track_name = trackName;
lyricsCache.artist = artist;
lyricsCache.lyrics_text = null;
lyricsCache.deleted = false;
// 1. 先尝试从数据库获取
const lyricsText = await loadLyricsFromDB(trackName || "", artist || "");
if (lyricsText) {
lyricsCache.lyrics_text = lyricsText;
lyricsCache.source = "db";
}
// 2. 如果本地没有且不是被删除的歌词尝试从网易云音乐API获取歌词
else if (!lyricsCache.lyrics_text && !lyricsCache.deleted) {
const lyricsText = await searchLyrics(trackName || "", artist || "");
if (lyricsText) {
// 保存到数据库
await saveLyrics(trackName || "", artist || "", lyricsText);
lyricsCache.lyrics_text = lyricsText;
lyricsCache.source = "netease";
}
}
}
// 如果有缓存的歌词,直接解析
if (lyricsCache.lyrics_text) {
const lyricsData = formatLrcLyrics(lyricsCache.lyrics_text, position);
return {
...lyricsData,
track_name: trackName || null,
artist_name: artist || null,
};
}
// 没有歌词
return {
current_lyric_time: null,
current_lyric: null,
next_lyric: null,
next_next_lyric: null,
track_name: trackName || null,
artist_name: artist || null,
};
}

View File

@@ -0,0 +1,161 @@
/**
* 路由模块 (Deno 版本)
* 定义所有 API 路由
*/
import { Hono } from "hono";
import { cors } from "cors";
import { logger } from "logger";
import { ConnectionManager } from "./websocket_manager.ts";
import { getMusicStatus, controlMusic } from "./apple_music.ts";
import { getLyricsData } from "./lyrics.ts";
export interface ControlRequest {
action: "playpause" | "previous" | "next" | "seek";
position?: number;
}
export interface SearchLyricsRequest {
title: string;
artist: string;
}
export interface GetLyricsFromIdRequest {
title: string;
artist: string;
}
/**
* 注册所有路由
*/
export function registerRoutes(app: Hono, manager: ConnectionManager): void {
// 添加中间件
app.use("*", async (c, next) => {
if (c.req.path !== "/ws") {
console.log("[CORS middleware triggered]", c.req.method, c.req.path)
return await cors({
origin: "*",
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
})(c, next);
} else {
return await next();
}
});
// 健康检查
app.get("/health", (c) => {
return c.json({
status: "ok",
timestamp: Date.now(),
connections: manager.getConnectionCount(),
});
});
// 获取音乐状态
app.get("/music/status", async (c) => {
try {
const status = await getMusicStatus();
return c.json(status);
} catch (error) {
return c.json({ status: "error", error: String(error) }, 500);
}
});
// 控制音乐播放
app.post("/music/control", async (c) => {
try {
const body = (await c.req.json()) as ControlRequest;
if (!body.action) {
return c.json({ status: "error", message: "缺少 action 参数" }, 400);
}
const result = await controlMusic(body.action, body.position);
return c.json(result);
} catch (error) {
return c.json({ status: "error", message: String(error) }, 500);
}
});
// 获取歌词
app.get("/lyrics", async (c) => {
try {
const status = await getMusicStatus();
const lyricsData = await getLyricsData(status);
return c.json(lyricsData);
} catch (error) {
return c.json({ status: "error", error: String(error) }, 500);
}
});
// 搜索歌词
app.post("/lyrics/search", async (c) => {
try {
const body = (await c.req.json()) as SearchLyricsRequest;
if (!body.title) {
return c.json({ status: "error", message: "缺少 title 参数" }, 400);
}
// 这里可以实现歌词搜索逻辑
// 暂时返回空结果
return c.json({
status: "success",
lyrics: null,
message: "歌词搜索功能待实现",
});
} catch (error) {
return c.json({ status: "error", error: String(error) }, 500);
}
});
// 根据歌曲信息获取歌词
app.post("/lyrics/get", async (c) => {
try {
const body = (await c.req.json()) as GetLyricsFromIdRequest;
if (!body.title) {
return c.json({ status: "error", message: "缺少 title 参数" }, 400);
}
// 构造音乐状态对象
const mockStatus = {
status: "playing" as const,
track_name: body.title,
artist: body.artist || "",
position: 0,
duration: 0,
};
const lyricsData = await getLyricsData(mockStatus);
return c.json(lyricsData);
} catch (error) {
return c.json({ status: "error", error: String(error) }, 500);
}
});
// WebSocket 连接端点
app.get("/ws", (c) => {
const upgrade = c.req.header("upgrade");
if (upgrade !== "websocket") {
return c.text("Expected websocket", 400);
}
const { socket, response } = Deno.upgradeWebSocket(c.req.raw);
const connectionId = `conn_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
// 添加连接到管理器
manager.addConnection(connectionId, socket);
return response;
});
// 404 处理
app.notFound((c) => {
return c.json({ status: "error", message: "Not Found" }, 404);
});
}

View File

@@ -0,0 +1,163 @@
/**
* WebSocket 连接管理器 (Deno 版本)
* 负责管理 WebSocket 连接和消息广播
*/
export interface WebSocketConnection {
id: string;
socket: WebSocket;
isAlive: boolean;
}
export class ConnectionManager {
private connections: Map<string, WebSocketConnection> = new Map();
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
constructor() {
this.startHeartbeat();
}
/**
* 添加新连接
*/
addConnection(id: string, socket: WebSocket): void {
const connection: WebSocketConnection = {
id,
socket,
isAlive: true,
};
this.connections.set(id, connection);
console.log(`新连接已添加: ${id}, 当前连接数: ${this.connections.size}`);
// 设置连接关闭处理
socket.addEventListener("close", () => {
this.removeConnection(id);
});
// 设置心跳响应处理
socket.addEventListener("message", (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === "ping") {
connection.isAlive = true;
this.sendToConnection(id, { type: "pong" });
}
} catch (error) {
console.warn(`处理消息时出错: ${error}`);
}
});
}
/**
* 移除连接
*/
removeConnection(id: string): void {
if (this.connections.has(id)) {
this.connections.delete(id);
console.log(`连接已移除: ${id}, 当前连接数: ${this.connections.size}`);
}
}
/**
* 向特定连接发送消息
*/
sendToConnection(id: string, message: any): boolean {
const connection = this.connections.get(id);
if (connection && connection.socket.readyState === WebSocket.OPEN) {
try {
connection.socket.send(JSON.stringify(message));
return true;
} catch (error) {
console.error(`发送消息到连接 ${id} 失败: ${error}`);
this.removeConnection(id);
return false;
}
}
return false;
}
/**
* 向所有连接广播消息
*/
broadcast(message: any): void {
const deadConnections: string[] = [];
for (const [id, connection] of this.connections) {
if (connection.socket.readyState === WebSocket.OPEN) {
try {
connection.socket.send(JSON.stringify(message));
} catch (error) {
console.error(`广播消息到连接 ${id} 失败: ${error}`);
deadConnections.push(id);
}
} else {
deadConnections.push(id);
}
}
// 清理死连接
deadConnections.forEach((id) => this.removeConnection(id));
}
/**
* 获取连接数量
*/
getConnectionCount(): number {
return this.connections.size;
}
/**
* 获取所有连接 ID
*/
getConnectionIds(): string[] {
return Array.from(this.connections.keys());
}
/**
* 启动心跳检测
*/
private startHeartbeat(): void {
this.heartbeatInterval = setInterval(() => {
const deadConnections: string[] = [];
for (const [id, connection] of this.connections) {
if (!connection.isAlive) {
deadConnections.push(id);
} else {
connection.isAlive = false;
// 发送心跳检测
this.sendToConnection(id, { type: "ping" });
}
}
// 清理死连接
deadConnections.forEach((id) => this.removeConnection(id));
}, 30000); // 每30秒检测一次
}
/**
* 停止心跳检测
*/
stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
/**
* 关闭所有连接
*/
closeAllConnections(): void {
for (const [id, connection] of this.connections) {
try {
connection.socket.close();
} catch (error) {
console.error(`关闭连接 ${id} 失败: ${error}`);
}
}
this.connections.clear();
this.stopHeartbeat();
}
}