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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# 歌词服务器配置示例
|
|
||||||
LYRICS_SERVER=http://123.57.93.143:28883
|
|
||||||
LYRICS_AUTHORIZATION=fzt_tom
|
|
||||||
7
backend/.gitignore
vendored
7
backend/.gitignore
vendored
@@ -1,7 +0,0 @@
|
|||||||
__pycache__
|
|
||||||
lyrics
|
|
||||||
.DS_Store
|
|
||||||
build
|
|
||||||
dist
|
|
||||||
.env
|
|
||||||
venv
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
'''
|
|
||||||
Date: 2025-04-24 16:32:55
|
|
||||||
LastEditors: 陈子健
|
|
||||||
LastEditTime: 2025-05-26 17:13:51
|
|
||||||
FilePath: /mac-lyric-vue/backend/config.py
|
|
||||||
'''
|
|
||||||
import os
|
|
||||||
|
|
||||||
APP_SUPPORT_DIR = os.path.expanduser('~/Library/Application Support/lyroc')
|
|
||||||
os.makedirs(APP_SUPPORT_DIR, exist_ok=True)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
'''
|
|
||||||
Date: 2025-05-06 09:26:51
|
|
||||||
LastEditors: 陈子健
|
|
||||||
LastEditTime: 2025-05-26 17:14:17
|
|
||||||
FilePath: /mac-lyric-vue/backend/main.py
|
|
||||||
'''
|
|
||||||
"""
|
|
||||||
lyroc 后端主程序
|
|
||||||
负责初始化应用和启动服务器
|
|
||||||
"""
|
|
||||||
import uvicorn
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from fastapi import FastAPI
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
# 导入自定义模块
|
|
||||||
from modules.websocket_manager import ConnectionManager
|
|
||||||
from modules.background_tasks import start_background_task, cancel_all_tasks, background_tasks
|
|
||||||
from modules.routes import register_routes
|
|
||||||
|
|
||||||
# 配置日志
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.DEBUG,
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# 应用支持目录和歌词目录在lyrics模块中已定义
|
|
||||||
|
|
||||||
# Lifespan 上下文管理器,替代 on_event
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
logger.info("正在启动API服务...")
|
|
||||||
try:
|
|
||||||
# 启动时创建后台任务
|
|
||||||
start_background_task(manager)
|
|
||||||
logger.info("后台任务已启动")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"启动后台任务失败: {e}")
|
|
||||||
|
|
||||||
yield # 应用正常运行
|
|
||||||
|
|
||||||
# 关闭时清理任务
|
|
||||||
try:
|
|
||||||
cancel_all_tasks()
|
|
||||||
logger.info("后台任务已关闭")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"关闭后台任务失败: {e}")
|
|
||||||
|
|
||||||
# 创建FastAPI应用
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
|
||||||
|
|
||||||
# 确保在应用启动时直接启动任务(作为后备方案)
|
|
||||||
@app.on_event("startup")
|
|
||||||
async def startup_event():
|
|
||||||
# 仅当通过 lifespan 未启动任务时才启动
|
|
||||||
if not background_tasks:
|
|
||||||
logger.info("通过startup_event启动后台任务...")
|
|
||||||
try:
|
|
||||||
start_background_task(manager)
|
|
||||||
logger.info("后台任务通过startup_event成功启动")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"通过startup_event启动后台任务失败: {e}")
|
|
||||||
|
|
||||||
# 配置CORS
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# 创建WebSocket连接管理器
|
|
||||||
manager = ConnectionManager()
|
|
||||||
|
|
||||||
# 注册API路由
|
|
||||||
register_routes(app, manager)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# 从环境变量获取端口号,如果未设置则默认使用5000
|
|
||||||
port = int(os.environ.get("PORT", 5000))
|
|
||||||
logger.info(f"启动后端服务器在端口: {port}")
|
|
||||||
uvicorn.run("main:app", host="127.0.0.1", port=port, reload=True)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
'''
|
|
||||||
Date: 2025-05-06 09:25:16
|
|
||||||
LastEditors: 陈子健
|
|
||||||
LastEditTime: 2025-05-26 17:14:27
|
|
||||||
FilePath: /mac-lyric-vue/backend/modules/__init__.py
|
|
||||||
'''
|
|
||||||
"""
|
|
||||||
lyroc 后端模块包
|
|
||||||
"""
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
"""
|
|
||||||
Apple Music 控制和状态获取模块
|
|
||||||
负责与 Apple Music 应用交互的所有功能
|
|
||||||
"""
|
|
||||||
import subprocess
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
def run_applescript(script: str):
|
|
||||||
"""运行 AppleScript 并返回结果"""
|
|
||||||
result = subprocess.run([
|
|
||||||
"osascript", "-e", script
|
|
||||||
], capture_output=True, text=True)
|
|
||||||
return result.stdout.strip()
|
|
||||||
|
|
||||||
def get_music_status():
|
|
||||||
"""获取 Apple Music 播放状态"""
|
|
||||||
script = '''
|
|
||||||
tell application "Music"
|
|
||||||
if it is running then
|
|
||||||
set playerState to (get player state) as string
|
|
||||||
if playerState is "stopped" then
|
|
||||||
return "stopped"
|
|
||||||
else
|
|
||||||
set trackName to name of current track
|
|
||||||
set artistName to artist of current track
|
|
||||||
set pos to player position
|
|
||||||
set dur to duration of current track
|
|
||||||
return playerState & "|||" & trackName & "|||" & artistName & "|||" & (pos as text) & "|||" & (dur as text)
|
|
||||||
end if
|
|
||||||
else
|
|
||||||
return "notrunning"
|
|
||||||
end if
|
|
||||||
end tell
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
out = run_applescript(script)
|
|
||||||
if out is None:
|
|
||||||
logger.error("AppleScript执行失败,返回了None")
|
|
||||||
return {"status": "error", "error": "AppleScript execution failed"}
|
|
||||||
|
|
||||||
if out == "notrunning":
|
|
||||||
return {"status": "notrunning"}
|
|
||||||
if out == "stopped":
|
|
||||||
return {"status": "stopped"}
|
|
||||||
try:
|
|
||||||
player_state, track_name, artist, pos, dur = out.split("|||")
|
|
||||||
return {
|
|
||||||
"status": player_state,
|
|
||||||
"track_name": track_name,
|
|
||||||
"artist": artist,
|
|
||||||
"position": float(pos),
|
|
||||||
"duration": float(dur)
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {"status": "error", "error": str(e), "raw": out}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取音乐状态时发生异常: {e}")
|
|
||||||
return {"status": "error", "error": str(e)}
|
|
||||||
|
|
||||||
def control_music(action, position=None):
|
|
||||||
"""控制Apple Music播放"""
|
|
||||||
try:
|
|
||||||
if action == "playpause":
|
|
||||||
script = '''
|
|
||||||
tell application "Music"
|
|
||||||
if it is running then
|
|
||||||
playpause
|
|
||||||
return "ok"
|
|
||||||
else
|
|
||||||
return "notrunning"
|
|
||||||
end if
|
|
||||||
end tell
|
|
||||||
'''
|
|
||||||
elif action == "previous":
|
|
||||||
script = '''
|
|
||||||
tell application "Music"
|
|
||||||
if it is running then
|
|
||||||
previous track
|
|
||||||
return "ok"
|
|
||||||
else
|
|
||||||
return "notrunning"
|
|
||||||
end if
|
|
||||||
end tell
|
|
||||||
'''
|
|
||||||
elif action == "next":
|
|
||||||
script = '''
|
|
||||||
tell application "Music"
|
|
||||||
if it is running then
|
|
||||||
next track
|
|
||||||
return "ok"
|
|
||||||
else
|
|
||||||
return "notrunning"
|
|
||||||
end if
|
|
||||||
end tell
|
|
||||||
'''
|
|
||||||
elif action == "seek" and position is not None:
|
|
||||||
script = f'''
|
|
||||||
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": "未知的命令"}
|
|
||||||
|
|
||||||
result = run_applescript(script)
|
|
||||||
if result == "ok":
|
|
||||||
return {"status": "success"}
|
|
||||||
else:
|
|
||||||
return {"status": "error", "message": result}
|
|
||||||
except Exception as e:
|
|
||||||
return {"status": "error", "message": str(e)}
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
"""
|
|
||||||
后台任务模块
|
|
||||||
负责创建和管理定期检查Apple Music状态并推送更新的后台任务
|
|
||||||
"""
|
|
||||||
import time
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from .apple_music import get_music_status
|
|
||||||
from .lyrics import get_lyrics_data
|
|
||||||
from .websocket_manager import ConnectionManager
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# 存储后台任务引用
|
|
||||||
background_tasks = set()
|
|
||||||
|
|
||||||
async def fetch_and_update_lyrics(status, manager):
|
|
||||||
"""异步获取歌词并发送更新"""
|
|
||||||
try:
|
|
||||||
# 异步获取歌词
|
|
||||||
logger.info(f"正在获取歌词...")
|
|
||||||
lyrics = get_lyrics_data(status)
|
|
||||||
logger.info(f"歌词: {lyrics}")
|
|
||||||
# 获取到歌词后,再发送一个歌词更新消息
|
|
||||||
if lyrics and isinstance(lyrics, dict): # 确保lyrics是一个非空的字典
|
|
||||||
lyrics_update = {
|
|
||||||
"type": "lyric_update", # 使用不同的消息类型
|
|
||||||
"timestamp": time.time(),
|
|
||||||
"lyrics": lyrics
|
|
||||||
}
|
|
||||||
|
|
||||||
await manager.broadcast(lyrics_update)
|
|
||||||
logger.info("歌词获取完成,已发送更新")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取歌词并发送更新时出错: {e}")
|
|
||||||
|
|
||||||
async def update_music_info(manager: ConnectionManager):
|
|
||||||
"""定期检查音乐状态并向所有客户端推送更新"""
|
|
||||||
logger.info("后台任务update_music_info开始运行...")
|
|
||||||
|
|
||||||
# 记录上一次状态
|
|
||||||
last_position = -1
|
|
||||||
last_track_name = None
|
|
||||||
last_lyrics = None
|
|
||||||
last_playback_status = None # 记录上一次的播放状态
|
|
||||||
|
|
||||||
# 配置更新间隔
|
|
||||||
check_interval = 0.05 # 内部检查间隔仍保持较短
|
|
||||||
|
|
||||||
# 设定重试次数
|
|
||||||
consecutive_errors = 0
|
|
||||||
max_consecutive_errors = 10
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
current_time = time.time()
|
|
||||||
|
|
||||||
# 获取当前状态
|
|
||||||
status = get_music_status()
|
|
||||||
if status is None:
|
|
||||||
logger.error("获取音乐状态失败,返回None")
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
current_status = status.get("status", "unknown")
|
|
||||||
|
|
||||||
# 错误状态处理
|
|
||||||
if current_status == "error":
|
|
||||||
logger.error(f"获取音乐状态返回错误: {status.get('error', 'Unknown error')}")
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 状态变化处理
|
|
||||||
if current_status != last_playback_status and last_playback_status is not None:
|
|
||||||
logger.info(f"播放状态变化: {last_playback_status} -> {current_status}")
|
|
||||||
|
|
||||||
# 确定消息类型
|
|
||||||
message_type = "status_change"
|
|
||||||
if current_status == "playing":
|
|
||||||
message_type = "playback_resumed"
|
|
||||||
elif current_status == "paused":
|
|
||||||
message_type = "playback_paused"
|
|
||||||
elif current_status == "stopped":
|
|
||||||
message_type = "playback_stopped"
|
|
||||||
elif current_status == "notrunning":
|
|
||||||
message_type = "app_not_running"
|
|
||||||
|
|
||||||
# 向客户端通知状态变化
|
|
||||||
status_update = {
|
|
||||||
"type": message_type,
|
|
||||||
"timestamp": current_time,
|
|
||||||
"status": status
|
|
||||||
}
|
|
||||||
|
|
||||||
await manager.broadcast(status_update)
|
|
||||||
last_playback_status = current_status
|
|
||||||
consecutive_errors = 0
|
|
||||||
await asyncio.sleep(check_interval)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 如果没有状态变化,但是还没有记录过状态,进行初始化
|
|
||||||
if last_playback_status is None:
|
|
||||||
last_playback_status = current_status
|
|
||||||
|
|
||||||
# 如果应用未运行或已停止,无需进一步处理
|
|
||||||
if current_status in ["stopped", "notrunning"]:
|
|
||||||
await asyncio.sleep(check_interval)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 保存当前播放状态用于下次比较
|
|
||||||
last_playback_status = current_status
|
|
||||||
|
|
||||||
# 如果已停止,无需进一步处理
|
|
||||||
if current_status in ["stopped", "notrunning"]:
|
|
||||||
await asyncio.sleep(check_interval)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 以下是正常播放状态下的处理
|
|
||||||
position = status.get("position", -1)
|
|
||||||
track_name = status.get("track_name")
|
|
||||||
|
|
||||||
# 安全检查:如果缺少关键数据,跳过本次循环
|
|
||||||
if track_name is None:
|
|
||||||
logger.debug("歌曲名称为空,跳过本次更新")
|
|
||||||
await asyncio.sleep(check_interval)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 检查是否需要发送更新
|
|
||||||
track_changed = track_name != last_track_name
|
|
||||||
|
|
||||||
# 如果歌曲变化,获取新歌词
|
|
||||||
if track_changed:
|
|
||||||
try:
|
|
||||||
# 立即发送歌曲变化通知(不含歌词)
|
|
||||||
logger.info(f"歌曲变化: {last_track_name} -> {track_name}")
|
|
||||||
|
|
||||||
# 先发送一个不含歌词的track_change消息
|
|
||||||
initial_update = {
|
|
||||||
"type": "track_change",
|
|
||||||
"timestamp": current_time,
|
|
||||||
"status": status,
|
|
||||||
"lyrics": None # 不包含歌词
|
|
||||||
}
|
|
||||||
|
|
||||||
await manager.broadcast(initial_update)
|
|
||||||
|
|
||||||
# 更新歌曲名称记录
|
|
||||||
last_track_name = track_name
|
|
||||||
last_position = position
|
|
||||||
|
|
||||||
# 重置错误计数
|
|
||||||
consecutive_errors = 0
|
|
||||||
|
|
||||||
# 启动一个独立的异步任务来获取歌词并后续发送更新
|
|
||||||
# 这样主循环可以继续运行,不会阻塞
|
|
||||||
asyncio.create_task(fetch_and_update_lyrics(status, manager))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"处理歌曲变化时出错: {e}")
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
# 检查歌词是否需要更新(播放位置变化明显)
|
|
||||||
elif abs(last_position - position) > 0.5: # 如果播放位置变化超过0.5秒
|
|
||||||
try:
|
|
||||||
lyrics = get_lyrics_data(status)
|
|
||||||
|
|
||||||
# 只有当歌词内容变化时才发送更新
|
|
||||||
current_lyric = lyrics.get("current_lyric", "") if lyrics and isinstance(lyrics, dict) else ""
|
|
||||||
current_lyric_time = lyrics.get("current_lyric_time", "") if lyrics and isinstance(lyrics, dict) else ""
|
|
||||||
last_current_lyric_time = last_lyrics.get("current_lyric_time", "") if last_lyrics and isinstance(last_lyrics, dict) else ""
|
|
||||||
|
|
||||||
if current_lyric_time != last_current_lyric_time:
|
|
||||||
if current_lyric is None:
|
|
||||||
current_lyric = ""
|
|
||||||
logger.debug(f"歌词更新: {current_lyric[:20]}...")
|
|
||||||
|
|
||||||
combined_data = {
|
|
||||||
"type": "lyric_change",
|
|
||||||
"timestamp": current_time,
|
|
||||||
"status": status,
|
|
||||||
"lyrics": lyrics
|
|
||||||
}
|
|
||||||
|
|
||||||
await manager.broadcast(combined_data)
|
|
||||||
last_lyrics = lyrics
|
|
||||||
last_position = position
|
|
||||||
|
|
||||||
# 重置错误计数
|
|
||||||
consecutive_errors = 0
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"处理歌词更新时出错: {e}")
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
# 更新上一次位置
|
|
||||||
if track_changed or (abs(last_position - position) > 0.5):
|
|
||||||
last_position = position
|
|
||||||
|
|
||||||
# 等待下一次检查
|
|
||||||
await asyncio.sleep(check_interval)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"更新音乐信息时出错: {e}")
|
|
||||||
consecutive_errors += 1
|
|
||||||
if consecutive_errors >= max_consecutive_errors:
|
|
||||||
logger.error(f"连续错误次数超过{max_consecutive_errors},退出后台任务")
|
|
||||||
break
|
|
||||||
await asyncio.sleep(1) # 出错时等待1秒再重试
|
|
||||||
|
|
||||||
def start_background_task(manager: ConnectionManager):
|
|
||||||
"""启动后台任务并添加到跟踪集合中"""
|
|
||||||
task = asyncio.create_task(update_music_info(manager))
|
|
||||||
background_tasks.add(task)
|
|
||||||
task.add_done_callback(background_tasks.discard)
|
|
||||||
return task
|
|
||||||
|
|
||||||
def cancel_all_tasks():
|
|
||||||
"""取消所有后台任务"""
|
|
||||||
for task in background_tasks:
|
|
||||||
task.cancel()
|
|
||||||
@@ -1,435 +0,0 @@
|
|||||||
"""
|
|
||||||
歌词处理模块
|
|
||||||
负责歌词的解析、格式化、存储和获取
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import logging
|
|
||||||
import requests
|
|
||||||
import sqlite3
|
|
||||||
from urllib.parse import quote
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
import base64
|
|
||||||
import random
|
|
||||||
import codecs
|
|
||||||
import json
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# 配置应用支持目录
|
|
||||||
APP_SUPPORT_DIR = os.path.expanduser('~/Library/Application Support/lyroc')
|
|
||||||
os.makedirs(APP_SUPPORT_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
# SQLite数据库配置
|
|
||||||
DB_PATH = os.path.join(APP_SUPPORT_DIR, 'lyroc.db')
|
|
||||||
|
|
||||||
def init_db():
|
|
||||||
"""初始化SQLite数据库"""
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# 创建歌词表
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS lyrics (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
track_name TEXT NOT NULL,
|
|
||||||
artist TEXT NOT NULL,
|
|
||||||
lyrics_content TEXT NOT NULL,
|
|
||||||
source TEXT DEFAULT 'netease',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(track_name, artist)
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
logger.debug(f"数据库初始化完成: {DB_PATH}")
|
|
||||||
|
|
||||||
# 初始化数据库
|
|
||||||
init_db()
|
|
||||||
|
|
||||||
def clear_lyrics_cache():
|
|
||||||
"""清空歌词缓存"""
|
|
||||||
global _lyrics_cache
|
|
||||||
_lyrics_cache = {
|
|
||||||
"track_name": None,
|
|
||||||
"artist": None,
|
|
||||||
"lyrics_text": None,
|
|
||||||
"source": None,
|
|
||||||
"deleted": False
|
|
||||||
}
|
|
||||||
|
|
||||||
# 歌词缓存
|
|
||||||
clear_lyrics_cache()
|
|
||||||
|
|
||||||
# 网易云音乐API配置
|
|
||||||
NETEASE_API_BASE = "https://music.163.com/weapi"
|
|
||||||
NETEASE_SEARCH_URL = f"{NETEASE_API_BASE}/search/get"
|
|
||||||
NETEASE_LYRIC_URL = f"{NETEASE_API_BASE}/song/lyric"
|
|
||||||
|
|
||||||
# 加密相关配置
|
|
||||||
MODULUS = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
|
|
||||||
NONCE = '0CoJUm6Qyw8W8jud'
|
|
||||||
PUBKEY = '010001'
|
|
||||||
IV = '0102030405060708'
|
|
||||||
|
|
||||||
class LyricLine:
|
|
||||||
def __init__(self, time, text):
|
|
||||||
self.time = time # 时间(秒)
|
|
||||||
self.text = text # 歌词文本
|
|
||||||
|
|
||||||
def load_lyrics_from_db(track_name, artist):
|
|
||||||
"""从数据库加载歌词"""
|
|
||||||
try:
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute('''
|
|
||||||
SELECT lyrics_content FROM lyrics WHERE track_name = ? AND artist = ?
|
|
||||||
''', (track_name, artist))
|
|
||||||
result = cursor.fetchone()
|
|
||||||
conn.close()
|
|
||||||
if result:
|
|
||||||
return result[0]
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"加载数据库歌词时出错: {e}", exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def parse_time(time_str):
|
|
||||||
"""解析时间标记 [mm:ss.xxx]"""
|
|
||||||
try:
|
|
||||||
# 移除方括号
|
|
||||||
time_str = time_str.strip('[]')
|
|
||||||
# 分离分钟和秒
|
|
||||||
minutes, seconds = time_str.split(':')
|
|
||||||
# 转换为秒
|
|
||||||
total_seconds = float(minutes) * 60 + float(seconds)
|
|
||||||
return total_seconds
|
|
||||||
except:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def format_lrc_lyrics(lyrics_text, current_position=None):
|
|
||||||
"""格式化 LRC 歌词,找出当前、下一句和下下句歌词"""
|
|
||||||
if not lyrics_text:
|
|
||||||
return None, None, None, None
|
|
||||||
|
|
||||||
# 解析歌词行
|
|
||||||
lyric_lines = []
|
|
||||||
for line in lyrics_text.split('\n'):
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 查找所有时间标记
|
|
||||||
time_tags = re.findall(r'\[([0-9:.]+)\]', line)
|
|
||||||
if not time_tags:
|
|
||||||
continue
|
|
||||||
|
|
||||||
text = re.sub(r'\[[0-9:.]+\]', '', line).strip()
|
|
||||||
if not text:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 一行可能有多个时间标记
|
|
||||||
for time_tag in time_tags:
|
|
||||||
try:
|
|
||||||
time_seconds = parse_time(time_tag)
|
|
||||||
lyric_lines.append({"time": time_seconds, "text": text})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"解析时间标记出错: {time_tag}, 错误: {e}")
|
|
||||||
|
|
||||||
# 按时间排序
|
|
||||||
lyric_lines.sort(key=lambda x: x["time"])
|
|
||||||
|
|
||||||
# 如果没有有效歌词,返回空
|
|
||||||
if not lyric_lines:
|
|
||||||
return None, None, None, None
|
|
||||||
|
|
||||||
if not current_position:
|
|
||||||
# 如果没有提供当前位置,返回前三句歌词
|
|
||||||
current = lyric_lines[0]["text"] if len(lyric_lines) > 0 else None
|
|
||||||
next_ = lyric_lines[1]["text"] if len(lyric_lines) > 1 else None
|
|
||||||
next_next = lyric_lines[2]["text"] if len(lyric_lines) > 2 else None
|
|
||||||
return current, next_, next_next, lyric_lines[0]["time"]
|
|
||||||
|
|
||||||
# 找出当前歌词
|
|
||||||
current_index = -1
|
|
||||||
for i, line in enumerate(lyric_lines):
|
|
||||||
if line["time"] > current_position:
|
|
||||||
if i > 0:
|
|
||||||
current_index = i - 1
|
|
||||||
break
|
|
||||||
|
|
||||||
# 处理当前位置超过最后一行歌词的情况
|
|
||||||
if current_index == -1 and lyric_lines:
|
|
||||||
# 如果遍历完所有歌词都没找到大于当前位置的,说明当前位置超过了最后一行歌词
|
|
||||||
if current_position >= lyric_lines[-1]["time"]:
|
|
||||||
# 返回最后三句歌词,将最后一句作为当前歌词
|
|
||||||
last_index = len(lyric_lines) - 1
|
|
||||||
if last_index >= 2:
|
|
||||||
return lyric_lines[last_index]["text"], None, None, lyric_lines[last_index]["time"]
|
|
||||||
elif last_index == 1:
|
|
||||||
return lyric_lines[1]["text"], None, None, lyric_lines[1]["time"]
|
|
||||||
else:
|
|
||||||
return lyric_lines[0]["text"], None, None, lyric_lines[0]["time"]
|
|
||||||
|
|
||||||
# 如果找到了当前歌词
|
|
||||||
if current_index >= 0:
|
|
||||||
current = lyric_lines[current_index]["text"]
|
|
||||||
next_ = lyric_lines[current_index + 1]["text"] if current_index + 1 < len(lyric_lines) else None
|
|
||||||
next_next = lyric_lines[current_index + 2]["text"] if current_index + 2 < len(lyric_lines) else None
|
|
||||||
return current, next_, next_next, lyric_lines[current_index]["time"]
|
|
||||||
|
|
||||||
# 如果当前位置在所有歌词之前
|
|
||||||
if lyric_lines:
|
|
||||||
return None, lyric_lines[0]["text"], lyric_lines[1]["text"] if len(lyric_lines) > 1 else None, lyric_lines[0]["time"]
|
|
||||||
|
|
||||||
return None, None, None, None
|
|
||||||
|
|
||||||
def get_lyrics_data(status):
|
|
||||||
"""获取当前歌词(先获取状态,再从缓存或存储中获取歌词)"""
|
|
||||||
global _lyrics_cache
|
|
||||||
|
|
||||||
# 如果没有播放,清空缓存并返回空
|
|
||||||
if status.get("status") not in ["playing", "paused"]:
|
|
||||||
clear_lyrics_cache()
|
|
||||||
return {
|
|
||||||
"current_lyric_time": None,
|
|
||||||
"current_lyric": None,
|
|
||||||
"next_lyric": None,
|
|
||||||
"next_next_lyric": None,
|
|
||||||
"track_name": None,
|
|
||||||
"artist_name": None
|
|
||||||
}
|
|
||||||
|
|
||||||
track_name = status.get("track_name")
|
|
||||||
artist = status.get("artist")
|
|
||||||
position = status.get("position", 0)
|
|
||||||
|
|
||||||
# 检查是否切换了歌曲
|
|
||||||
if _lyrics_cache["track_name"] != track_name or _lyrics_cache["artist"] != artist:
|
|
||||||
logger.debug(f"歌曲切换,重新获取歌词: {track_name} - {artist}")
|
|
||||||
# 清空缓存
|
|
||||||
_lyrics_cache["track_name"] = track_name
|
|
||||||
_lyrics_cache["artist"] = artist
|
|
||||||
_lyrics_cache["lyrics_text"] = None
|
|
||||||
_lyrics_cache["deleted"] = False
|
|
||||||
|
|
||||||
# 1. 先尝试从数据库获取
|
|
||||||
lyrics_text = load_lyrics_from_db(track_name, artist)
|
|
||||||
if lyrics_text:
|
|
||||||
_lyrics_cache["lyrics_text"] = lyrics_text
|
|
||||||
_lyrics_cache["source"] = "db"
|
|
||||||
|
|
||||||
# 2. 如果本地没有,且不是被删除的歌词,尝试从网易云音乐API获取歌词
|
|
||||||
elif not _lyrics_cache["lyrics_text"] and not _lyrics_cache.get("deleted", False):
|
|
||||||
lyrics_text = search_lyrics(track_name, artist)
|
|
||||||
if lyrics_text:
|
|
||||||
# 保存到数据库
|
|
||||||
save_lyrics(track_name, artist, lyrics_text)
|
|
||||||
_lyrics_cache["lyrics_text"] = lyrics_text
|
|
||||||
_lyrics_cache["source"] = "netease"
|
|
||||||
|
|
||||||
# 如果有缓存的歌词,直接解析
|
|
||||||
if _lyrics_cache["lyrics_text"]:
|
|
||||||
if _lyrics_cache.get("deleted", False):
|
|
||||||
return {
|
|
||||||
"current_lyric_time": None,
|
|
||||||
"current_lyric": None,
|
|
||||||
"next_lyric": None,
|
|
||||||
"next_next_lyric": None,
|
|
||||||
}
|
|
||||||
# 根据播放位置解析当前歌词
|
|
||||||
current, next_, next_next, current_time = format_lrc_lyrics(_lyrics_cache["lyrics_text"], position)
|
|
||||||
if current or next_ or next_next:
|
|
||||||
return {
|
|
||||||
"current_lyric_time": current_time,
|
|
||||||
"current_lyric": current,
|
|
||||||
"next_lyric": next_,
|
|
||||||
"next_next_lyric": next_next,
|
|
||||||
"track_name": track_name,
|
|
||||||
"artist_name": artist,
|
|
||||||
"lyrics_source": _lyrics_cache["source"] # 标记歌词来源
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 都没找到返回空
|
|
||||||
return {
|
|
||||||
"current_lyric_time": None,
|
|
||||||
"current_lyric": None,
|
|
||||||
"next_lyric": None,
|
|
||||||
"next_next_lyric": None,
|
|
||||||
"track_name": track_name,
|
|
||||||
"artist_name": artist,
|
|
||||||
"lyrics_source": "none" # 标记歌词来源
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_secret_key(size):
|
|
||||||
"""生成随机密钥"""
|
|
||||||
return ''.join(random.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(size))
|
|
||||||
|
|
||||||
def aes_encrypt(text, key):
|
|
||||||
"""AES加密"""
|
|
||||||
pad = 16 - len(text) % 16
|
|
||||||
text = text + chr(pad) * pad
|
|
||||||
encryptor = AES.new(key.encode('utf-8'), AES.MODE_CBC, IV.encode('utf-8'))
|
|
||||||
encrypt_text = encryptor.encrypt(text.encode('utf-8'))
|
|
||||||
encrypt_text = base64.b64encode(encrypt_text).decode('utf-8')
|
|
||||||
return encrypt_text
|
|
||||||
|
|
||||||
def rsa_encrypt(text, pubkey, modulus):
|
|
||||||
"""RSA加密"""
|
|
||||||
text = text[::-1]
|
|
||||||
rs = pow(int(codecs.encode(text.encode('utf-8'), 'hex'), 16), int(pubkey, 16), int(modulus, 16))
|
|
||||||
return format(rs, 'x').zfill(256)
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
|
|
||||||
'Referer': 'https://music.163.com/',
|
|
||||||
'Origin': 'https://music.163.com',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
|
||||||
}
|
|
||||||
def encrypt_request(text):
|
|
||||||
"""加密请求数据"""
|
|
||||||
secret_key = create_secret_key(16)
|
|
||||||
params = aes_encrypt(text, NONCE)
|
|
||||||
params = aes_encrypt(params, secret_key)
|
|
||||||
encSecKey = rsa_encrypt(secret_key, PUBKEY, MODULUS)
|
|
||||||
return {
|
|
||||||
'params': params,
|
|
||||||
'encSecKey': encSecKey
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_lyrics_from_netease(track_name, artist_name):
|
|
||||||
# 构建搜索关键词
|
|
||||||
search_keyword = f"{track_name} {artist_name}"
|
|
||||||
|
|
||||||
# 搜索歌曲
|
|
||||||
data = {
|
|
||||||
's': search_keyword,
|
|
||||||
'type': 1, # 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单
|
|
||||||
'limit': 5,
|
|
||||||
'offset': 0,
|
|
||||||
'hlpretag': '<span class="s-fc7">',
|
|
||||||
'hlposttag': '</span>',
|
|
||||||
'total': True,
|
|
||||||
'csrf_token': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypted_data = encrypt_request(json.dumps(data))
|
|
||||||
response = requests.post(NETEASE_SEARCH_URL, data=encrypted_data, headers=headers)
|
|
||||||
logger.debug(f"网易云音乐搜索响应: {response.text}")
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
result = response.json()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_lyrics_from_id(id):
|
|
||||||
"""从网易云音乐获取歌词"""
|
|
||||||
# 获取歌词
|
|
||||||
data = {
|
|
||||||
'id': id,
|
|
||||||
'lv': 1, # 获取歌词
|
|
||||||
'kv': 1, # 获取翻译
|
|
||||||
'tv': -1, # 不获取罗马音
|
|
||||||
'csrf_token': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypted_data = encrypt_request(json.dumps(data))
|
|
||||||
response = requests.post(NETEASE_LYRIC_URL, data=encrypted_data, headers=headers)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
result = response.json()
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def search_lyrics(track_name, artist_name):
|
|
||||||
"""从网易云音乐搜索并获取歌词"""
|
|
||||||
try:
|
|
||||||
result = get_lyrics_from_netease(track_name, artist_name)
|
|
||||||
if result['code'] == 200 and result['result']['songCount'] > 0:
|
|
||||||
song = result['result']['songs'][0]
|
|
||||||
id = song['id']
|
|
||||||
lrc_result = get_lyrics_from_id(id)
|
|
||||||
if lrc_result['code'] == 200 and 'lrc' in lrc_result:
|
|
||||||
logger.debug(f"网易云音乐歌词: {lrc_result['lrc']['lyric']}")
|
|
||||||
return lrc_result['lrc']['lyric']
|
|
||||||
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"从网易云音乐获取歌词时出错: {e}", exc_info=True)
|
|
||||||
return None
|
|
||||||
def format_lyrics(lyrics):
|
|
||||||
"""格式化歌词文本,保留时间信息"""
|
|
||||||
if not lyrics:
|
|
||||||
logger.debug("歌词内容为空")
|
|
||||||
return []
|
|
||||||
|
|
||||||
lyric_lines = []
|
|
||||||
for line in lyrics.split('\n'):
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 查找所有时间标记
|
|
||||||
time_tags = re.findall(r'\[([0-9:.]+)\]', line)
|
|
||||||
if not time_tags:
|
|
||||||
continue
|
|
||||||
|
|
||||||
text = re.sub(r'\[[0-9:.]+\]', '', line).strip()
|
|
||||||
if not text:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 可能一行有多个时间标记
|
|
||||||
for time_tag in time_tags:
|
|
||||||
try:
|
|
||||||
time_seconds = parse_time(time_tag)
|
|
||||||
lyric_lines.append(LyricLine(time_seconds, text))
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"解析时间标记出错: {time_tag}, 错误: {e}")
|
|
||||||
|
|
||||||
# 按时间排序
|
|
||||||
sorted_lyrics = sorted(lyric_lines, key=lambda x: x.time)
|
|
||||||
logger.debug(f"解析完成,共 {len(sorted_lyrics)} 行歌词")
|
|
||||||
return sorted_lyrics
|
|
||||||
|
|
||||||
def save_lyrics(track_name, artist, lyrics):
|
|
||||||
"""保存歌词到本地文件,如果已存在则更新"""
|
|
||||||
try:
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute('''
|
|
||||||
INSERT INTO lyrics (track_name, artist, lyrics_content)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
ON CONFLICT(track_name, artist)
|
|
||||||
DO UPDATE SET
|
|
||||||
lyrics_content = excluded.lyrics_content,
|
|
||||||
created_at = CURRENT_TIMESTAMP
|
|
||||||
''', (track_name, artist, lyrics))
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
logger.debug(f"歌词已保存/更新到数据库: {track_name} - {artist}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"保存歌词时出错: {e}", exc_info=True)
|
|
||||||
|
|
||||||
def delete_lyrics(track_name, artist):
|
|
||||||
"""从数据库中删除歌词"""
|
|
||||||
try:
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute('''
|
|
||||||
DELETE FROM lyrics WHERE track_name = ? AND artist = ?
|
|
||||||
''', (track_name, artist))
|
|
||||||
conn.commit()
|
|
||||||
deleted = cursor.rowcount > 0
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if deleted:
|
|
||||||
logger.info(f"已删除歌词: {track_name} - {artist}")
|
|
||||||
# 标记这首歌的歌词已被删除,避免重新获取
|
|
||||||
_lyrics_cache["deleted"] = True
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.warning(f"未找到要删除的歌词: {track_name} - {artist}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"删除歌词时出错: {e}", exc_info=True)
|
|
||||||
return False
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
"""
|
|
||||||
路由模块
|
|
||||||
包含所有API端点的定义和处理逻辑
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
from fastapi import WebSocket, WebSocketDisconnect
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from .apple_music import get_music_status, control_music
|
|
||||||
from .lyrics import get_lyrics_data, delete_lyrics, get_lyrics_from_netease, get_lyrics_from_id, save_lyrics, clear_lyrics_cache
|
|
||||||
from .websocket_manager import ConnectionManager
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# 音乐控制请求模型
|
|
||||||
class ControlRequest(BaseModel):
|
|
||||||
action: str # 'playpause', 'previous', 'next', 'seek'
|
|
||||||
position: float = None # 仅当 action=='seek' 时有效
|
|
||||||
|
|
||||||
# 手动搜索歌词请求模型
|
|
||||||
class SearchLyricsRequest(BaseModel):
|
|
||||||
track_name: str
|
|
||||||
artist: str
|
|
||||||
|
|
||||||
# 获取指定ID的歌词请求模型
|
|
||||||
class GetLyricsFromIdRequest(BaseModel):
|
|
||||||
id: int
|
|
||||||
track_name: str
|
|
||||||
artist: str
|
|
||||||
|
|
||||||
def register_routes(app, manager: ConnectionManager):
|
|
||||||
"""注册所有API路由到FastAPI应用"""
|
|
||||||
|
|
||||||
# 保留向下兼容的HTTP接口
|
|
||||||
@app.get("/status")
|
|
||||||
def status_endpoint():
|
|
||||||
"""获取 Apple Music 播放状态"""
|
|
||||||
return get_music_status()
|
|
||||||
|
|
||||||
@app.get("/lyrics")
|
|
||||||
def lyrics_endpoint():
|
|
||||||
"""获取当前歌词"""
|
|
||||||
status = get_music_status()
|
|
||||||
return get_lyrics_data(status)
|
|
||||||
|
|
||||||
@app.delete("/lyrics")
|
|
||||||
def delete_lyrics_endpoint(track_name: str, artist: str):
|
|
||||||
"""删除指定歌曲的歌词"""
|
|
||||||
success = delete_lyrics(track_name, artist)
|
|
||||||
if success:
|
|
||||||
return {"status": "success", "message": "歌词已删除"}
|
|
||||||
else:
|
|
||||||
return {"status": "error", "message": "未找到要删除的歌词"}
|
|
||||||
|
|
||||||
@app.post("/lyrics/search")
|
|
||||||
def search_lyrics_endpoint(request: SearchLyricsRequest):
|
|
||||||
"""手动搜索歌词"""
|
|
||||||
try:
|
|
||||||
# 搜索歌词
|
|
||||||
result = get_lyrics_from_netease(request.track_name, request.artist)
|
|
||||||
if result['code'] == 200 and result['result']['songCount'] > 0:
|
|
||||||
songs = result['result']['songs']
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"songs": songs
|
|
||||||
}
|
|
||||||
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"message": "未找到歌词"
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"搜索歌词时出错: {e}", exc_info=True)
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"message": f"搜索歌词时出错: {str(e)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.post("/lyrics/getLyricsFromId")
|
|
||||||
def get_lyrics_from_id_endpoint(request: GetLyricsFromIdRequest):
|
|
||||||
"""获取指定ID的歌词"""
|
|
||||||
result = get_lyrics_from_id(request.id)
|
|
||||||
if result['code'] == 200 and 'lrc' in result:
|
|
||||||
logger.debug(f"网易云音乐歌词: {result['lrc']['lyric']}")
|
|
||||||
lyrics_text = result['lrc']['lyric']
|
|
||||||
# 保存到数据库
|
|
||||||
save_lyrics(request.track_name, request.artist, lyrics_text)
|
|
||||||
clear_lyrics_cache()
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 添加播放控制接口
|
|
||||||
@app.post("/control")
|
|
||||||
async def control_endpoint(request: ControlRequest):
|
|
||||||
"""控制Apple Music播放"""
|
|
||||||
result = control_music(request.action, request.position)
|
|
||||||
|
|
||||||
# 如果控制成功,广播更新的状态给所有WebSocket客户端
|
|
||||||
if result["status"] == "success":
|
|
||||||
# 延迟一下,让Apple Music有时间执行命令
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
status = get_music_status()
|
|
||||||
lyrics = get_lyrics_data(status)
|
|
||||||
|
|
||||||
combined_data = {
|
|
||||||
"type": "position_update", # 添加消息类型
|
|
||||||
"timestamp": time.time(),
|
|
||||||
"status": status,
|
|
||||||
"lyrics": lyrics
|
|
||||||
}
|
|
||||||
|
|
||||||
# 异步广播更新
|
|
||||||
asyncio.create_task(manager.broadcast(combined_data))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
# WebSocket端点
|
|
||||||
@app.websocket("/ws")
|
|
||||||
async def websocket_endpoint(websocket: WebSocket):
|
|
||||||
"""WebSocket连接处理,合并推送歌词和状态"""
|
|
||||||
await manager.connect(websocket)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 连接建立后立即发送一次数据
|
|
||||||
status = get_music_status()
|
|
||||||
lyrics = get_lyrics_data(status)
|
|
||||||
|
|
||||||
logger.info(f"发送初始状态数据: 当前歌曲={status.get('track_name', '无')}, 歌词可用={lyrics is not None}")
|
|
||||||
|
|
||||||
combined_data = {
|
|
||||||
"type": "track_change", # 确保添加消息类型
|
|
||||||
"timestamp": time.time(),
|
|
||||||
"status": status,
|
|
||||||
"lyrics": lyrics
|
|
||||||
}
|
|
||||||
|
|
||||||
await websocket.send_json(combined_data)
|
|
||||||
|
|
||||||
# 发送欢迎消息
|
|
||||||
await websocket.send_json({"type": "info", "message": "WebSocket连接已建立"})
|
|
||||||
|
|
||||||
# 连接状态标志,用于安全终止循环
|
|
||||||
is_connected = True
|
|
||||||
|
|
||||||
# 持续接收并处理客户端消息
|
|
||||||
while is_connected:
|
|
||||||
try:
|
|
||||||
# 接收客户端消息,设置超时避免无限阻塞
|
|
||||||
data = await asyncio.wait_for(websocket.receive_json(), timeout=1.0)
|
|
||||||
logger.info(f"收到WebSocket消息: {data}")
|
|
||||||
|
|
||||||
# 处理不同类型的消息
|
|
||||||
if isinstance(data, dict):
|
|
||||||
# 处理请求状态消息
|
|
||||||
if data.get("type") == "request_status":
|
|
||||||
logger.info("客户端请求状态更新")
|
|
||||||
status = get_music_status()
|
|
||||||
lyrics = get_lyrics_data(status)
|
|
||||||
|
|
||||||
update_data = {
|
|
||||||
"type": "track_change", # 确保添加消息类型
|
|
||||||
"timestamp": time.time(),
|
|
||||||
"status": status,
|
|
||||||
"lyrics": lyrics
|
|
||||||
}
|
|
||||||
|
|
||||||
await websocket.send_json(update_data)
|
|
||||||
|
|
||||||
# 处理ping消息
|
|
||||||
elif data.get("type") == "ping":
|
|
||||||
logger.debug("收到客户端ping")
|
|
||||||
await websocket.send_json({
|
|
||||||
"type": "pong",
|
|
||||||
"timestamp": time.time()
|
|
||||||
})
|
|
||||||
except WebSocketDisconnect:
|
|
||||||
# 连接已断开,退出循环
|
|
||||||
logger.info("WebSocket连接断开")
|
|
||||||
is_connected = False
|
|
||||||
break
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
# 接收超时,但连接可能仍然有效,继续循环
|
|
||||||
continue
|
|
||||||
except Exception as e:
|
|
||||||
# 其他错误,记录后继续(除非是连接断开)
|
|
||||||
if "disconnect" in str(e).lower() or "not connected" in str(e).lower():
|
|
||||||
logger.info(f"WebSocket连接可能已断开: {e}")
|
|
||||||
is_connected = False
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
logger.error(f"处理WebSocket消息时出错: {e}")
|
|
||||||
# 继续监听下一条消息,不断开连接
|
|
||||||
continue
|
|
||||||
|
|
||||||
except WebSocketDisconnect:
|
|
||||||
logger.info("WebSocket连接断开 (外层异常)")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"WebSocket错误: {e}")
|
|
||||||
finally:
|
|
||||||
# 确保连接从管理器中移除
|
|
||||||
await manager.disconnect(websocket)
|
|
||||||
logger.info("WebSocket连接已清理")
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
"""
|
|
||||||
WebSocket连接管理模块
|
|
||||||
负责管理WebSocket连接和广播消息
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from typing import List
|
|
||||||
from fastapi import WebSocket
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class ConnectionManager:
|
|
||||||
def __init__(self):
|
|
||||||
self.active_connections: List[WebSocket] = []
|
|
||||||
self._lock = asyncio.Lock() # 添加锁以保护连接列表
|
|
||||||
|
|
||||||
async def connect(self, websocket: WebSocket):
|
|
||||||
await websocket.accept()
|
|
||||||
async with self._lock:
|
|
||||||
# 检查连接是否已经存在,如果存在则不重复添加
|
|
||||||
if websocket not in self.active_connections:
|
|
||||||
self.active_connections.append(websocket)
|
|
||||||
logger.info(f"WebSocket客户端连接,当前连接数: {len(self.active_connections)}")
|
|
||||||
|
|
||||||
async def disconnect(self, websocket: WebSocket):
|
|
||||||
async with self._lock:
|
|
||||||
# 安全移除连接,如果不存在则忽略
|
|
||||||
if websocket in self.active_connections:
|
|
||||||
self.active_connections.remove(websocket)
|
|
||||||
logger.info(f"WebSocket客户端断开,当前连接数: {len(self.active_connections)}")
|
|
||||||
|
|
||||||
async def broadcast(self, data):
|
|
||||||
"""向所有连接的客户端广播数据"""
|
|
||||||
async with self._lock:
|
|
||||||
# 复制列表以避免在迭代时修改
|
|
||||||
connections = self.active_connections.copy()
|
|
||||||
|
|
||||||
# 在锁外处理发送操作
|
|
||||||
disconnect_list = []
|
|
||||||
for connection in connections:
|
|
||||||
try:
|
|
||||||
await connection.send_json(data)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"发送消息失败: {e}")
|
|
||||||
disconnect_list.append(connection)
|
|
||||||
|
|
||||||
# 移除断开的连接
|
|
||||||
if disconnect_list:
|
|
||||||
async with self._lock:
|
|
||||||
for conn in disconnect_list:
|
|
||||||
if conn in self.active_connections:
|
|
||||||
self.active_connections.remove(conn)
|
|
||||||
logger.info(f"移除断开的连接,剩余连接数: {len(self.active_connections)}")
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
fastapi>=0.95.0
|
|
||||||
uvicorn[standard]>=0.22.0
|
|
||||||
websockets>=10.4
|
|
||||||
pydantic>=1.10.7
|
|
||||||
requests>=2.31.0
|
|
||||||
pycryptodome>=3.22.0
|
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
* lyroc 主程序文件
|
* lyroc 主程序文件
|
||||||
* 使用模块化结构,只负责应用的生命周期管理和组件集成
|
* 使用模块化结构,只负责应用的生命周期管理和组件集成
|
||||||
*/
|
*/
|
||||||
const { app, protocol, ipcMain } = require('electron');
|
const { app, protocol, ipcMain } = require("electron");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const isDev = require('electron-is-dev');
|
const isDev = require("electron-is-dev");
|
||||||
|
|
||||||
// 导入自定义模块
|
// 导入自定义模块
|
||||||
const windowManager = require('./modules/window-manager');
|
const windowManager = require("./modules/window-manager");
|
||||||
const backendService = require('./modules/backend-service');
|
const backendService = require("./modules/backend-service");
|
||||||
const logger = require('./modules/logger');
|
const logger = require("./modules/logger");
|
||||||
const searchWindowManager = require('./windows/search-window');
|
const searchWindowManager = require("./windows/search-window");
|
||||||
|
|
||||||
// 应用全局状态
|
// 应用全局状态
|
||||||
global.appReady = false;
|
global.appReady = false;
|
||||||
@@ -21,197 +21,197 @@ global.appReady = false;
|
|||||||
*/
|
*/
|
||||||
async function initApp() {
|
async function initApp() {
|
||||||
try {
|
try {
|
||||||
console.log('初始化应用...');
|
console.log("初始化应用...");
|
||||||
|
|
||||||
// 确保只有一个实例在运行
|
// 确保只有一个实例在运行
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
console.log('另一个实例已经在运行');
|
console.log("另一个实例已经在运行");
|
||||||
app.quit();
|
app.quit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建加载窗口
|
// 创建加载窗口
|
||||||
const loadingWindow = windowManager.createLoadingWindow();
|
const loadingWindow = windowManager.createLoadingWindow();
|
||||||
windowManager.updateLoadingProgress(10, 'initializing');
|
windowManager.updateLoadingProgress(10, "initializing");
|
||||||
|
|
||||||
// 注册文件协议处理器
|
// 注册文件协议处理器
|
||||||
protocol.registerFileProtocol('file', (request, callback) => {
|
protocol.registerFileProtocol("file", (request, callback) => {
|
||||||
const url = request.url.replace('file://', '');
|
const url = request.url.replace("file://", "");
|
||||||
try {
|
try {
|
||||||
return callback(decodeURIComponent(url));
|
return callback(decodeURIComponent(url));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('协议处理器错误:', error);
|
console.error("协议处理器错误:", error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查系统环境
|
// 检查系统环境
|
||||||
console.log('运行模式:', isDev ? '开发' : '生产');
|
console.log("运行模式:", isDev ? "开发" : "生产");
|
||||||
|
|
||||||
// 清理可能运行的Python进程
|
// 清理可能运行的Python进程
|
||||||
await backendService.cleanupExistingProcesses();
|
await backendService.cleanupExistingProcesses();
|
||||||
|
|
||||||
// 检查端口可用性
|
// 检查端口可用性
|
||||||
await backendService.checkPort();
|
await backendService.checkPort();
|
||||||
windowManager.updateLoadingProgress(30, 'allocatingPorts');
|
windowManager.updateLoadingProgress(30, "allocatingPorts");
|
||||||
|
|
||||||
// 获取前端随机端口
|
// 获取前端随机端口
|
||||||
await backendService.getFrontendPort();
|
await backendService.getFrontendPort();
|
||||||
|
|
||||||
// 启动Python后端
|
// 启动Deno后端
|
||||||
windowManager.updateLoadingProgress(40, 'loadingBackend');
|
windowManager.updateLoadingProgress(40, "loadingBackend");
|
||||||
await backendService.startPythonBackend(isDev);
|
await backendService.startDenoBackend(isDev);
|
||||||
|
|
||||||
// 更新加载进度
|
// 更新加载进度
|
||||||
windowManager.updateLoadingProgress(60, 'loadingFrontend');
|
windowManager.updateLoadingProgress(60, "loadingFrontend");
|
||||||
|
|
||||||
// 初始化窗口配置
|
// 初始化窗口配置
|
||||||
windowManager.initConfig(app.getPath('userData'));
|
windowManager.initConfig(app.getPath("userData"));
|
||||||
|
|
||||||
// 创建窗口
|
// 创建窗口
|
||||||
windowManager.updateLoadingProgress(70, 'connecting');
|
windowManager.updateLoadingProgress(70, "connecting");
|
||||||
const mainWindow = windowManager.createWindow(
|
const mainWindow = windowManager.createWindow(
|
||||||
null,
|
null,
|
||||||
backendService.getFrontendPortNumber(),
|
backendService.getFrontendPortNumber(),
|
||||||
backendService.getBackendPort(),
|
backendService.getBackendPort(),
|
||||||
isDev
|
isDev
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新加载进度
|
// 更新加载进度
|
||||||
windowManager.updateLoadingProgress(80, 'connecting');
|
windowManager.updateLoadingProgress(80, "connecting");
|
||||||
|
|
||||||
// 设置窗口事件
|
// 设置窗口事件
|
||||||
windowManager.setupWindowEvents();
|
windowManager.setupWindowEvents();
|
||||||
|
|
||||||
// 设置IPC通信
|
// 设置IPC通信
|
||||||
windowManager.setupIPC(
|
windowManager.setupIPC(
|
||||||
backendService.getBackendPort(),
|
backendService.getBackendPort(),
|
||||||
backendService.getFrontendPortNumber()
|
backendService.getFrontendPortNumber()
|
||||||
);
|
);
|
||||||
|
|
||||||
// 处理搜索窗口相关事件
|
// 处理搜索窗口相关事件
|
||||||
ipcMain.on('open-search-window', () => {
|
ipcMain.on("open-search-window", () => {
|
||||||
console.log('收到打开搜索窗口请求');
|
console.log("收到打开搜索窗口请求");
|
||||||
searchWindowManager.createWindow();
|
searchWindowManager.createWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('close-search-window', () => {
|
ipcMain.on("close-search-window", () => {
|
||||||
console.log('收到关闭搜索窗口请求');
|
console.log("收到关闭搜索窗口请求");
|
||||||
searchWindowManager.closeWindow();
|
searchWindowManager.closeWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理发送数据到搜索窗口的请求
|
// 处理发送数据到搜索窗口的请求
|
||||||
ipcMain.on('send-to-search-window', (_, data) => {
|
ipcMain.on("send-to-search-window", (_, data) => {
|
||||||
console.log('收到发送到搜索窗口的数据:', data);
|
console.log("收到发送到搜索窗口的数据:", data);
|
||||||
searchWindowManager.receiveMainWindowData(data);
|
searchWindowManager.receiveMainWindowData(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理搜索窗口请求主窗口数据
|
// 处理搜索窗口请求主窗口数据
|
||||||
ipcMain.on('request-main-window-data', () => {
|
ipcMain.on("request-main-window-data", () => {
|
||||||
console.log('收到搜索窗口数据请求');
|
console.log("收到搜索窗口数据请求");
|
||||||
// 获取主窗口数据并发送
|
// 获取主窗口数据并发送
|
||||||
const mainWindow = windowManager.mainWindow();
|
const mainWindow = windowManager.mainWindow();
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('get-main-window-data');
|
mainWindow.webContents.send("get-main-window-data");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新加载进度
|
// 更新加载进度
|
||||||
windowManager.updateLoadingProgress(90, 'loadingLyrics');
|
windowManager.updateLoadingProgress(90, "loadingLyrics");
|
||||||
|
|
||||||
// 监听主窗口内容加载完成事件
|
// 监听主窗口内容加载完成事件
|
||||||
mainWindow.webContents.once('did-finish-load', () => {
|
mainWindow.webContents.once("did-finish-load", () => {
|
||||||
windowManager.updateLoadingProgress(100, 'ready');
|
windowManager.updateLoadingProgress(100, "ready");
|
||||||
|
|
||||||
// 短暂延迟后关闭加载窗口
|
// 短暂延迟后关闭加载窗口
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 不需要在这里显示主窗口,动画结束时会自动显示
|
// 不需要在这里显示主窗口,动画结束时会自动显示
|
||||||
windowManager.closeLoadingWindow();
|
windowManager.closeLoadingWindow();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// 设置应用就绪标志
|
// 设置应用就绪标志
|
||||||
global.appReady = true;
|
global.appReady = true;
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('应用初始化失败:', err);
|
console.error("应用初始化失败:", err);
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Electron 应用就绪时
|
// Electron 应用就绪时
|
||||||
app.on('ready', () => {
|
app.on("ready", () => {
|
||||||
// 设置日志记录
|
// 设置日志记录
|
||||||
const logPath = logger.setupLogging(app.getPath('userData'));
|
const logPath = logger.setupLogging(app.getPath("userData"));
|
||||||
|
|
||||||
// 添加日志以显示Electron路径
|
// 添加日志以显示Electron路径
|
||||||
console.log('Electron应用已准备就绪');
|
console.log("Electron应用已准备就绪");
|
||||||
console.log('应用路径:', app.getAppPath());
|
console.log("应用路径:", app.getAppPath());
|
||||||
console.log('__dirname:', __dirname);
|
console.log("__dirname:", __dirname);
|
||||||
console.log('用户数据路径:', app.getPath('userData'));
|
console.log("用户数据路径:", app.getPath("userData"));
|
||||||
console.log('日志文件路径:', logPath);
|
console.log("日志文件路径:", logPath);
|
||||||
|
|
||||||
// 检查资源路径
|
// 检查资源路径
|
||||||
const resourcePath = isDev
|
const resourcePath = isDev
|
||||||
? path.join(__dirname, '..', 'backend')
|
? path.join(__dirname, "..", "backend")
|
||||||
: path.join(process.resourcesPath, 'backend');
|
: path.join(process.resourcesPath, "backend");
|
||||||
console.log('后端资源路径:', resourcePath);
|
console.log("后端资源路径:", resourcePath);
|
||||||
console.log('该路径存在:', fs.existsSync(resourcePath));
|
console.log("该路径存在:", fs.existsSync(resourcePath));
|
||||||
|
|
||||||
// 隐藏dock栏图标
|
// 隐藏dock栏图标
|
||||||
if (process.platform === 'darwin' && app.dock) {
|
if (process.platform === "darwin" && app.dock) {
|
||||||
app.dock.hide();
|
app.dock.hide();
|
||||||
console.log('已隐藏Dock图标');
|
console.log("已隐藏Dock图标");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动应用初始化
|
// 启动应用初始化
|
||||||
initApp()
|
initApp()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('应用初始化完成');
|
console.log("应用初始化完成");
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error('初始化应用失败:', err);
|
console.error("初始化应用失败:", err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 所有窗口关闭时退出应用
|
// 所有窗口关闭时退出应用
|
||||||
app.on('window-all-closed', function() {
|
app.on("window-all-closed", function () {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== "darwin") {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 应用激活时
|
// 应用激活时
|
||||||
app.on('activate', function() {
|
app.on("activate", function () {
|
||||||
if (!windowManager.mainWindow()) {
|
if (!windowManager.mainWindow()) {
|
||||||
// 初始化窗口配置
|
// 初始化窗口配置
|
||||||
windowManager.initConfig(app.getPath('userData'));
|
windowManager.initConfig(app.getPath("userData"));
|
||||||
|
|
||||||
// 重新创建窗口
|
// 重新创建窗口
|
||||||
const mainWindow = windowManager.createWindow(
|
const mainWindow = windowManager.createWindow(
|
||||||
null,
|
null,
|
||||||
backendService.getFrontendPortNumber(),
|
backendService.getFrontendPortNumber(),
|
||||||
backendService.getBackendPort(),
|
backendService.getBackendPort(),
|
||||||
isDev
|
isDev
|
||||||
);
|
);
|
||||||
|
|
||||||
// 设置窗口事件
|
// 设置窗口事件
|
||||||
windowManager.setupWindowEvents();
|
windowManager.setupWindowEvents();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 应用退出前
|
// 应用退出前
|
||||||
app.on('before-quit', () => {
|
app.on("before-quit", () => {
|
||||||
// 清理Python进程
|
// 清理Python进程
|
||||||
backendService.stopPythonBackend();
|
backendService.stopPythonBackend();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听第二实例启动事件
|
// 监听第二实例启动事件
|
||||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
app.on("second-instance", (event, commandLine, workingDirectory) => {
|
||||||
// 当尝试启动第二个实例时,让主窗口获得焦点
|
// 当尝试启动第二个实例时,让主窗口获得焦点
|
||||||
const mainWindow = windowManager.mainWindow();
|
const mainWindow = windowManager.mainWindow();
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
console.log('已有实例在运行,已将其窗口聚焦');
|
console.log("已有实例在运行,已将其窗口聚焦");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* 后端服务管理模块
|
* 后端服务管理模块
|
||||||
* 负责启动、管理和监控Python后端服务
|
* 负责启动、管理和监控Deno后端服务
|
||||||
*/
|
*/
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require("child_process");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const findProcess = require('find-process');
|
const findProcess = require("find-process");
|
||||||
const getPort = require('get-port');
|
const getPort = require("get-port");
|
||||||
|
|
||||||
// 存储Python进程引用
|
// 存储Deno进程引用
|
||||||
let pythonProcess = null;
|
let denoProcess = null;
|
||||||
let backendPort = 5000;
|
let backendPort = 5005;
|
||||||
let frontendPort = 5173;
|
let frontendPort = 5173;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,12 +19,12 @@ let frontendPort = 5173;
|
|||||||
*/
|
*/
|
||||||
async function checkPort() {
|
async function checkPort() {
|
||||||
try {
|
try {
|
||||||
// 获取可用端口,首选5000
|
// 获取可用端口,首选5005
|
||||||
backendPort = await getPort({ port: 5000 });
|
backendPort = await getPort({ port: 5005 });
|
||||||
console.log(`后端将使用端口: ${backendPort}`);
|
console.log(`后端将使用端口: ${backendPort}`);
|
||||||
return backendPort;
|
return backendPort;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取可用端口失败:', err);
|
console.error("获取可用端口失败:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,179 +40,195 @@ async function getFrontendPort() {
|
|||||||
console.log(`前端将使用随机端口: ${frontendPort}`);
|
console.log(`前端将使用随机端口: ${frontendPort}`);
|
||||||
return frontendPort;
|
return frontendPort;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取前端端口失败:', err);
|
console.error("获取前端端口失败:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理已存在的Python进程
|
* 清理已存在的Deno进程
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function cleanupExistingProcesses() {
|
async function cleanupExistingProcesses() {
|
||||||
try {
|
try {
|
||||||
const processList = await findProcess('port', 5000);
|
const processList = await findProcess("port", 5005);
|
||||||
|
|
||||||
for (const proc of processList) {
|
for (const proc of processList) {
|
||||||
if (proc.name.includes('python')) {
|
if (proc.name.includes("deno")) {
|
||||||
console.log(`杀死已存在的Python进程: PID ${proc.pid}`);
|
console.log(`杀死已存在的Deno进程: PID ${proc.pid}`);
|
||||||
process.kill(proc.pid, 'SIGKILL');
|
process.kill(proc.pid, "SIGKILL");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('清理进程失败:', err);
|
console.error("清理进程失败:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动Python后端服务
|
* 启动Deno后端服务
|
||||||
* @param {boolean} isDev 是否为开发模式
|
* @param {boolean} isDev 是否为开发模式
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function startPythonBackend(isDev) {
|
async function startDenoBackend(isDev) {
|
||||||
try {
|
try {
|
||||||
// 在开发模式和生产模式下使用不同的路径
|
// 在开发模式和生产模式下使用不同的路径
|
||||||
let pythonPath;
|
|
||||||
let scriptPath;
|
let scriptPath;
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
// 开发模式:使用项目中的虚拟环境
|
// 开发模式:使用项目中的Deno后端
|
||||||
const venvPath = path.join(__dirname, '..', '..', 'backend', 'venv');
|
scriptPath = path.join(__dirname, "..", "..", "backend-deno", "main.ts");
|
||||||
pythonPath = process.platform === 'win32'
|
|
||||||
? path.join(venvPath, 'Scripts', 'python.exe')
|
|
||||||
: path.join(venvPath, 'bin', 'python3');
|
|
||||||
scriptPath = path.join(__dirname, '..', '..', 'backend', 'main.py');
|
|
||||||
} else {
|
} else {
|
||||||
// 生产模式:使用打包的虚拟环境
|
// 生产模式:使用打包的Deno后端
|
||||||
const venvPath = path.join(process.resourcesPath, 'backend', 'venv');
|
scriptPath = path.join(process.resourcesPath, "backend-deno", "main.ts");
|
||||||
pythonPath = process.platform === 'win32'
|
|
||||||
? path.join(venvPath, 'Scripts', 'python.exe')
|
|
||||||
: path.join(venvPath, 'bin', 'python3');
|
|
||||||
scriptPath = path.join(process.resourcesPath, 'backend', 'main.py');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`启动Python后端: ${scriptPath} 在端口 ${backendPort}`);
|
console.log(`启动Deno后端: ${scriptPath} 在端口 ${backendPort}`);
|
||||||
console.log(`后端脚本路径存在: ${fs.existsSync(scriptPath)}`);
|
console.log(`后端脚本路径存在: ${fs.existsSync(scriptPath)}`);
|
||||||
console.log(`Python解释器路径: ${pythonPath}`);
|
|
||||||
|
// 检查Deno是否安装
|
||||||
// 检查Python解释器是否存在
|
try {
|
||||||
if (!fs.existsSync(pythonPath)) {
|
const denoCheck = spawn("deno", ["--version"], { stdio: "pipe" });
|
||||||
throw new Error(`Python解释器不存在: ${pythonPath}`);
|
denoCheck.on("error", (err) => {
|
||||||
|
throw new Error(`Deno未安装或不在PATH中: ${err.message}`);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Deno未安装或不在PATH中: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打印Python脚本目录内容
|
// 打印Deno脚本目录内容
|
||||||
try {
|
try {
|
||||||
const scriptDir = path.dirname(scriptPath);
|
const scriptDir = path.dirname(scriptPath);
|
||||||
console.log(`后端目录内容: ${fs.readdirSync(scriptDir).join(', ')}`);
|
console.log(`后端目录内容: ${fs.readdirSync(scriptDir).join(", ")}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`无法读取后端目录: ${err}`);
|
console.error(`无法读取后端目录: ${err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动子进程
|
// 启动子进程
|
||||||
pythonProcess = spawn(pythonPath, [scriptPath], {
|
denoProcess = spawn(
|
||||||
env: { ...process.env, PORT: backendPort.toString() },
|
"deno",
|
||||||
stdio: 'pipe' // 确保可以读取标准输出和错误
|
[
|
||||||
});
|
"run",
|
||||||
|
"--allow-net",
|
||||||
// 输出Python进程的日志
|
"--allow-run",
|
||||||
pythonProcess.stdout.on('data', (data) => {
|
"--allow-read",
|
||||||
|
"--allow-write",
|
||||||
|
"--allow-env",
|
||||||
|
scriptPath,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
env: { ...process.env, PORT: backendPort.toString() },
|
||||||
|
stdio: "pipe", // 确保可以读取标准输出和错误
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 输出Deno进程的日志
|
||||||
|
denoProcess.stdout.on("data", (data) => {
|
||||||
const output = data.toString().trim();
|
const output = data.toString().trim();
|
||||||
console.log(`Python后端输出: ${output}`);
|
console.log(`Deno后端输出: ${output}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
pythonProcess.stderr.on('data', (data) => {
|
denoProcess.stderr.on("data", (data) => {
|
||||||
const message = data.toString().trim();
|
const message = data.toString().trim();
|
||||||
// 判断是否是错误日志还是普通日志
|
// 判断是否是错误日志还是普通日志
|
||||||
if (message.includes('ERROR') || message.includes('CRITICAL') || message.includes('WARN')) {
|
if (
|
||||||
console.error(`Python后端错误: ${message}`);
|
message.includes("ERROR") ||
|
||||||
|
message.includes("CRITICAL") ||
|
||||||
|
message.includes("WARN")
|
||||||
|
) {
|
||||||
|
console.error(`Deno后端错误: ${message}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Python后端日志: ${message}`);
|
console.log(`Deno后端日志: ${message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
pythonProcess.on('close', (code) => {
|
denoProcess.on("close", (code) => {
|
||||||
console.log(`Python后端退出,退出码: ${code}`);
|
console.log(`Deno后端退出,退出码: ${code}`);
|
||||||
// 如果应用仍在运行,尝试重启后端
|
// 如果应用仍在运行,尝试重启后端
|
||||||
if (global.appReady && code !== 0) {
|
if (global.appReady && code !== 0) {
|
||||||
console.log('尝试重启Python后端...');
|
console.log("尝试重启Deno后端...");
|
||||||
setTimeout(() => startPythonBackend(isDev), 1000);
|
setTimeout(() => startDenoBackend(isDev), 1000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 等待后端启动 - 改为检测后端是否真正启动完成而非固定等待时间
|
// 等待后端启动 - 改为检测后端是否真正启动完成而非固定等待时间
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 后端启动超时时间,开发模式较短,生产模式较长
|
// 后端启动超时时间,开发模式较短,生产模式较长
|
||||||
const maxTimeout = isDev ? 15000 : 30000;
|
const maxTimeout = isDev ? 15000 : 30000;
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
let backendReady = false;
|
let backendReady = false;
|
||||||
|
|
||||||
// 添加额外的日志解析以检测后端就绪状态
|
// 添加额外的日志解析以检测后端就绪状态
|
||||||
pythonProcess.stdout.on('data', (data) => {
|
denoProcess.stdout.on("data", (data) => {
|
||||||
const output = data.toString().trim();
|
const output = data.toString().trim();
|
||||||
console.log(`Python后端输出: ${output}`);
|
console.log(`Deno后端输出: ${output}`);
|
||||||
|
|
||||||
// 检测后端就绪信号(uvicorn输出"Application startup complete"表示应用已启动)
|
// 检测后端就绪信号(Deno输出"启动后端服务器在端口"表示应用已启动)
|
||||||
if (output.includes('Application startup complete') || output.includes('INFO: Application startup complete')) {
|
if (
|
||||||
console.log('检测到后端应用启动完成信号');
|
output.includes("启动后端服务器在端口") ||
|
||||||
|
output.includes("Server running")
|
||||||
|
) {
|
||||||
|
console.log("检测到后端应用启动完成信号");
|
||||||
backendReady = true;
|
backendReady = true;
|
||||||
// 再等待短暂时间确保所有服务都初始化完成
|
// 再等待短暂时间确保所有服务都初始化完成
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('后端已完全启动,准备创建前端窗口');
|
console.log("后端已完全启动,准备创建前端窗口");
|
||||||
resolve();
|
resolve();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 实现HTTP测试以检测后端是否正常响应
|
// 实现HTTP测试以检测后端是否正常响应
|
||||||
const testBackendConnection = () => {
|
const testBackendConnection = () => {
|
||||||
const http = require('http');
|
const http = require("http");
|
||||||
const testUrl = `http://127.0.0.1:${backendPort}/`;
|
const testUrl = `http://127.0.0.1:${backendPort}/`;
|
||||||
|
|
||||||
// 如果已经检测到后端就绪,不再继续测试
|
// 如果已经检测到后端就绪,不再继续测试
|
||||||
if (backendReady) return;
|
if (backendReady) return;
|
||||||
|
|
||||||
// 检查是否超时
|
// 检查是否超时
|
||||||
if (Date.now() - startTime > maxTimeout) {
|
if (Date.now() - startTime > maxTimeout) {
|
||||||
console.warn(`后端启动超时(${maxTimeout}ms),将继续尝试启动前端`);
|
console.warn(`后端启动超时(${maxTimeout}ms),将继续尝试启动前端`);
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
http.get(testUrl, (res) => {
|
http
|
||||||
if (res.statusCode === 200 || res.statusCode === 404) {
|
.get(testUrl, (res) => {
|
||||||
// 404也表示服务器在运行,只是路径不存在
|
if (res.statusCode === 200 || res.statusCode === 404) {
|
||||||
console.log('通过HTTP检测确认后端已启动');
|
// 404也表示服务器在运行,只是路径不存在
|
||||||
backendReady = true;
|
console.log("通过HTTP检测确认后端已启动");
|
||||||
resolve();
|
backendReady = true;
|
||||||
} else {
|
resolve();
|
||||||
console.log(`后端响应状态码: ${res.statusCode},继续等待...`);
|
} else {
|
||||||
|
console.log(`后端响应状态码: ${res.statusCode},继续等待...`);
|
||||||
|
// 短时间后再次测试
|
||||||
|
setTimeout(testBackendConnection, 1000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
console.log(`后端连接测试失败: ${err.message}, 继续等待...`);
|
||||||
// 短时间后再次测试
|
// 短时间后再次测试
|
||||||
setTimeout(testBackendConnection, 1000);
|
setTimeout(testBackendConnection, 1000);
|
||||||
}
|
});
|
||||||
}).on('error', (err) => {
|
|
||||||
console.log(`后端连接测试失败: ${err.message}, 继续等待...`);
|
|
||||||
// 短时间后再次测试
|
|
||||||
setTimeout(testBackendConnection, 1000);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 启动后端连接测试
|
// 启动后端连接测试
|
||||||
setTimeout(testBackendConnection, 1000);
|
setTimeout(testBackendConnection, 1000);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('启动Python后端失败:', err);
|
console.error("启动Python后端失败:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止Python后端
|
* 停止Deno后端
|
||||||
*/
|
*/
|
||||||
function stopPythonBackend() {
|
function stopDenoBackend() {
|
||||||
if (pythonProcess) {
|
if (denoProcess) {
|
||||||
console.log('终止Python后端进程...');
|
console.log("终止Deno后端进程...");
|
||||||
pythonProcess.kill();
|
denoProcess.kill();
|
||||||
pythonProcess = null;
|
denoProcess = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,9 +236,9 @@ module.exports = {
|
|||||||
checkPort,
|
checkPort,
|
||||||
getFrontendPort,
|
getFrontendPort,
|
||||||
cleanupExistingProcesses,
|
cleanupExistingProcesses,
|
||||||
startPythonBackend,
|
startDenoBackend,
|
||||||
stopPythonBackend,
|
stopDenoBackend,
|
||||||
getBackendPort: () => backendPort,
|
getBackendPort: () => backendPort,
|
||||||
getFrontendPortNumber: () => frontendPort,
|
getFrontendPortNumber: () => frontendPort,
|
||||||
getPythonProcess: () => pythonProcess
|
getDenoProcess: () => denoProcess,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -113,16 +113,57 @@ function setupWebSocketHandlers() {
|
|||||||
websocketService.registerHandler('info', (data) => {
|
websocketService.registerHandler('info', (data) => {
|
||||||
console.log('服务器消息:', data.message);
|
console.log('服务器消息:', data.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 处理后端Deno发出的综合更新消息(包含music与lyrics)
|
||||||
|
websocketService.registerHandler('music_update', (data) => {
|
||||||
|
console.log('收到 music_update');
|
||||||
|
const status = data.music;
|
||||||
|
const lyrics = data.lyrics;
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
positionTracker.updateState(
|
||||||
|
status.status,
|
||||||
|
status.position,
|
||||||
|
status.duration
|
||||||
|
);
|
||||||
|
// 填充歌曲信息(在没有歌词数据提供时)
|
||||||
|
if (status.track_name) trackName.value = status.track_name;
|
||||||
|
if (status.artist) artistName.value = status.artist;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lyrics) {
|
||||||
|
isLoadingLyrics.value = false;
|
||||||
|
currentLyric.value = lyrics.current_lyric || '';
|
||||||
|
nextLyric.value = lyrics.next_lyric || '';
|
||||||
|
nextNextLyric.value = lyrics.next_next_lyric || '';
|
||||||
|
if (lyrics.track_name) trackName.value = lyrics.track_name;
|
||||||
|
if (lyrics.artist_name) artistName.value = lyrics.artist_name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理后端Deno发出的仅歌词更新
|
||||||
|
websocketService.registerHandler('lyrics_update', (data) => {
|
||||||
|
console.log('收到 lyrics_update');
|
||||||
|
const lyrics = data.lyrics;
|
||||||
|
if (lyrics) {
|
||||||
|
isLoadingLyrics.value = false;
|
||||||
|
currentLyric.value = lyrics.current_lyric || '';
|
||||||
|
nextLyric.value = lyrics.next_lyric || '';
|
||||||
|
nextNextLyric.value = lyrics.next_next_lyric || '';
|
||||||
|
if (lyrics.track_name) trackName.value = lyrics.track_name;
|
||||||
|
if (lyrics.artist_name) artistName.value = lyrics.artist_name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 处理心跳响应
|
// 处理心跳响应
|
||||||
websocketService.registerHandler('pong', (data) => {
|
websocketService.registerHandler('pong', (data) => {
|
||||||
console.log('收到服务器心跳响应');
|
console.log('收到服务器心跳响应');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理轨道变化消息 - 完整更新
|
// 处理轨道变化消息 - 完整更新
|
||||||
websocketService.registerHandler('track_change', (data) => {
|
websocketService.registerHandler('track_change', (data) => {
|
||||||
console.log('收到轨道变化:', data);
|
console.log('收到轨道变化:', data);
|
||||||
|
|
||||||
// 更新所有数据
|
// 更新所有数据
|
||||||
if (data.status) {
|
if (data.status) {
|
||||||
positionTracker.updateState(
|
positionTracker.updateState(
|
||||||
@@ -131,20 +172,20 @@ function setupWebSocketHandlers() {
|
|||||||
data.status.duration
|
data.status.duration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.lyrics) {
|
if (data.lyrics) {
|
||||||
// 有歌词数据,重置加载状态
|
// 有歌词数据,重置加载状态
|
||||||
isLoadingLyrics.value = false;
|
isLoadingLyrics.value = false;
|
||||||
|
|
||||||
console.log('更新歌词:', data.lyrics);
|
console.log('更新歌词:', data.lyrics);
|
||||||
currentLyric.value = data.lyrics.current_lyric || '';
|
currentLyric.value = data.lyrics.current_lyric || '';
|
||||||
nextLyric.value = data.lyrics.next_lyric || '';
|
nextLyric.value = data.lyrics.next_lyric || '';
|
||||||
nextNextLyric.value = data.lyrics.next_next_lyric || '';
|
nextNextLyric.value = data.lyrics.next_next_lyric || '';
|
||||||
|
|
||||||
if (data.lyrics.track_name) {
|
if (data.lyrics.track_name) {
|
||||||
trackName.value = data.lyrics.track_name;
|
trackName.value = data.lyrics.track_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.lyrics.artist_name) {
|
if (data.lyrics.artist_name) {
|
||||||
artistName.value = data.lyrics.artist_name;
|
artistName.value = data.lyrics.artist_name;
|
||||||
}
|
}
|
||||||
@@ -152,12 +193,12 @@ function setupWebSocketHandlers() {
|
|||||||
// 没有歌词数据,设置加载状态
|
// 没有歌词数据,设置加载状态
|
||||||
console.log('歌曲切换,但没有歌词数据,显示加载中...');
|
console.log('歌曲切换,但没有歌词数据,显示加载中...');
|
||||||
isLoadingLyrics.value = true;
|
isLoadingLyrics.value = true;
|
||||||
|
|
||||||
// 清空歌词显示
|
// 清空歌词显示
|
||||||
currentLyric.value = '';
|
currentLyric.value = '';
|
||||||
nextLyric.value = '';
|
nextLyric.value = '';
|
||||||
nextNextLyric.value = '';
|
nextNextLyric.value = '';
|
||||||
|
|
||||||
// 可以保留歌曲信息(如果status中有的话)
|
// 可以保留歌曲信息(如果status中有的话)
|
||||||
if (data.status) {
|
if (data.status) {
|
||||||
trackName.value = data.status.track_name || trackName.value;
|
trackName.value = data.status.track_name || trackName.value;
|
||||||
@@ -165,11 +206,11 @@ function setupWebSocketHandlers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理歌词变化消息 - 只更新歌词和相关状态
|
// 处理歌词变化消息 - 只更新歌词和相关状态
|
||||||
websocketService.registerHandler('lyric_change', (data) => {
|
websocketService.registerHandler('lyric_change', (data) => {
|
||||||
console.log('收到歌词变化');
|
console.log('收到歌词变化');
|
||||||
|
|
||||||
// 更新时间相关状态
|
// 更新时间相关状态
|
||||||
if (data.status) {
|
if (data.status) {
|
||||||
positionTracker.updateState(
|
positionTracker.updateState(
|
||||||
@@ -178,37 +219,37 @@ function setupWebSocketHandlers() {
|
|||||||
data.status.duration
|
data.status.duration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新歌词
|
// 更新歌词
|
||||||
if (data.lyrics) {
|
if (data.lyrics) {
|
||||||
// 有歌词数据,重置加载状态
|
// 有歌词数据,重置加载状态
|
||||||
isLoadingLyrics.value = false;
|
isLoadingLyrics.value = false;
|
||||||
|
|
||||||
currentLyric.value = data.lyrics.current_lyric || '';
|
currentLyric.value = data.lyrics.current_lyric || '';
|
||||||
nextLyric.value = data.lyrics.next_lyric || '';
|
nextLyric.value = data.lyrics.next_lyric || '';
|
||||||
nextNextLyric.value = data.lyrics.next_next_lyric || '';
|
nextNextLyric.value = data.lyrics.next_next_lyric || '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理新增的歌词更新消息类型 - 专门用于接收后期加载的歌词
|
// 处理新增的歌词更新消息类型 - 专门用于接收后期加载的歌词
|
||||||
websocketService.registerHandler('lyric_update', (data) => {
|
websocketService.registerHandler('lyric_update', (data) => {
|
||||||
console.log('收到歌词更新消息');
|
console.log('收到歌词更新消息');
|
||||||
|
|
||||||
if (data.lyrics) {
|
if (data.lyrics) {
|
||||||
// 加载完成,重置加载状态
|
// 加载完成,重置加载状态
|
||||||
isLoadingLyrics.value = false;
|
isLoadingLyrics.value = false;
|
||||||
|
|
||||||
// 更新歌词内容
|
// 更新歌词内容
|
||||||
currentLyric.value = data.lyrics.current_lyric || '';
|
currentLyric.value = data.lyrics.current_lyric || '';
|
||||||
nextLyric.value = data.lyrics.next_lyric || '';
|
nextLyric.value = data.lyrics.next_lyric || '';
|
||||||
nextNextLyric.value = data.lyrics.next_next_lyric || '';
|
nextNextLyric.value = data.lyrics.next_next_lyric || '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理播放停止消息 - 更新播放状态
|
// 处理播放停止消息 - 更新播放状态
|
||||||
websocketService.registerHandler('playback_stopped', (data) => {
|
websocketService.registerHandler('playback_stopped', (data) => {
|
||||||
console.log('收到播放停止消息');
|
console.log('收到播放停止消息');
|
||||||
|
|
||||||
// 更新播放状态
|
// 更新播放状态
|
||||||
positionTracker.updateState(
|
positionTracker.updateState(
|
||||||
data.status ? data.status.status : 'stopped',
|
data.status ? data.status.status : 'stopped',
|
||||||
@@ -216,11 +257,11 @@ function setupWebSocketHandlers() {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理播放暂停消息 - 线控暂停
|
// 处理播放暂停消息 - 线控暂停
|
||||||
websocketService.registerHandler('playback_paused', (data) => {
|
websocketService.registerHandler('playback_paused', (data) => {
|
||||||
console.log('收到播放暂停消息 (线控暂停)');
|
console.log('收到播放暂停消息 (线控暂停)');
|
||||||
|
|
||||||
// 更新播放状态
|
// 更新播放状态
|
||||||
positionTracker.updateState(
|
positionTracker.updateState(
|
||||||
'paused',
|
'paused',
|
||||||
@@ -228,11 +269,11 @@ function setupWebSocketHandlers() {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理播放恢复消息 - 从暂停恢复
|
// 处理播放恢复消息 - 从暂停恢复
|
||||||
websocketService.registerHandler('playback_resumed', (data) => {
|
websocketService.registerHandler('playback_resumed', (data) => {
|
||||||
console.log('收到播放恢复消息');
|
console.log('收到播放恢复消息');
|
||||||
|
|
||||||
// 更新播放状态
|
// 更新播放状态
|
||||||
positionTracker.updateState(
|
positionTracker.updateState(
|
||||||
'playing',
|
'playing',
|
||||||
@@ -240,32 +281,32 @@ function setupWebSocketHandlers() {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理Apple Music未运行消息
|
// 处理Apple Music未运行消息
|
||||||
websocketService.registerHandler('app_not_running', (data) => {
|
websocketService.registerHandler('app_not_running', (data) => {
|
||||||
console.log('收到Apple Music未运行消息');
|
console.log('收到Apple Music未运行消息');
|
||||||
|
|
||||||
// 更新播放状态为notrunning
|
// 更新播放状态为notrunning
|
||||||
positionTracker.updateState(
|
positionTracker.updateState(
|
||||||
'notrunning',
|
'notrunning',
|
||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
// 清空歌曲信息
|
// 清空歌曲信息
|
||||||
trackName.value = null;
|
trackName.value = null;
|
||||||
artistName.value = null;
|
artistName.value = null;
|
||||||
|
|
||||||
// 清空歌词显示
|
// 清空歌词显示
|
||||||
currentLyric.value = '';
|
currentLyric.value = '';
|
||||||
nextLyric.value = '';
|
nextLyric.value = '';
|
||||||
nextNextLyric.value = '';
|
nextNextLyric.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理位置更新消息 - 只更新播放位置
|
// 处理位置更新消息 - 只更新播放位置
|
||||||
websocketService.registerHandler('position_update', (data) => {
|
websocketService.registerHandler('position_update', (data) => {
|
||||||
console.log('收到位置更新消息');
|
console.log('收到位置更新消息');
|
||||||
|
|
||||||
// 更新位置信息
|
// 更新位置信息
|
||||||
if (data.status && data.status.position !== undefined) {
|
if (data.status && data.status.position !== undefined) {
|
||||||
positionTracker.updateState(
|
positionTracker.updateState(
|
||||||
@@ -302,10 +343,10 @@ const handleMouseLeave = () => {
|
|||||||
// 组件挂载时初始化
|
// 组件挂载时初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('组件已挂载');
|
console.log('组件已挂载');
|
||||||
|
|
||||||
// 注册WebSocket消息处理器
|
// 注册WebSocket消息处理器
|
||||||
setupWebSocketHandlers();
|
setupWebSocketHandlers();
|
||||||
|
|
||||||
// 检查是否在Electron环境中,获取后端URL
|
// 检查是否在Electron环境中,获取后端URL
|
||||||
if (window.electronAPI) {
|
if (window.electronAPI) {
|
||||||
window.electronAPI.getBackendUrl()
|
window.electronAPI.getBackendUrl()
|
||||||
@@ -314,11 +355,11 @@ onMounted(() => {
|
|||||||
baseUrl.value = url;
|
baseUrl.value = url;
|
||||||
const wsUrl = url.replace('http://', 'ws://') + '/ws';
|
const wsUrl = url.replace('http://', 'ws://') + '/ws';
|
||||||
console.log('WebSocket URL设置为:', wsUrl);
|
console.log('WebSocket URL设置为:', wsUrl);
|
||||||
|
|
||||||
// 设置WebSocket URL并连接
|
// 设置WebSocket URL并连接
|
||||||
websocketService.setWsUrl(wsUrl);
|
websocketService.setWsUrl(wsUrl);
|
||||||
websocketService.connect();
|
websocketService.connect();
|
||||||
|
|
||||||
// 使用Electron提供的悬停状态,移除本地事件监听
|
// 使用Electron提供的悬停状态,移除本地事件监听
|
||||||
window.electronAPI.onMouseHoverChange((hovering) => {
|
window.electronAPI.onMouseHoverChange((hovering) => {
|
||||||
isHovering.value = hovering;
|
isHovering.value = hovering;
|
||||||
@@ -329,7 +370,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听锁定窗口事件
|
// 监听锁定窗口事件
|
||||||
if (window.electronAPI.onToggleLockWindow) {
|
if (window.electronAPI.onToggleLockWindow) {
|
||||||
window.electronAPI.onToggleLockWindow((locked) => {
|
window.electronAPI.onToggleLockWindow((locked) => {
|
||||||
@@ -359,35 +400,35 @@ onMounted(() => {
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('获取后端URL失败,使用默认URL', err);
|
console.error('获取后端URL失败,使用默认URL', err);
|
||||||
websocketService.connect();
|
websocketService.connect();
|
||||||
|
|
||||||
// 设置鼠标移入移出事件(仅在非Electron环境或Electron API失败时使用)
|
// 设置鼠标移入移出事件(仅在非Electron环境或Electron API失败时使用)
|
||||||
setupMouseEvents();
|
setupMouseEvents();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
websocketService.connect();
|
websocketService.connect();
|
||||||
|
|
||||||
// 设置鼠标移入移出事件(仅在非Electron环境下使用)
|
// 设置鼠标移入移出事件(仅在非Electron环境下使用)
|
||||||
setupMouseEvents();
|
setupMouseEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置定期检查连接状态的间隔
|
// 设置定期检查连接状态的间隔
|
||||||
const connectionCheckInterval = setInterval(() => {
|
const connectionCheckInterval = setInterval(() => {
|
||||||
websocketService.checkAndRestoreConnection();
|
websocketService.checkAndRestoreConnection();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
// 组件卸载时清理资源
|
// 组件卸载时清理资源
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
console.log('组件将卸载');
|
console.log('组件将卸载');
|
||||||
|
|
||||||
// 清理WebSocket连接
|
// 清理WebSocket连接
|
||||||
websocketService.disconnect();
|
websocketService.disconnect();
|
||||||
|
|
||||||
// 清理位置跟踪器
|
// 清理位置跟踪器
|
||||||
positionTracker.cleanup();
|
positionTracker.cleanup();
|
||||||
|
|
||||||
// 清理连接检查定时器
|
// 清理连接检查定时器
|
||||||
clearInterval(connectionCheckInterval);
|
clearInterval(connectionCheckInterval);
|
||||||
|
|
||||||
// 移除鼠标事件监听器
|
// 移除鼠标事件监听器
|
||||||
const lyricWindowEl = lyricWindow.value;
|
const lyricWindowEl = lyricWindow.value;
|
||||||
if (lyricWindowEl) {
|
if (lyricWindowEl) {
|
||||||
@@ -410,7 +451,7 @@ import request from '../utils/request';
|
|||||||
// 删除歌词
|
// 删除歌词
|
||||||
const onDeleteLyrics = async () => {
|
const onDeleteLyrics = async () => {
|
||||||
if (!trackName.value || !artistName.value) return;
|
if (!trackName.value || !artistName.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await request({
|
const result = await request({
|
||||||
url: '/lyrics',
|
url: '/lyrics',
|
||||||
@@ -420,7 +461,7 @@ const onDeleteLyrics = async () => {
|
|||||||
artist: artistName.value
|
artist: artistName.value
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
console.log('歌词已删除');
|
console.log('歌词已删除');
|
||||||
// 清空当前歌词显示
|
// 清空当前歌词显示
|
||||||
@@ -440,6 +481,7 @@ const onDeleteLyrics = async () => {
|
|||||||
* {
|
* {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyric-window {
|
.lyric-window {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -452,24 +494,28 @@ const onDeleteLyrics = async () => {
|
|||||||
background: rgba(30, 30, 40);
|
background: rgba(30, 30, 40);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-app-region: drag; /* 使整个窗口可拖动 */
|
-webkit-app-region: drag;
|
||||||
|
/* 使整个窗口可拖动 */
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 当鼠标不在窗口上时,背景完全透明 */
|
/* 当鼠标不在窗口上时,背景完全透明 */
|
||||||
.lyric-window.no-focus {
|
.lyric-window.no-focus {
|
||||||
background: rgba(0, 0, 0, 0); /* 完全透明的背景 */
|
background: rgba(0, 0, 0, 0);
|
||||||
|
/* 完全透明的背景 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 控制按钮等交互元素不可拖动 */
|
/* 控制按钮等交互元素不可拖动 */
|
||||||
:deep(.control-btn), :deep(.progress-bar) {
|
:deep(.control-btn),
|
||||||
|
:deep(.progress-bar) {
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 控制栏容器样式 */
|
/* 控制栏容器样式 */
|
||||||
.control-bar-container {
|
.control-bar-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
-webkit-app-region: drag; /* 允许控制栏区域拖动窗口 */
|
-webkit-app-region: drag;
|
||||||
|
/* 允许控制栏区域拖动窗口 */
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +556,8 @@ const onDeleteLyrics = async () => {
|
|||||||
.track-name {
|
.track-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
font-size: 20px; /* 增大歌曲名 */
|
font-size: 20px;
|
||||||
|
/* 增大歌曲名 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.artist-name {
|
.artist-name {
|
||||||
@@ -530,9 +577,12 @@ const onDeleteLyrics = async () => {
|
|||||||
/* Make text fully opaque when window loses focus */
|
/* Make text fully opaque when window loses focus */
|
||||||
.no-focus .lyric-line {
|
.no-focus .lyric-line {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.9), /* 增加阴影深度 */
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.9),
|
||||||
0 0 8px rgba(0, 0, 0, 0.8), /* 增加阴影强度 */
|
/* 增加阴影深度 */
|
||||||
0 1px 15px rgba(0, 0, 0, 0.7); /* 添加额外的阴影层 */
|
0 0 8px rgba(0, 0, 0, 0.8),
|
||||||
|
/* 增加阴影强度 */
|
||||||
|
0 1px 15px rgba(0, 0, 0, 0.7);
|
||||||
|
/* 添加额外的阴影层 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyric-line.highlight {
|
.lyric-line.highlight {
|
||||||
@@ -543,9 +593,12 @@ const onDeleteLyrics = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.no-focus .lyric-line.highlight {
|
.no-focus .lyric-line.highlight {
|
||||||
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.95), /* 增加阴影深度 */
|
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.95),
|
||||||
0 0 10px rgba(0, 0, 0, 0.8), /* 增加阴影强度 */
|
/* 增加阴影深度 */
|
||||||
0 1px 20px rgba(0, 0, 0, 0.7); /* 添加额外的阴影层 */
|
0 0 10px rgba(0, 0, 0, 0.8),
|
||||||
|
/* 增加阴影强度 */
|
||||||
|
0 1px 20px rgba(0, 0, 0, 0.7);
|
||||||
|
/* 添加额外的阴影层 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyric-line.next {
|
.lyric-line.next {
|
||||||
@@ -584,9 +637,17 @@ const onDeleteLyrics = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { opacity: 0.5; }
|
0% {
|
||||||
50% { opacity: 1; }
|
opacity: 0.5;
|
||||||
100% { opacity: 0.5; }
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-focus .loading-text {
|
.no-focus .loading-text {
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ export const useLyricsStore = defineStore('lyrics', {
|
|||||||
state: () => ({
|
state: () => ({
|
||||||
trackName: '',
|
trackName: '',
|
||||||
artist: '',
|
artist: '',
|
||||||
baseUrl: 'http://127.0.0.1:5000'
|
baseUrl: 'http://127.0.0.1:5005'
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -20,7 +20,7 @@ async function sendControlCommand(action, position = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/control',
|
url: '/music/control',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: payload
|
data: payload
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ref } from 'vue';
|
|||||||
class WebSocketService {
|
class WebSocketService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.wsUrl = ref('ws://127.0.0.1:5000/ws');
|
this.wsUrl = ref('ws://127.0.0.1:5005/ws');
|
||||||
this.reconnectInterval = 1000; // 重连间隔(毫秒)
|
this.reconnectInterval = 1000; // 重连间隔(毫秒)
|
||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null;
|
||||||
this.pingInterval = null;
|
this.pingInterval = null;
|
||||||
@@ -178,6 +178,11 @@ class WebSocketService {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
console.log('收到WebSocket消息类型:', data.type);
|
console.log('收到WebSocket消息类型:', data.type);
|
||||||
|
// 心跳:服务器发来的 ping,需要立即回复 pong 保持连接
|
||||||
|
if (data && data.type === 'ping') {
|
||||||
|
this.sendMessage({ type: 'pong' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 调用对应类型的消息处理器
|
// 调用对应类型的消息处理器
|
||||||
if (data.type && this.messageHandlers.has(data.type)) {
|
if (data.type && this.messageHandlers.has(data.type)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user