feat: 后端改为deno
This commit is contained in:
28
backend-deno/config.ts
Normal file
28
backend-deno/config.ts
Normal 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
12
backend-deno/deno.json
Normal 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
42
backend-deno/deno.lock
generated
Normal 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
79
backend-deno/main.ts
Normal 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);
|
||||
230
backend-deno/modules/apple_music.ts
Normal file
230
backend-deno/modules/apple_music.ts
Normal 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) };
|
||||
}
|
||||
}
|
||||
94
backend-deno/modules/background_tasks.ts
Normal file
94
backend-deno/modules/background_tasks.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
305
backend-deno/modules/lyrics.ts
Normal file
305
backend-deno/modules/lyrics.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
161
backend-deno/modules/routes.ts
Normal file
161
backend-deno/modules/routes.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
163
backend-deno/modules/websocket_manager.ts
Normal file
163
backend-deno/modules/websocket_manager.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user