329 lines
8.3 KiB
TypeScript
329 lines
8.3 KiB
TypeScript
/**
|
||
* 歌词管理模块 (Deno 版本)
|
||
* 负责歌词的获取、解析和缓存
|
||
*/
|
||
|
||
import { MusicStatus } from "./apple_music.ts";
|
||
import { DatabaseSync } from 'node:sqlite'
|
||
|
||
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";
|
||
const db = new DatabaseSync(DB_PATH);
|
||
|
||
/**
|
||
* 初始化数据库
|
||
*/
|
||
export async function initDB(): Promise<void> {
|
||
try {
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS lyrics (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
track_name TEXT NOT NULL,
|
||
artist TEXT NOT NULL,
|
||
lyrics_text TEXT NOT NULL,
|
||
source TEXT NOT NULL,
|
||
deleted BOOLEAN NOT NULL DEFAULT 0
|
||
)
|
||
`);
|
||
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 content = db.prepare(`SELECT lyrics_text FROM lyrics WHERE track_name = ? AND artist = ?`).get(trackName, artist);
|
||
return content.lyrics_text;
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存歌词到数据库
|
||
*/
|
||
export async function saveLyrics(
|
||
trackName: string,
|
||
artist: string,
|
||
lyricsText: string,
|
||
source: string
|
||
): Promise<void> {
|
||
try {
|
||
db.prepare(`DELETE FROM lyrics WHERE track_name = ? AND artist = ?`).run(trackName, artist);
|
||
db.prepare(`INSERT INTO lyrics (track_name, artist, lyrics_text, source) VALUES (?, ?, ?, ?)`).run(trackName, artist, lyricsText, source);
|
||
lyricsCache.track_name = trackName;
|
||
lyricsCache.artist = artist;
|
||
lyricsCache.lyrics_text = lyricsText;
|
||
lyricsCache.source = source;
|
||
console.log(`保存歌词成功: ${trackName} - ${artist}`);
|
||
} catch (error) {
|
||
console.error(`保存歌词失败: ${error}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从服务器 API 搜索歌词
|
||
*/
|
||
export 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 deleteLyrics(trackName: string, artist: string): Promise<void> {
|
||
try {
|
||
db.prepare(`DELETE FROM lyrics WHERE track_name = ? AND artist = ?`).run(trackName, artist);
|
||
console.log(`删除歌词成功: ${trackName} - ${artist}`);
|
||
} catch (error) {
|
||
console.error(`删除歌词失败: ${error}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前歌词数据
|
||
*/
|
||
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, "server");
|
||
lyricsCache.lyrics_text = lyricsText;
|
||
lyricsCache.source = "server";
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果有缓存的歌词,直接解析
|
||
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,
|
||
};
|
||
}
|