feat: 后端改为deno

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
# 歌词服务器配置示例
LYRICS_SERVER=http://123.57.93.143:28883
LYRICS_AUTHORIZATION=fzt_tom

7
backend/.gitignore vendored
View File

@@ -1,7 +0,0 @@
__pycache__
lyrics
.DS_Store
build
dist
.env
venv

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 后端模块包
"""

View File

@@ -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)}

View File

@@ -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()

View File

@@ -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

View File

@@ -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连接已清理")

View File

@@ -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)}")

View File

View File

@@ -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

View File

@@ -2,16 +2,16 @@
* lyroc 主程序文件
* 使用模块化结构,只负责应用的生命周期管理和组件集成
*/
const { app, protocol, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');
const isDev = require('electron-is-dev');
const { app, protocol, ipcMain } = require("electron");
const path = require("path");
const fs = require("fs");
const isDev = require("electron-is-dev");
// 导入自定义模块
const windowManager = require('./modules/window-manager');
const backendService = require('./modules/backend-service');
const logger = require('./modules/logger');
const searchWindowManager = require('./windows/search-window');
const windowManager = require("./modules/window-manager");
const backendService = require("./modules/backend-service");
const logger = require("./modules/logger");
const searchWindowManager = require("./windows/search-window");
// 应用全局状态
global.appReady = false;
@@ -21,197 +21,197 @@ global.appReady = false;
*/
async function initApp() {
try {
console.log('初始化应用...');
console.log("初始化应用...");
// 确保只有一个实例在运行
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
console.log('另一个实例已经在运行');
console.log("另一个实例已经在运行");
app.quit();
return;
}
// 创建加载窗口
const loadingWindow = windowManager.createLoadingWindow();
windowManager.updateLoadingProgress(10, 'initializing');
windowManager.updateLoadingProgress(10, "initializing");
// 注册文件协议处理器
protocol.registerFileProtocol('file', (request, callback) => {
const url = request.url.replace('file://', '');
protocol.registerFileProtocol("file", (request, callback) => {
const url = request.url.replace("file://", "");
try {
return callback(decodeURIComponent(url));
} catch (error) {
console.error('协议处理器错误:', error);
console.error("协议处理器错误:", error);
}
});
// 检查系统环境
console.log('运行模式:', isDev ? '开发' : '生产');
console.log("运行模式:", isDev ? "开发" : "生产");
// 清理可能运行的Python进程
await backendService.cleanupExistingProcesses();
// 检查端口可用性
await backendService.checkPort();
windowManager.updateLoadingProgress(30, 'allocatingPorts');
windowManager.updateLoadingProgress(30, "allocatingPorts");
// 获取前端随机端口
await backendService.getFrontendPort();
// 启动Python后端
windowManager.updateLoadingProgress(40, 'loadingBackend');
await backendService.startPythonBackend(isDev);
// 启动Deno后端
windowManager.updateLoadingProgress(40, "loadingBackend");
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(
null,
backendService.getFrontendPortNumber(),
null,
backendService.getFrontendPortNumber(),
backendService.getBackendPort(),
isDev
);
// 更新加载进度
windowManager.updateLoadingProgress(80, 'connecting');
windowManager.updateLoadingProgress(80, "connecting");
// 设置窗口事件
windowManager.setupWindowEvents();
// 设置IPC通信
windowManager.setupIPC(
backendService.getBackendPort(),
backendService.getBackendPort(),
backendService.getFrontendPortNumber()
);
// 处理搜索窗口相关事件
ipcMain.on('open-search-window', () => {
console.log('收到打开搜索窗口请求');
ipcMain.on("open-search-window", () => {
console.log("收到打开搜索窗口请求");
searchWindowManager.createWindow();
});
ipcMain.on('close-search-window', () => {
console.log('收到关闭搜索窗口请求');
ipcMain.on("close-search-window", () => {
console.log("收到关闭搜索窗口请求");
searchWindowManager.closeWindow();
});
// 处理发送数据到搜索窗口的请求
ipcMain.on('send-to-search-window', (_, data) => {
console.log('收到发送到搜索窗口的数据:', data);
ipcMain.on("send-to-search-window", (_, data) => {
console.log("收到发送到搜索窗口的数据:", data);
searchWindowManager.receiveMainWindowData(data);
});
// 处理搜索窗口请求主窗口数据
ipcMain.on('request-main-window-data', () => {
console.log('收到搜索窗口数据请求');
ipcMain.on("request-main-window-data", () => {
console.log("收到搜索窗口数据请求");
// 获取主窗口数据并发送
const mainWindow = windowManager.mainWindow();
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', () => {
windowManager.updateLoadingProgress(100, 'ready');
mainWindow.webContents.once("did-finish-load", () => {
windowManager.updateLoadingProgress(100, "ready");
// 短暂延迟后关闭加载窗口
setTimeout(() => {
// 不需要在这里显示主窗口,动画结束时会自动显示
windowManager.closeLoadingWindow();
}, 500);
// 设置应用就绪标志
global.appReady = true;
});
} catch (err) {
console.error('应用初始化失败:', err);
console.error("应用初始化失败:", err);
app.quit();
}
}
// Electron 应用就绪时
app.on('ready', () => {
app.on("ready", () => {
// 设置日志记录
const logPath = logger.setupLogging(app.getPath('userData'));
const logPath = logger.setupLogging(app.getPath("userData"));
// 添加日志以显示Electron路径
console.log('Electron应用已准备就绪');
console.log('应用路径:', app.getAppPath());
console.log('__dirname:', __dirname);
console.log('用户数据路径:', app.getPath('userData'));
console.log('日志文件路径:', logPath);
console.log("Electron应用已准备就绪");
console.log("应用路径:", app.getAppPath());
console.log("__dirname:", __dirname);
console.log("用户数据路径:", app.getPath("userData"));
console.log("日志文件路径:", logPath);
// 检查资源路径
const resourcePath = isDev
? path.join(__dirname, '..', 'backend')
: path.join(process.resourcesPath, 'backend');
console.log('后端资源路径:', resourcePath);
console.log('该路径存在:', fs.existsSync(resourcePath));
const resourcePath = isDev
? path.join(__dirname, "..", "backend")
: path.join(process.resourcesPath, "backend");
console.log("后端资源路径:", resourcePath);
console.log("该路径存在:", fs.existsSync(resourcePath));
// 隐藏dock栏图标
if (process.platform === 'darwin' && app.dock) {
if (process.platform === "darwin" && app.dock) {
app.dock.hide();
console.log('已隐藏Dock图标');
console.log("已隐藏Dock图标");
}
// 启动应用初始化
initApp()
.then(() => {
console.log('应用初始化完成');
console.log("应用初始化完成");
})
.catch(err => {
console.error('初始化应用失败:', err);
.catch((err) => {
console.error("初始化应用失败:", err);
});
});
// 所有窗口关闭时退出应用
app.on('window-all-closed', function() {
if (process.platform !== 'darwin') {
app.on("window-all-closed", function () {
if (process.platform !== "darwin") {
app.quit();
}
});
// 应用激活时
app.on('activate', function() {
app.on("activate", function () {
if (!windowManager.mainWindow()) {
// 初始化窗口配置
windowManager.initConfig(app.getPath('userData'));
windowManager.initConfig(app.getPath("userData"));
// 重新创建窗口
const mainWindow = windowManager.createWindow(
null,
backendService.getFrontendPortNumber(),
null,
backendService.getFrontendPortNumber(),
backendService.getBackendPort(),
isDev
);
// 设置窗口事件
windowManager.setupWindowEvents();
}
});
// 应用退出前
app.on('before-quit', () => {
app.on("before-quit", () => {
// 清理Python进程
backendService.stopPythonBackend();
});
// 监听第二实例启动事件
app.on('second-instance', (event, commandLine, workingDirectory) => {
app.on("second-instance", (event, commandLine, workingDirectory) => {
// 当尝试启动第二个实例时,让主窗口获得焦点
const mainWindow = windowManager.mainWindow();
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
console.log('已有实例在运行,已将其窗口聚焦');
console.log("已有实例在运行,已将其窗口聚焦");
}
});

View File

@@ -1,16 +1,16 @@
/**
* 后端服务管理模块
* 负责启动、管理和监控Python后端服务
* 负责启动、管理和监控Deno后端服务
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const findProcess = require('find-process');
const getPort = require('get-port');
const { spawn } = require("child_process");
const path = require("path");
const fs = require("fs");
const findProcess = require("find-process");
const getPort = require("get-port");
// 存储Python进程引用
let pythonProcess = null;
let backendPort = 5000;
// 存储Deno进程引用
let denoProcess = null;
let backendPort = 5005;
let frontendPort = 5173;
/**
@@ -19,12 +19,12 @@ let frontendPort = 5173;
*/
async function checkPort() {
try {
// 获取可用端口首选5000
backendPort = await getPort({ port: 5000 });
// 获取可用端口首选5005
backendPort = await getPort({ port: 5005 });
console.log(`后端将使用端口: ${backendPort}`);
return backendPort;
} catch (err) {
console.error('获取可用端口失败:', err);
console.error("获取可用端口失败:", err);
throw err;
}
}
@@ -40,179 +40,195 @@ async function getFrontendPort() {
console.log(`前端将使用随机端口: ${frontendPort}`);
return frontendPort;
} catch (err) {
console.error('获取前端端口失败:', err);
console.error("获取前端端口失败:", err);
throw err;
}
}
/**
* 清理已存在的Python进程
* 清理已存在的Deno进程
* @returns {Promise<void>}
*/
async function cleanupExistingProcesses() {
try {
const processList = await findProcess('port', 5000);
const processList = await findProcess("port", 5005);
for (const proc of processList) {
if (proc.name.includes('python')) {
console.log(`杀死已存在的Python进程: PID ${proc.pid}`);
process.kill(proc.pid, 'SIGKILL');
if (proc.name.includes("deno")) {
console.log(`杀死已存在的Deno进程: PID ${proc.pid}`);
process.kill(proc.pid, "SIGKILL");
}
}
} catch (err) {
console.error('清理进程失败:', err);
console.error("清理进程失败:", err);
}
}
/**
* 启动Python后端服务
* 启动Deno后端服务
* @param {boolean} isDev 是否为开发模式
* @returns {Promise<void>}
*/
async function startPythonBackend(isDev) {
async function startDenoBackend(isDev) {
try {
// 在开发模式和生产模式下使用不同的路径
let pythonPath;
let scriptPath;
if (isDev) {
// 开发模式:使用项目中的虚拟环境
const venvPath = path.join(__dirname, '..', '..', 'backend', 'venv');
pythonPath = process.platform === 'win32'
? path.join(venvPath, 'Scripts', 'python.exe')
: path.join(venvPath, 'bin', 'python3');
scriptPath = path.join(__dirname, '..', '..', 'backend', 'main.py');
// 开发模式:使用项目中的Deno后端
scriptPath = path.join(__dirname, "..", "..", "backend-deno", "main.ts");
} else {
// 生产模式:使用打包的虚拟环境
const venvPath = path.join(process.resourcesPath, 'backend', 'venv');
pythonPath = process.platform === 'win32'
? path.join(venvPath, 'Scripts', 'python.exe')
: path.join(venvPath, 'bin', 'python3');
scriptPath = path.join(process.resourcesPath, 'backend', 'main.py');
// 生产模式:使用打包的Deno后端
scriptPath = path.join(process.resourcesPath, "backend-deno", "main.ts");
}
console.log(`启动Python后端: ${scriptPath} 在端口 ${backendPort}`);
console.log(`启动Deno后端: ${scriptPath} 在端口 ${backendPort}`);
console.log(`后端脚本路径存在: ${fs.existsSync(scriptPath)}`);
console.log(`Python解释器路径: ${pythonPath}`);
// 检查Python解释器是否存在
if (!fs.existsSync(pythonPath)) {
throw new Error(`Python解释器不存在: ${pythonPath}`);
// 检查Deno是否安装
try {
const denoCheck = spawn("deno", ["--version"], { stdio: "pipe" });
denoCheck.on("error", (err) => {
throw new Error(`Deno未安装或不在PATH中: ${err.message}`);
});
} catch (err) {
throw new Error(`Deno未安装或不在PATH中: ${err.message}`);
}
// 打印Python脚本目录内容
// 打印Deno脚本目录内容
try {
const scriptDir = path.dirname(scriptPath);
console.log(`后端目录内容: ${fs.readdirSync(scriptDir).join(', ')}`);
console.log(`后端目录内容: ${fs.readdirSync(scriptDir).join(", ")}`);
} catch (err) {
console.error(`无法读取后端目录: ${err}`);
}
// 启动子进程
pythonProcess = spawn(pythonPath, [scriptPath], {
env: { ...process.env, PORT: backendPort.toString() },
stdio: 'pipe' // 确保可以读取标准输出和错误
});
// 输出Python进程的日志
pythonProcess.stdout.on('data', (data) => {
denoProcess = spawn(
"deno",
[
"run",
"--allow-net",
"--allow-run",
"--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();
console.log(`Python后端输出: ${output}`);
console.log(`Deno后端输出: ${output}`);
});
pythonProcess.stderr.on('data', (data) => {
denoProcess.stderr.on("data", (data) => {
const message = data.toString().trim();
// 判断是否是错误日志还是普通日志
if (message.includes('ERROR') || message.includes('CRITICAL') || message.includes('WARN')) {
console.error(`Python后端错误: ${message}`);
if (
message.includes("ERROR") ||
message.includes("CRITICAL") ||
message.includes("WARN")
) {
console.error(`Deno后端错误: ${message}`);
} else {
console.log(`Python后端日志: ${message}`);
console.log(`Deno后端日志: ${message}`);
}
});
pythonProcess.on('close', (code) => {
console.log(`Python后端退出,退出码: ${code}`);
denoProcess.on("close", (code) => {
console.log(`Deno后端退出,退出码: ${code}`);
// 如果应用仍在运行,尝试重启后端
if (global.appReady && code !== 0) {
console.log('尝试重启Python后端...');
setTimeout(() => startPythonBackend(isDev), 1000);
console.log("尝试重启Deno后端...");
setTimeout(() => startDenoBackend(isDev), 1000);
}
});
// 等待后端启动 - 改为检测后端是否真正启动完成而非固定等待时间
return new Promise((resolve, reject) => {
// 后端启动超时时间,开发模式较短,生产模式较长
const maxTimeout = isDev ? 15000 : 30000;
const startTime = Date.now();
let backendReady = false;
// 添加额外的日志解析以检测后端就绪状态
pythonProcess.stdout.on('data', (data) => {
denoProcess.stdout.on("data", (data) => {
const output = data.toString().trim();
console.log(`Python后端输出: ${output}`);
// 检测后端就绪信号(uvicorn输出"Application startup complete"表示应用已启动)
if (output.includes('Application startup complete') || output.includes('INFO: Application startup complete')) {
console.log('检测到后端应用启动完成信号');
console.log(`Deno后端输出: ${output}`);
// 检测后端就绪信号(Deno输出"启动后端服务器在端口"表示应用已启动)
if (
output.includes("启动后端服务器在端口") ||
output.includes("Server running")
) {
console.log("检测到后端应用启动完成信号");
backendReady = true;
// 再等待短暂时间确保所有服务都初始化完成
setTimeout(() => {
console.log('后端已完全启动,准备创建前端窗口');
console.log("后端已完全启动,准备创建前端窗口");
resolve();
}, 500);
}
});
// 实现HTTP测试以检测后端是否正常响应
const testBackendConnection = () => {
const http = require('http');
const http = require("http");
const testUrl = `http://127.0.0.1:${backendPort}/`;
// 如果已经检测到后端就绪,不再继续测试
if (backendReady) return;
// 检查是否超时
if (Date.now() - startTime > maxTimeout) {
console.warn(`后端启动超时(${maxTimeout}ms),将继续尝试启动前端`);
resolve();
return;
}
http.get(testUrl, (res) => {
if (res.statusCode === 200 || res.statusCode === 404) {
// 404也表示服务器在运行只是路径不存在
console.log('通过HTTP检测确认后端已启动');
backendReady = true;
resolve();
} else {
console.log(`后端响应状态码: ${res.statusCode},继续等待...`);
http
.get(testUrl, (res) => {
if (res.statusCode === 200 || res.statusCode === 404) {
// 404也表示服务器在运行只是路径不存在
console.log("通过HTTP检测确认后端已启动");
backendReady = true;
resolve();
} else {
console.log(`后端响应状态码: ${res.statusCode},继续等待...`);
// 短时间后再次测试
setTimeout(testBackendConnection, 1000);
}
})
.on("error", (err) => {
console.log(`后端连接测试失败: ${err.message}, 继续等待...`);
// 短时间后再次测试
setTimeout(testBackendConnection, 1000);
}
}).on('error', (err) => {
console.log(`后端连接测试失败: ${err.message}, 继续等待...`);
// 短时间后再次测试
setTimeout(testBackendConnection, 1000);
});
});
};
// 启动后端连接测试
setTimeout(testBackendConnection, 1000);
});
} catch (err) {
console.error('启动Python后端失败:', err);
console.error("启动Python后端失败:", err);
throw err;
}
}
/**
* 停止Python后端
* 停止Deno后端
*/
function stopPythonBackend() {
if (pythonProcess) {
console.log('终止Python后端进程...');
pythonProcess.kill();
pythonProcess = null;
function stopDenoBackend() {
if (denoProcess) {
console.log("终止Deno后端进程...");
denoProcess.kill();
denoProcess = null;
}
}
@@ -220,9 +236,9 @@ module.exports = {
checkPort,
getFrontendPort,
cleanupExistingProcesses,
startPythonBackend,
stopPythonBackend,
startDenoBackend,
stopDenoBackend,
getBackendPort: () => backendPort,
getFrontendPortNumber: () => frontendPort,
getPythonProcess: () => pythonProcess
getDenoProcess: () => denoProcess,
};

View File

@@ -113,16 +113,57 @@ function setupWebSocketHandlers() {
websocketService.registerHandler('info', (data) => {
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) => {
console.log('收到服务器心跳响应');
});
// 处理轨道变化消息 - 完整更新
websocketService.registerHandler('track_change', (data) => {
console.log('收到轨道变化:', data);
// 更新所有数据
if (data.status) {
positionTracker.updateState(
@@ -131,20 +172,20 @@ function setupWebSocketHandlers() {
data.status.duration
);
}
if (data.lyrics) {
// 有歌词数据,重置加载状态
isLoadingLyrics.value = false;
console.log('更新歌词:', data.lyrics);
currentLyric.value = data.lyrics.current_lyric || '';
nextLyric.value = data.lyrics.next_lyric || '';
nextNextLyric.value = data.lyrics.next_next_lyric || '';
if (data.lyrics.track_name) {
trackName.value = data.lyrics.track_name;
}
if (data.lyrics.artist_name) {
artistName.value = data.lyrics.artist_name;
}
@@ -152,12 +193,12 @@ function setupWebSocketHandlers() {
// 没有歌词数据,设置加载状态
console.log('歌曲切换,但没有歌词数据,显示加载中...');
isLoadingLyrics.value = true;
// 清空歌词显示
currentLyric.value = '';
nextLyric.value = '';
nextNextLyric.value = '';
// 可以保留歌曲信息如果status中有的话
if (data.status) {
trackName.value = data.status.track_name || trackName.value;
@@ -165,11 +206,11 @@ function setupWebSocketHandlers() {
}
}
});
// 处理歌词变化消息 - 只更新歌词和相关状态
websocketService.registerHandler('lyric_change', (data) => {
console.log('收到歌词变化');
// 更新时间相关状态
if (data.status) {
positionTracker.updateState(
@@ -178,37 +219,37 @@ function setupWebSocketHandlers() {
data.status.duration
);
}
// 更新歌词
if (data.lyrics) {
// 有歌词数据,重置加载状态
isLoadingLyrics.value = false;
currentLyric.value = data.lyrics.current_lyric || '';
nextLyric.value = data.lyrics.next_lyric || '';
nextNextLyric.value = data.lyrics.next_next_lyric || '';
}
});
// 处理新增的歌词更新消息类型 - 专门用于接收后期加载的歌词
websocketService.registerHandler('lyric_update', (data) => {
console.log('收到歌词更新消息');
if (data.lyrics) {
// 加载完成,重置加载状态
isLoadingLyrics.value = false;
// 更新歌词内容
currentLyric.value = data.lyrics.current_lyric || '';
nextLyric.value = data.lyrics.next_lyric || '';
nextNextLyric.value = data.lyrics.next_next_lyric || '';
}
});
// 处理播放停止消息 - 更新播放状态
websocketService.registerHandler('playback_stopped', (data) => {
console.log('收到播放停止消息');
// 更新播放状态
positionTracker.updateState(
data.status ? data.status.status : 'stopped',
@@ -216,11 +257,11 @@ function setupWebSocketHandlers() {
null
);
});
// 处理播放暂停消息 - 线控暂停
websocketService.registerHandler('playback_paused', (data) => {
console.log('收到播放暂停消息 (线控暂停)');
// 更新播放状态
positionTracker.updateState(
'paused',
@@ -228,11 +269,11 @@ function setupWebSocketHandlers() {
null
);
});
// 处理播放恢复消息 - 从暂停恢复
websocketService.registerHandler('playback_resumed', (data) => {
console.log('收到播放恢复消息');
// 更新播放状态
positionTracker.updateState(
'playing',
@@ -240,32 +281,32 @@ function setupWebSocketHandlers() {
null
);
});
// 处理Apple Music未运行消息
websocketService.registerHandler('app_not_running', (data) => {
console.log('收到Apple Music未运行消息');
// 更新播放状态为notrunning
positionTracker.updateState(
'notrunning',
0,
0
);
// 清空歌曲信息
trackName.value = null;
artistName.value = null;
// 清空歌词显示
currentLyric.value = '';
nextLyric.value = '';
nextNextLyric.value = '';
});
// 处理位置更新消息 - 只更新播放位置
websocketService.registerHandler('position_update', (data) => {
console.log('收到位置更新消息');
// 更新位置信息
if (data.status && data.status.position !== undefined) {
positionTracker.updateState(
@@ -302,10 +343,10 @@ const handleMouseLeave = () => {
// 组件挂载时初始化
onMounted(() => {
console.log('组件已挂载');
// 注册WebSocket消息处理器
setupWebSocketHandlers();
// 检查是否在Electron环境中获取后端URL
if (window.electronAPI) {
window.electronAPI.getBackendUrl()
@@ -314,11 +355,11 @@ onMounted(() => {
baseUrl.value = url;
const wsUrl = url.replace('http://', 'ws://') + '/ws';
console.log('WebSocket URL设置为:', wsUrl);
// 设置WebSocket URL并连接
websocketService.setWsUrl(wsUrl);
websocketService.connect();
// 使用Electron提供的悬停状态移除本地事件监听
window.electronAPI.onMouseHoverChange((hovering) => {
isHovering.value = hovering;
@@ -329,7 +370,7 @@ onMounted(() => {
}
}
});
// 监听锁定窗口事件
if (window.electronAPI.onToggleLockWindow) {
window.electronAPI.onToggleLockWindow((locked) => {
@@ -359,35 +400,35 @@ onMounted(() => {
.catch(err => {
console.error('获取后端URL失败使用默认URL', err);
websocketService.connect();
// 设置鼠标移入移出事件仅在非Electron环境或Electron API失败时使用
setupMouseEvents();
});
} else {
websocketService.connect();
// 设置鼠标移入移出事件仅在非Electron环境下使用
setupMouseEvents();
}
// 设置定期检查连接状态的间隔
const connectionCheckInterval = setInterval(() => {
websocketService.checkAndRestoreConnection();
}, 10000);
// 组件卸载时清理资源
onUnmounted(() => {
console.log('组件将卸载');
// 清理WebSocket连接
websocketService.disconnect();
// 清理位置跟踪器
positionTracker.cleanup();
// 清理连接检查定时器
clearInterval(connectionCheckInterval);
// 移除鼠标事件监听器
const lyricWindowEl = lyricWindow.value;
if (lyricWindowEl) {
@@ -410,7 +451,7 @@ import request from '../utils/request';
// 删除歌词
const onDeleteLyrics = async () => {
if (!trackName.value || !artistName.value) return;
try {
const result = await request({
url: '/lyrics',
@@ -420,7 +461,7 @@ const onDeleteLyrics = async () => {
artist: artistName.value
}
});
if (result.status === 'success') {
console.log('歌词已删除');
// 清空当前歌词显示
@@ -440,6 +481,7 @@ const onDeleteLyrics = async () => {
* {
user-select: none;
}
.lyric-window {
width: 100%;
height: 100%;
@@ -452,24 +494,28 @@ const onDeleteLyrics = async () => {
background: rgba(30, 30, 40);
position: relative;
overflow: hidden;
-webkit-app-region: drag; /* 使整个窗口可拖动 */
-webkit-app-region: drag;
/* 使整个窗口可拖动 */
transition: background-color 0.3s ease;
}
/* 当鼠标不在窗口上时,背景完全透明 */
.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;
}
/* 控制栏容器样式 */
.control-bar-container {
width: 100%;
-webkit-app-region: drag; /* 允许控制栏区域拖动窗口 */
-webkit-app-region: drag;
/* 允许控制栏区域拖动窗口 */
transition: opacity 0.3s ease;
}
@@ -510,7 +556,8 @@ const onDeleteLyrics = async () => {
.track-name {
font-weight: bold;
margin-right: 10px;
font-size: 20px; /* 增大歌曲名 */
font-size: 20px;
/* 增大歌曲名 */
}
.artist-name {
@@ -530,9 +577,12 @@ const onDeleteLyrics = async () => {
/* Make text fully opaque when window loses focus */
.no-focus .lyric-line {
opacity: 1;
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); /* 添加额外的阴影层 */
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);
/* 添加额外的阴影层 */
}
.lyric-line.highlight {
@@ -543,9 +593,12 @@ const onDeleteLyrics = async () => {
}
.no-focus .lyric-line.highlight {
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); /* 添加额外的阴影层 */
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);
/* 添加额外的阴影层 */
}
.lyric-line.next {
@@ -584,9 +637,17 @@ const onDeleteLyrics = async () => {
}
@keyframes pulse {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
0% {
opacity: 0.5;
}
50% {
opacity: 1;
}
100% {
opacity: 0.5;
}
}
.no-focus .loading-text {

View File

@@ -10,6 +10,6 @@ export const useLyricsStore = defineStore('lyrics', {
state: () => ({
trackName: '',
artist: '',
baseUrl: 'http://127.0.0.1:5000'
baseUrl: 'http://127.0.0.1:5005'
}),
})

View File

@@ -20,7 +20,7 @@ async function sendControlCommand(action, position = null) {
}
const response = await request({
url: '/control',
url: '/music/control',
method: 'POST',
data: payload
});

View File

@@ -7,7 +7,7 @@ import { ref } from 'vue';
class WebSocketService {
constructor() {
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.reconnectTimer = null;
this.pingInterval = null;
@@ -178,6 +178,11 @@ class WebSocketService {
try {
const data = JSON.parse(event.data);
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)) {