@@ -6,7 +6,7 @@ import { useMusicStore, useSettingStore, useStatusStore, useStreamingStore } fro
66import { type SongLyric } from "@/types/lyric" ;
77import { SongType } from "@/types/main" ;
88import { isElectron } from "@/utils/env" ;
9- import { isWordLevelFormat , parseSmartLrc } from "@/utils/lyricParser" ;
9+ import { alignLyrics , isWordLevelFormat , parseQRCLyric , parseSmartLrc } from "@/utils/lyricParser" ;
1010import { stripLyricMetadata } from "@/utils/lyricStripper" ;
1111import { getConverter } from "@/utils/opencc" ;
1212import { type LyricLine , parseLrc , parseTTML , parseYrc } from "@applemusic-like-lyrics/lyric" ;
@@ -36,6 +36,7 @@ class LyricManager {
3636 // 重置歌词数据
3737 musicStore . setSongLyric ( { } , true ) ;
3838 statusStore . usingTTMLLyric = false ;
39+ statusStore . usingQRCLyric = false ;
3940 // 重置歌词索引
4041 statusStore . lyricIndex = - 1 ;
4142 statusStore . lyricLoading = false ;
@@ -95,17 +96,7 @@ class LyricManager {
9596 otherLyrics : LyricLine [ ] ,
9697 key : "translatedLyric" | "romanLyric" ,
9798 ) : LyricLine [ ] {
98- const lyricsData = lyrics ;
99- if ( lyricsData . length && otherLyrics . length ) {
100- lyricsData . forEach ( ( v : LyricLine ) => {
101- otherLyrics . forEach ( ( x : LyricLine ) => {
102- if ( v . startTime === x . startTime || Math . abs ( v . startTime - x . startTime ) < 300 ) {
103- v [ key ] = x . words . map ( ( word ) => word . word ) . join ( "" ) ;
104- }
105- } ) ;
106- } ) ;
107- }
108- return lyricsData ;
99+ return alignLyrics ( lyrics , otherLyrics , key ) ;
109100 }
110101
111102 /**
@@ -193,6 +184,7 @@ class LyricManager {
193184 if ( durationDiff > 5000 ) {
194185 console . warn (
195186 `QQ 音乐歌词时长不匹配: ${ data . song . duration } ms vs ${ song . duration } ms (差异 ${ durationDiff } ms)` ,
187+ data ,
196188 ) ;
197189 return null ;
198190 }
@@ -256,127 +248,7 @@ class LyricManager {
256248 * @returns LyricLine 数组
257249 */
258250 private parseQRCLyric ( qrcContent : string , trans ?: string , roma ?: string ) : LyricLine [ ] {
259- // 行匹配: [开始时间,持续时间]内容
260- const linePattern = / ^ \[ ( \d + ) , ( \d + ) \] ( .* ) $ / ;
261- // 逐字匹配: 文字(开始时间,持续时间)
262- const wordPattern = / ( [ ^ ( ] * ) \( ( \d + ) , ( \d + ) \) / g;
263- /**
264- * 解析 QRC 内容为行数据
265- */
266- const parseQRCContent = (
267- rawContent : string ,
268- ) : Array < {
269- startTime : number ;
270- endTime : number ;
271- words : Array < { word : string ; startTime : number ; endTime : number } > ;
272- } > => {
273- // 从 XML 中提取歌词内容
274- const contentMatch = / < L y r i c _ 1 [ ^ > ] * L y r i c C o n t e n t = " ( [ ^ " ] * ) " [ ^ > ] * \/ > / . exec ( rawContent ) ;
275- const content = contentMatch ? contentMatch [ 1 ] : rawContent ;
276-
277- const result : Array < {
278- startTime : number ;
279- endTime : number ;
280- words : Array < { word : string ; startTime : number ; endTime : number } > ;
281- } > = [ ] ;
282-
283- for ( const rawLine of content . split ( "\n" ) ) {
284- const line = rawLine . trim ( ) ;
285- if ( ! line ) continue ;
286-
287- // 跳过元数据标签 [ti:xxx] [ar:xxx] 等
288- if ( / ^ \[ [ a - z ] + : / i. test ( line ) ) continue ;
289-
290- const lineMatch = linePattern . exec ( line ) ;
291- if ( ! lineMatch ) continue ;
292-
293- const lineStart = parseInt ( lineMatch [ 1 ] , 10 ) ;
294- const lineDuration = parseInt ( lineMatch [ 2 ] , 10 ) ;
295- const lineContent = lineMatch [ 3 ] ;
296-
297- // 解析逐字
298- const words : Array < { word : string ; startTime : number ; endTime : number } > = [ ] ;
299- let wordMatch : RegExpExecArray | null ;
300- const wordRegex = new RegExp ( wordPattern . source , "g" ) ;
301-
302- while ( ( wordMatch = wordRegex . exec ( lineContent ) ) !== null ) {
303- const wordText = wordMatch [ 1 ] ;
304- const wordStart = parseInt ( wordMatch [ 2 ] , 10 ) ;
305- const wordDuration = parseInt ( wordMatch [ 3 ] , 10 ) ;
306-
307- if ( wordText ) {
308- words . push ( {
309- word : wordText ,
310- startTime : wordStart ,
311- endTime : wordStart + wordDuration ,
312- } ) ;
313- }
314- }
315-
316- if ( words . length > 0 ) {
317- result . push ( {
318- startTime : lineStart ,
319- endTime : lineStart + lineDuration ,
320- words,
321- } ) ;
322- }
323- }
324- return result ;
325- } ;
326- // 解析主歌词
327- const qrcLines = parseQRCContent ( qrcContent ) ;
328- let result = qrcLines . map ( ( qrcLine ) => {
329- return {
330- words : qrcLine . words . map ( ( word ) => ( {
331- ...word ,
332- romanWord : "" ,
333- } ) ) ,
334- startTime : qrcLine . startTime ,
335- endTime : qrcLine . endTime ,
336- translatedLyric : "" ,
337- romanLyric : "" ,
338- isBG : false ,
339- isDuet : false ,
340- } ;
341- } ) ;
342- // 处理翻译
343- if ( trans ) {
344- let transLines = parseLrc ( trans ) ;
345- if ( transLines ?. length ) {
346- // 过滤包含 "//" 或 "作品的著作权" 的翻译行
347- transLines = transLines . filter ( ( line ) => {
348- const text = line . words . map ( ( w ) => w . word ) . join ( "" ) ;
349- return ! text . includes ( "//" ) && ! text . includes ( "作品的著作权" ) ;
350- } ) ;
351- result = this . alignLyrics ( result , transLines , "translatedLyric" ) ;
352- }
353- }
354- // 处理音译
355- if ( roma ) {
356- const qrcRomaLines = parseQRCContent ( roma ) ;
357- if ( qrcRomaLines ?. length ) {
358- const romaLines = qrcRomaLines . map ( ( line ) => {
359- return {
360- words : [
361- {
362- startTime : line . startTime ,
363- endTime : line . endTime ,
364- word : line . words . map ( ( w ) => w . word ) . join ( "" ) ,
365- romanWord : "" ,
366- } ,
367- ] ,
368- startTime : line . startTime ,
369- endTime : line . endTime ,
370- translatedLyric : "" ,
371- romanLyric : "" ,
372- isBG : false ,
373- isDuet : false ,
374- } ;
375- } ) ;
376- result = this . alignLyrics ( result , romaLines , "romanLyric" ) ;
377- }
378- }
379- return result ;
251+ return parseQRCLyric ( qrcContent , trans , roma ) ;
380252 }
381253
382254 /**
@@ -505,6 +377,8 @@ class LyricManager {
505377 await Promise . allSettled ( [ adoptTTML ( ) , adoptLRC ( ) ] ) ;
506378 // 优先使用 TTML
507379 statusStore . usingTTMLLyric = ttmlAdopted ;
380+ // 设置是否使用 QRC 歌词(来自 QQ 音乐,且未被 TTML 覆盖)
381+ statusStore . usingQRCLyric = qqMusicAdopted && ! ttmlAdopted ;
508382 return await this . applyChineseVariant ( this . handleLyricExclude ( result ) ) ;
509383 }
510384
@@ -560,6 +434,7 @@ class LyricManager {
560434 lrcData : aligned . lrcData ,
561435 yrcData : qqLyric . yrcData ,
562436 } ;
437+ statusStore . usingQRCLyric = true ;
563438 }
564439 }
565440 return await this . applyChineseVariant ( aligned ) ;
0 commit comments