import { computed, readonly, ref } from 'vue' import { cleanTTMLTranslations, parseQRCLyric, parseSmartLrc } from '~/utils/lyric/lyricParser' import { parseTTML, parseYrc } from '@applemusic-like-lyrics/lyric' import { useAudioPlayer } from './useAudioPlayer' import { useMusicSources } from './useMusicSources' import { useLyricSettings } from './useLyricSettings' import { useLocale } from '~/utils/locale' export interface ParsedLyricLine { time: number content: string words?: Array<{ time: number duration: number content: string }> } type ParsedLyricWord = NonNullable[number] export interface LyricData { lrc: string trans?: string yrc?: string ttml?: string /** YRC 对齐的翻译(ytlrc),时间戳与逐字主歌词行边界一致 */ ytrans?: string } // ─── 内部解析工具 ──────────────────────────────────────────────── /** * 将 @applemusic-like-lyrics/lyric 库返回的 lines 数组统一转换为 * ParsedLyricLine[],兼容 TTML / YRC / SmartLrc 三种来源。 */ const convertLibraryLines = (lines: any[]): ParsedLyricLine[] => { return lines .map((line): ParsedLyricLine | null => { const words: ParsedLyricWord[] | undefined = Array.isArray(line?.words) ? line.words .map((word: any): ParsedLyricWord | null => { const startTime = typeof word?.startTime === 'number' ? word.startTime : 0 const endTime = typeof word?.endTime === 'number' ? word.endTime : startTime + (typeof word?.duration === 'number' ? word.duration : 0) const content = typeof word?.word === 'string' ? word.word : String(word?.content ?? '') if (!content) return null return { time: startTime, duration: Math.max(endTime - startTime, 0), content } }) .filter((w: ParsedLyricWord | null): w is ParsedLyricWord => w !== null) : undefined const content = words?.map((w) => w.content).join('') || (typeof line?.content === 'string' ? line.content : '') const time = typeof line?.startTime === 'number' ? line.startTime : (line?.time ?? 0) if (!content && (!words || words.length === 0)) return null return { time, content, words: words && words.length > 0 ? words : undefined } }) .filter((l): l is ParsedLyricLine => l !== null) .sort((a, b) => a.time - b.time) } const hasWordByWord = (lines: ParsedLyricLine[]): boolean => lines.some((l) => (l.words?.length ?? 0) > 1) /** * 尝试将单段文本解析为歌词行。 * 优先级:TTML > QRC > YRC(JSON) > SmartLrc */ const parseFlexibleLyrics = (text: string): { lines: ParsedLyricLine[]; wordByWord: boolean } => { const raw = text?.trim() if (!raw) return { lines: [], wordByWord: false } // TTML(XML 开头) if (raw.startsWith('<')) { // 先尝试 TTML try { const parsed = parseTTML(cleanTTMLTranslations(raw)) const ttmlLines = convertLibraryLines((parsed as any)?.lines ?? []) if (ttmlLines.length > 0) { return { lines: ttmlLines, wordByWord: hasWordByWord(ttmlLines) } } } catch { // 解析失败继续尝试 QRC } // QRC(QQ 音乐 XML 格式) if (raw.includes('LyricContent="') || raw.includes(' 0) { return { lines: qrcLines, wordByWord: hasWordByWord(qrcLines) } } } catch { // 继续回退 } } } // YRC(网易云 JSON 逐字格式) if (raw.startsWith('{') || raw.trim().split('\n')[0]?.trimStart().startsWith('{')) { try { const lines = parseYrc(raw) if (lines && lines.length > 0) { const converted = convertLibraryLines(lines) if (converted.length > 0) { return { lines: converted, wordByWord: hasWordByWord(converted) } } } } catch { // 回退到 SmartLrc } } // SmartLrc(通用 LRC / 逐字扩展 LRC) const { lines: smartLines, format } = parseSmartLrc(raw) if (smartLines.length > 0) { const converted = convertLibraryLines(smartLines) return { lines: converted, wordByWord: hasWordByWord(converted) || format !== 'line' } } return { lines: [], wordByWord: false } } /** * 按优先级顺序尝试多个候选文本,返回第一个成功解析的结果。 * 外部调用时按 [ttml, yrc, lrc] 顺序传入。 */ const parseBestLyrics = ( candidates: Array ): { lines: ParsedLyricLine[]; wordByWord: boolean } => { for (const candidate of candidates) { if (!candidate?.trim()) continue const parsed = parseFlexibleLyrics(candidate) if (parsed.lines.length > 0) return parsed } return { lines: [], wordByWord: false } } // ─── Composable ──────────────────────────────────────────────── export const useLyrics = () => { const audioPlayer = useAudioPlayer() const { composableErrors } = useLocale() const lyricMessages = computed(() => composableErrors.value.lyrics) const currentLyrics = ref([]) const translationLyrics = ref([]) const wordByWordLyrics = ref([]) const currentLyricIndex = ref(0) const currentTime = ref(0) const isLoading = ref(false) const error = ref(null) const showTranslation = ref(false) const showWordByWord = ref(false) // 竞态令牌 let currentToken = 0 // ─── 通知 HarmonyOS ──────────────────────────────────────────── const notifyHarmonyOSLyricsUpdate = (lyrics: string) => { if (typeof window === 'undefined') return if (!window.HarmonyOS?.postMessage) { console.warn('[useLyrics] 鸿蒙侧消息接口不可用') return } try { window.HarmonyOS.postMessage( JSON.stringify({ method: 'updateLyrics', parameters: { lyrics } }) ) } catch (e) { console.error('[useLyrics] 通知鸿蒙侧失败:', e) } } // ─── 歌词获取 ──────────────────────────────────────────────── const applyLyricData = (lyricData?: LyricData | null): boolean => { if (!lyricData) return false // 主歌词:按优先级 ttml > yrc > lrc 解析 const parsedMain = parseBestLyrics([lyricData.ttml, lyricData.yrc, lyricData.lrc]) if (parsedMain.lines.length === 0) return false currentLyrics.value = parsedMain.lines wordByWordLyrics.value = parsedMain.wordByWord ? parsedMain.lines : [] showWordByWord.value = parsedMain.wordByWord translationLyrics.value = [] showTranslation.value = false // 翻译歌词(ytlrc 时间戳与逐字主歌词对齐,优先于 tlyric) const transSource = lyricData.ytrans || lyricData.trans if (transSource) { const parsedTrans = parseBestLyrics([transSource]) if (parsedTrans.lines.length > 0) { translationLyrics.value = parsedTrans.lines showTranslation.value = true } } error.value = null return true } const fetchLyrics = async ( platform: string, musicId: string, meta?: { title?: string; artist?: string; album?: string } ): Promise => { if (!platform || !musicId) { console.error('[useLyrics] fetchLyrics 参数错误:', { platform, musicId }) error.value = lyricMessages.value.missingParams _resetState() return } const token = ++currentToken isLoading.value = true error.value = null _resetState() let hasRenderedProgress = false try { const { getLyrics } = useMusicSources() const currentSong = audioPlayer.getCurrentSong().value as any const result = await getLyrics(platform as 'netease' | 'tencent', musicId, { title: meta?.title ?? currentSong?.title ?? '', artist: meta?.artist ?? currentSong?.artist ?? '', album: meta?.album ?? currentSong?.album ?? '', duration: currentSong?.duration, priority: useLyricSettings().lyricPriority.value, onProgress: ({ data }) => { if (token !== currentToken) return const applied = applyLyricData(data) if (!applied) return hasRenderedProgress = true isLoading.value = false notifyHarmonyOSLyricsUpdate(getFormattedLyricsForHarmonyOS()) } }) if (token !== currentToken) return if (!result.success || !result.data) { throw new Error(result.error || lyricMessages.value.notFound) } const lyricData = result.data const applied = applyLyricData(lyricData) if (!applied && !hasRenderedProgress) { throw new Error(lyricMessages.value.notFound) } const harmonyLyrics = getFormattedLyricsForHarmonyOS() notifyHarmonyOSLyricsUpdate(harmonyLyrics) error.value = null } catch (e: any) { if (token !== currentToken) return if (hasRenderedProgress) { error.value = null return } console.error('[useLyrics] 获取歌词失败:', e?.message ?? e) error.value = e?.message ?? lyricMessages.value.fetchFailed _resetState() notifyHarmonyOSLyricsUpdate('') } finally { if (token === currentToken) { isLoading.value = false } } } // ─── 时间同步 ──────────────────────────────────────────────── const updateCurrentLyricIndex = (time: number): void => { currentTime.value = time const lyricList = currentLyrics.value if (lyricList.length === 0) return // 二分查找当前时间所在行 let lo = 0 let hi = lyricList.length - 1 while (lo < hi) { const mid = (lo + hi + 1) >> 1 if (lyricList[mid].time <= time) lo = mid else hi = mid - 1 } if (currentLyricIndex.value !== lo) { currentLyricIndex.value = lo } } // ─── 计算属性 ──────────────────────────────────────────────── const currentLyricContent = computed(() => { const lyricList = currentLyrics.value if (lyricList.length === 0 || currentLyricIndex.value >= lyricList.length) return '' return lyricList[currentLyricIndex.value].content }) const currentTranslationContent = computed(() => { if (!showTranslation.value || translationLyrics.value.length === 0) return '' const refTime = currentLyrics.value[currentLyricIndex.value]?.time if (refTime == null) return '' const list = translationLyrics.value let lo = 0 let hi = list.length - 1 while (lo < hi) { const mid = (lo + hi + 1) >> 1 if (list[mid].time <= refTime) lo = mid else hi = mid - 1 } return list[lo]?.time <= refTime ? list[lo].content : '' }) // ─── 格式化工具 ──────────────────────────────────────────────── const _formatTime = (ms: number): string => { const minutes = Math.floor(ms / 60000) const seconds = Math.floor((ms % 60000) / 1000) const centiseconds = Math.floor((ms % 1000) / 10) return `[${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(centiseconds).padStart(2, '0')}]` } const getFormattedLrcText = (): string => { if (currentLyrics.value.length === 0) return `${_formatTime(0)}${lyricMessages.value.unavailable}` return currentLyrics.value .map((line) => `${_formatTime(line.time)}${line.content}`) .join('\r\n') } const getFormattedLyricsForHarmonyOS = (): string => { if (currentLyrics.value.length === 0) return `${_formatTime(0)}${lyricMessages.value.unavailable}` const MAX_BYTES = 40960 const encoder = new TextEncoder() const lines = currentLyrics.value const parts: string[] = [] let byteCount = 0 for (const line of lines) { const lineText = `${_formatTime(line.time)}${line.content}\r\n` const lineBytes = encoder.encode(lineText).length // 留 100 字节缓冲 if (byteCount + lineBytes > MAX_BYTES - 100) break parts.push(lineText) byteCount += lineBytes } return parts.join('').trimEnd() } // ─── 清理 ──────────────────────────────────────────────────── const _resetState = () => { currentLyrics.value = [] translationLyrics.value = [] wordByWordLyrics.value = [] currentLyricIndex.value = 0 showTranslation.value = false showWordByWord.value = false } const clearLyrics = (): void => { currentToken++ _resetState() currentTime.value = 0 error.value = null } const seekToLyricLine = (index: number): number => { if (index >= 0 && index < currentLyrics.value.length) { return currentLyrics.value[index].time / 1000 } return 0 } // 保留向后兼容的解析方法(部分组件可能直接调用) const parseLrcLyrics = (lrcText: string): ParsedLyricLine[] => { if (!lrcText) return [] return parseFlexibleLyrics(lrcText).lines } const parseYrcLyrics = (yrcText: string): ParsedLyricLine[] => { if (!yrcText) return [] return parseFlexibleLyrics(yrcText).lines } return { currentLyrics: readonly(currentLyrics), translationLyrics: readonly(translationLyrics), wordByWordLyrics: readonly(wordByWordLyrics), currentLyricIndex: readonly(currentLyricIndex), currentTime: readonly(currentTime), isLoading: readonly(isLoading), error: readonly(error), showTranslation, showWordByWord, currentLyricContent, currentTranslationContent, fetchLyrics, updateCurrentLyricIndex, getFormattedLrcText, getFormattedLyricsForHarmonyOS, notifyHarmonyOSLyricsUpdate, clearLyrics, seekToLyricLine, parseLrcLyrics, parseYrcLyrics } }