feat: 后端改为deno
This commit is contained in:
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user