@@ -8,6 +8,7 @@ import { isElectron } from "@/utils/env";
88import { stripLyricMetadata } from "@/utils/lyricStripper" ;
99import { type LyricLine , parseLrc , parseTTML , parseYrc } from "@applemusic-like-lyrics/lyric" ;
1010import { escapeRegExp , isEmpty } from "lodash-es" ;
11+ import { SongType } from "@/types/main" ;
1112
1213class LyricManager {
1314 /**
@@ -146,6 +147,99 @@ class LyricManager {
146147 return { lrcData : aligned , yrcData : lyricData . yrcData } ;
147148 }
148149
150+ /**
151+ * 从 QQ 音乐获取歌词(封装方法,供在线和本地歌曲使用)
152+ * @param song 歌曲对象,内部自动判断本地/在线并生成缓存 key
153+ * @returns 歌词数据,如果获取失败返回 null
154+ */
155+ private async fetchQQMusicLyric ( song : SongType ) : Promise < SongLyric | null > {
156+ // 构建歌手字符串
157+ const artistsStr = Array . isArray ( song . artists )
158+ ? song . artists . map ( ( a ) => a . name ) . join ( "/" )
159+ : String ( song . artists || "" ) ;
160+ // 判断本地/在线,生成缓存 key
161+ const isLocal = Boolean ( song . path ) ;
162+ const cacheKey = isLocal ? `local_${ song . id } ` : String ( song . id ) ;
163+ // 检查缓存
164+ let data : any = null ;
165+ try {
166+ const cacheManager = useCacheManager ( ) ;
167+ const result = await cacheManager . get ( "lyrics" , `${ cacheKey } .qrc.json` ) ;
168+ if ( result . success && result . data ) {
169+ const decoder = new TextDecoder ( ) ;
170+ const cachedStr = decoder . decode ( result . data ) ;
171+ data = JSON . parse ( cachedStr ) ;
172+ }
173+ } catch {
174+ data = null ;
175+ }
176+ // 如果没有缓存,则请求 API
177+ if ( ! data ) {
178+ const keyword = `${ song . name } -${ artistsStr } ` ;
179+ try {
180+ data = await qqMusicMatch ( keyword ) ;
181+ } catch ( error ) {
182+ console . warn ( "QQ 音乐歌词获取失败:" , error ) ;
183+ return null ;
184+ }
185+ }
186+ if ( ! data || data . code !== 200 ) return null ;
187+ // 验证时长匹配(相差超过 5 秒视为不匹配)
188+ if ( data . song ?. duration && song . duration > 0 ) {
189+ const durationDiff = Math . abs ( data . song . duration - song . duration ) ;
190+ if ( durationDiff > 5000 ) {
191+ console . warn (
192+ `QQ 音乐歌词时长不匹配: ${ data . song . duration } ms vs ${ song . duration } ms (差异 ${ durationDiff } ms)` ,
193+ ) ;
194+ return null ;
195+ }
196+ }
197+ // 保存到缓存
198+ if ( data . code === 200 ) {
199+ try {
200+ const cacheManager = useCacheManager ( ) ;
201+ await cacheManager . set ( "lyrics" , `${ cacheKey } .qrc.json` , JSON . stringify ( data ) ) ;
202+ } catch ( error ) {
203+ console . error ( "写入 QQ 音乐歌词缓存失败:" , error ) ;
204+ }
205+ }
206+ // 解析歌词
207+ const result : SongLyric = { lrcData : [ ] , yrcData : [ ] } ;
208+ // 解析 QRC 逐字歌词
209+ if ( data . qrc ) {
210+ const qrcLines = this . parseQRCLyric ( data . qrc , data . trans , data . roma ) ;
211+ if ( qrcLines . length > 0 ) {
212+ result . yrcData = qrcLines ;
213+ }
214+ }
215+ // 解析 LRC 歌词(如果没有 QRC)
216+ if ( ! result . yrcData . length && data . lrc ) {
217+ let lrcLines = parseLrc ( data . lrc ) || [ ] ;
218+ // 处理翻译
219+ if ( data . trans ) {
220+ const transLines = parseLrc ( data . trans ) ;
221+ if ( transLines ?. length ) {
222+ lrcLines = this . alignLyrics ( lrcLines , transLines , "translatedLyric" ) ;
223+ }
224+ }
225+ // 处理罗马音
226+ if ( data . roma ) {
227+ const romaLines = parseLrc ( data . roma ) ;
228+ if ( romaLines ?. length ) {
229+ lrcLines = this . alignLyrics ( lrcLines , romaLines , "romanLyric" ) ;
230+ }
231+ }
232+ if ( lrcLines . length > 0 ) {
233+ result . lrcData = lrcLines ;
234+ }
235+ }
236+ // 如果没有任何歌词数据,返回 null
237+ if ( ! result . lrcData . length && ! result . yrcData . length ) {
238+ return null ;
239+ }
240+ return result ;
241+ }
242+
149243 /**
150244 * 解析 QQ 音乐 QRC 格式歌词
151245 * @param qrcContent QRC 原始内容
@@ -254,76 +348,17 @@ class LyricManager {
254348 if ( ! settingStore . preferQQMusicLyric ) return ;
255349 const song = musicStore . playSong ;
256350 if ( ! song ) return ;
257- // 先检查缓存
258- const cached = await this . getRawLyricCache ( id , "qrc" ) ;
259- let data : any = null ;
260- if ( cached ) {
261- try {
262- data = JSON . parse ( cached ) ;
263- } catch {
264- data = null ;
265- }
266- }
267- // 如果没有缓存,则请求 API
268- if ( ! data ) {
269- // 构建搜索关键词
270- const artistsStr = Array . isArray ( song . artists )
271- ? song . artists . map ( ( a ) => a . name ) . join ( "/" )
272- : song . artists || "" ;
273- const keyword = `${ song . name } -${ artistsStr } ` ;
274- try {
275- data = await qqMusicMatch ( keyword ) ;
276- } catch ( error ) {
277- console . warn ( "QQ 音乐歌词获取失败:" , error ) ;
278- return ;
279- }
280- }
351+ const qqLyric = await this . fetchQQMusicLyric ( song ) ;
281352 if ( isStale ( ) ) return ;
282- if ( ! data || data . code !== 200 ) return ;
283-
284- // 验证时长匹配(相差超过 5 秒视为不匹配)
285- if ( data . song ?. duration ) {
286- const durationDiff = Math . abs ( data . song . duration - song . duration ) ;
287- if ( durationDiff > 5000 ) {
288- console . warn (
289- `QQ 音乐歌词时长不匹配: ${ data . song . duration } ms vs ${ song . duration } ms (差异 ${ durationDiff } ms)` ,
290- ) ;
291- return ;
292- }
293- }
294- // 保存到缓存
295- if ( ! cached && data . code === 200 ) {
296- this . saveRawLyricCache ( id , "qrc" , JSON . stringify ( data ) ) ;
297- }
298- // 解析 QRC 逐字歌词
299- if ( data . qrc ) {
300- const qrcLines = this . parseQRCLyric ( data . qrc , data . trans , data . roma ) ;
301- if ( qrcLines . length > 0 ) {
302- result . yrcData = qrcLines ;
303- qqMusicAdopted = true ;
304- }
353+ if ( ! qqLyric ) return ;
354+ // 设置结果
355+ if ( qqLyric . yrcData . length > 0 ) {
356+ result . yrcData = qqLyric . yrcData ;
357+ qqMusicAdopted = true ;
305358 }
306- // 解析 LRC 歌词(如果没有 QRC)
307- if ( ! qqMusicAdopted && data . lrc ) {
308- let lrcLines = parseLrc ( data . lrc ) || [ ] ;
309- // 处理翻译
310- if ( data . trans ) {
311- const transLines = parseLrc ( data . trans ) ;
312- if ( transLines ?. length ) {
313- lrcLines = this . alignLyrics ( lrcLines , transLines , "translatedLyric" ) ;
314- }
315- }
316- // 处理罗马音
317- if ( data . roma ) {
318- const romaLines = parseLrc ( data . roma ) ;
319- if ( romaLines ?. length ) {
320- lrcLines = this . alignLyrics ( lrcLines , romaLines , "romanLyric" ) ;
321- }
322- }
323- if ( lrcLines . length > 0 ) {
324- result . lrcData = lrcLines ;
325- qqMusicAdopted = true ;
326- }
359+ if ( qqLyric . lrcData . length > 0 ) {
360+ result . lrcData = qqLyric . lrcData ;
361+ if ( ! qqMusicAdopted ) qqMusicAdopted = true ;
327362 }
328363 } ;
329364
@@ -414,7 +449,9 @@ class LyricManager {
414449 */
415450 private async handleLocalLyric ( path : string ) : Promise < SongLyric > {
416451 try {
452+ const musicStore = useMusicStore ( ) ;
417453 const statusStore = useStatusStore ( ) ;
454+ const settingStore = useSettingStore ( ) ;
418455 const { lyric, format } : { lyric ?: string ; format ?: "lrc" | "ttml" } =
419456 await window . electron . ipcRenderer . invoke ( "get-music-lyric" , path ) ;
420457 if ( ! lyric ) return { lrcData : [ ] , yrcData : [ ] } ;
@@ -425,10 +462,21 @@ class LyricManager {
425462 statusStore . usingTTMLLyric = true ;
426463 return { lrcData : [ ] , yrcData : lines } ;
427464 }
428- // 解析本地歌词并对其
465+ // 解析本地歌词
429466 const lrcLines = parseLrc ( lyric ) ;
430- const aligned = this . alignLocalLyrics ( { lrcData : lrcLines , yrcData : [ ] } ) ;
467+ let aligned = this . alignLocalLyrics ( { lrcData : lrcLines , yrcData : [ ] } ) ;
431468 statusStore . usingTTMLLyric = false ;
469+ // 如果开启了本地歌曲 QQ 音乐匹配,尝试获取逐字歌词
470+ if ( settingStore . localLyricQQMusicMatch && musicStore . playSong ) {
471+ const qqLyric = await this . fetchQQMusicLyric ( musicStore . playSong ) ;
472+ if ( qqLyric && qqLyric . yrcData . length > 0 ) {
473+ // 使用 QQ 音乐的逐字歌词,但保留本地歌词作为 lrcData
474+ aligned = {
475+ lrcData : aligned . lrcData ,
476+ yrcData : qqLyric . yrcData ,
477+ } ;
478+ }
479+ }
432480 return aligned ;
433481 } catch {
434482 return { lrcData : [ ] , yrcData : [ ] } ;
0 commit comments