import { computed, effectScope, ref, watch, type EffectScope } from 'vue' import { useAudioPlayer } from './useAudioPlayer' import { useMusicSources } from './useMusicSources' import { useLyricSettings } from './useLyricSettings' import { useLocale } from '~/utils/locale' import { parseSmartLrc, alignLyrics, cleanTTMLTranslations, parseQRCLyric, type LrcFormat } from '~/utils/lyric/lyricParser' import { formatLyric } from '~/utils/lyric/lyricFormat' import type { LyricLine } from '@applemusic-like-lyrics/lyric' import { parseTTML, parseYrc } from '@applemusic-like-lyrics/lyric' /** 模块级共享状态:AMLyric 与 DefaultLyric 各自调用 useLyricManager 时共享同一份数据 */ const sharedLyrics = ref([]) const sharedLoading = ref(false) const sharedError = ref(null) const sharedCurrentTrackId = ref(null) const sharedHasTranslation = ref(false) const sharedHasRoma = ref(false) const sharedLyricFormat = ref('line') /** 当前歌词实际使用的来源(用于界面展示与问题定位) */ const sharedLyricSource = ref('') // 竞态令牌:每次新请求递增,旧请求收到结果后与当前令牌比对,不一致则丢弃 let currentToken = 0 // 全局 watch 只注册一次(detached scope 脱离组件生命周期,避免组件卸载后停止同步) let managerScope: EffectScope | null = null export const useLyricManager = () => { const audioPlayer = useAudioPlayer() const { composableErrors } = useLocale() const lyricMessages = computed(() => composableErrors.value.lyrics) const { getLyrics } = useMusicSources() const settings = useLyricSettings() const lyrics = sharedLyrics const loading = sharedLoading const error = sharedError const currentTrackId = sharedCurrentTrackId const hasTranslation = sharedHasTranslation const hasRoma = sharedHasRoma const lyricFormat = sharedLyricFormat const lyricSource = sharedLyricSource const clearLyrics = () => { lyrics.value = [] error.value = null hasTranslation.value = false hasRoma.value = false currentTrackId.value = null lyricFormat.value = 'line' lyricSource.value = '' } /** * 推断歌词实际来源: * ttml → AMLL DB;逐字 QRC → QQ;其余按抓取阶段与平台映射 */ const inferLyricSource = ( format: LrcFormat | 'ttml' | 'qrc', stage: string, track: any ): string => { const platform = track?.musicPlatform || 'netease' if (format === 'ttml') return 'amll' if (format === 'qrc') return 'qm' if (stage === 'upgrade') { // 跨平台升级:数据来自对侧平台 return platform === 'netease' ? 'qm' : 'official' } if (stage === 'meting') return 'meting' if (stage === 'amll') return 'amll' if (stage === 'qm') { // netease 平台的 qm 阶段走 vkeys 第三方接口 return platform === 'netease' ? 'vkeys' : 'qm' } // official 阶段 / 最终结果按平台保守推断 return platform === 'tencent' ? 'qm' : 'official' } const applyLyricData = ( track: any, lyricData: { lrc?: string trans?: string yrc?: string ttml?: string ytrans?: string } | null | undefined, sourceLabel: string, stage: string ) => { if (!lyricData) return false const { lrc, trans, yrc, ttml, ytrans } = lyricData console.log( `[LyricManager] ${sourceLabel} 获取结果: LRC=${!!lrc}, TRANS=${!!trans}, YRC=${!!yrc}, TTML=${!!ttml}, YTRANS=${!!ytrans}` ) let parsedLyrics: LyricLine[] = [] let format: LrcFormat | 'ttml' | 'qrc' = 'line' // 优先级:TTML > YRC/QRC > LRC if (settings.enableOnlineTTMLLyric.value && ttml) { try { const cleaned = cleanTTMLTranslations(ttml) const parsed = parseTTML(cleaned) const lines = (parsed as any)?.lines ?? [] if (lines.length > 0) { parsedLyrics = lines format = 'ttml' console.log('[LyricManager] 使用 TTML 格式') } } catch (e) { console.warn('[LyricManager] TTML 解析失败,回退', e) } } if (parsedLyrics.length === 0 && yrc) { // QRC:QQ音乐 XML 格式 if (yrc.trim().startsWith('<') || yrc.includes('LyricContent="')) { try { const qrcLines = parseQRCLyric(yrc, trans, undefined) if (qrcLines.length > 0) { parsedLyrics = qrcLines format = 'qrc' console.log('[LyricManager] 使用 QRC 格式') } } catch (e) { console.warn('[LyricManager] QRC 解析失败', e) } } // YRC:网易云 JSON 逐字格式 if (parsedLyrics.length === 0) { try { const lines = parseYrc(yrc) if (lines && lines.length > 0) { const validLines = lines.filter((l) => l.words && l.words.length > 0) if (validLines.length > 0) { parsedLyrics = lines format = 'word-by-word' console.log('[LyricManager] 使用 YRC 格式') } } } catch { // parseYrc 失败时回退 parseSmartLrc } // parseYrc 无效,用 SmartLrc 兜底 if (parsedLyrics.length === 0) { const { lines, format: detectedFormat } = parseSmartLrc(yrc) if (lines.length > 0) { parsedLyrics = lines format = detectedFormat console.log(`[LyricManager] 使用 YRC→SmartLrc 格式 (${detectedFormat})`) } } } } if (parsedLyrics.length === 0 && lrc) { const { lines, format: detectedFormat } = parseSmartLrc(lrc) parsedLyrics = lines format = detectedFormat console.log(`[LyricManager] 使用 LRC 格式 (${detectedFormat})`) } if (parsedLyrics.length === 0) return false hasTranslation.value = false hasRoma.value = false // 对齐翻译(TTML 已内嵌翻译,不需要再 align) if (format !== 'ttml') { // 逐字歌词(YRC/QRC)用 ytlrc(YRC 对齐翻译)优先;普通 LRC 用 tlyric const isWordLevel = format === 'qrc' || format === 'word-by-word' const transText = isWordLevel ? ytrans || trans : trans if (transText) { const { lines: transLines } = parseSmartLrc(transText) if (transLines.length > 0) { parsedLyrics = alignLyrics(parsedLyrics, transLines, 'translatedLyric') hasTranslation.value = true const transKind = ytrans && isWordLevel ? 'ytlrc' : 'tlyric' console.log(`[LyricManager] 已对齐翻译 (来源: ${transKind})`) } } } const metadata = { title: track.title, artists: track.artist ? [track.artist] : undefined } lyrics.value = formatLyric(parsedLyrics, settings, metadata) lyricFormat.value = format // 推断实际歌词来源(用于来源徽章展示与问题定位) lyricSource.value = inferLyricSource(format, stage, track) error.value = null console.log(`[LyricManager] 解析完成,格式=${format},行数=${lyrics.value.length}`) return true } const fetchLyric = async (track: any, options?: { force?: boolean }) => { if (!track?.id) return const trackId = track.id.toString() // 已有歌词且非新歌曲,直接复用(force 用于切换歌词来源后强制重拉) if (!options?.force && currentTrackId.value === trackId && lyrics.value.length > 0) return // 开启新令牌,使所有进行中的旧请求失效 const token = ++currentToken currentTrackId.value = trackId loading.value = true error.value = null hasTranslation.value = false hasRoma.value = false lyricFormat.value = 'line' lyricSource.value = '' let hasRenderedProgress = false const priority = settings.lyricPriority.value try { const platform = track.musicPlatform || 'netease' const musicId = track.musicId || track.id console.log(`[LyricManager] 开始获取歌词: ${track.title} (${platform}:${musicId})`) const result = await getLyrics(platform, musicId, { title: track.title, artist: track.artist, album: track.album, duration: track.duration, priority, onProgress: ({ data, stage }) => { if (token !== currentToken) return const applied = applyLyricData(track, data, `阶段 ${stage}`, stage) if (applied) { hasRenderedProgress = true loading.value = false } } }) // 令牌失效:歌曲已切换,丢弃结果 if (token !== currentToken) return if (!result.success || !result.data) { throw new Error(result.error || lyricMessages.value.notFound) } // 令牌二次校验(getLyrics 为异步,期间可能切歌) if (token !== currentToken) return const applied = applyLyricData(track, result.data, '最终', '最终') if (!applied && !hasRenderedProgress) { error.value = lyricMessages.value.unavailable lyrics.value = [] } } catch (e: any) { if (token !== currentToken) return if (hasRenderedProgress) { error.value = null return } console.error('[LyricManager] 获取歌词失败:', e) error.value = e.message || lyricMessages.value.fetchFailed lyrics.value = [] } finally { // 只有当前令牌的请求才更新 loading 状态 if (token === currentToken) { loading.value = false } } } if (!managerScope) { managerScope = effectScope(true) managerScope.run(() => { watch( () => audioPlayer.getCurrentSong().value, (newTrack, oldTrack) => { // 歌曲切换(包括 id 变化和从有到无) const newId = newTrack?.id?.toString() ?? null const oldId = oldTrack?.id?.toString() ?? null if (newId === oldId) return if (newTrack) { fetchLyric(newTrack) } else { currentToken++ // 使正在进行的请求失效 clearLyrics() } }, { immediate: true } ) }) } return { lyrics, loading, error, currentTrackId, hasTranslation, hasRoma, lyricFormat, lyricSource, fetchLyric, clearLyrics } }