feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
+319
View File
@@ -0,0 +1,319 @@
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<LyricLine[]>([])
const sharedLoading = ref(false)
const sharedError = ref<string | null>(null)
const sharedCurrentTrackId = ref<string | null>(null)
const sharedHasTranslation = ref(false)
const sharedHasRoma = ref(false)
const sharedLyricFormat = ref<LrcFormat | 'ttml' | 'qrc'>('line')
/** 当前歌词实际使用的来源(用于界面展示与问题定位) */
const sharedLyricSource = ref<string>('')
// 竞态令牌:每次新请求递增,旧请求收到结果后与当前令牌比对,不一致则丢弃
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
}
}