@@ -17,7 +17,7 @@ class DownloadManager {
1717 private queue : DownloadTask [ ] = [ ] ;
1818 private isProcessing : boolean = false ;
1919
20- private constructor ( ) { }
20+ private constructor ( ) { }
2121
2222 public static getInstance ( ) : DownloadManager {
2323 if ( ! DownloadManager . instance ) {
@@ -211,12 +211,11 @@ class DownloadManager {
211211 type = result . data . type ?. toLowerCase ( ) || "mp3" ;
212212 }
213213
214- const infoObj =
215- songManager . getPlayerInfoObj ( song ) || {
216- name : song . name || "未知歌曲" ,
217- artist : "未知歌手" ,
218- album : "未知专辑" ,
219- } ;
214+ const infoObj = songManager . getPlayerInfoObj ( song ) || {
215+ name : song . name || "未知歌曲" ,
216+ artist : "未知歌手" ,
217+ album : "未知专辑" ,
218+ } ;
220219
221220 const baseTitle = infoObj . name || "未知歌曲" ;
222221 const rawArtist = infoObj . artist || "未知歌手" ;
@@ -256,14 +255,139 @@ class DownloadManager {
256255 let lyric = "" ;
257256 if ( downloadLyric ) {
258257 const lyricResult = await songLyric ( song . id ) ;
259- const rawLyric = lyricResult ?. lrc ?. lyric || "" ;
258+
260259 // 排除特定格式的脏数据
261- const excludeRegex =
262- / ^ \{ " t " : \d + , " c " : \[ \{ " [ ^ " ] + " : \ "[ ^ " ] * \ "} (?: , \{ " [ ^ " ] + " : \ "[ ^ " ] * \ "} ) * ] } $ / ;
260+ const rawLyric = lyricResult ?. lrc ?. lyric || "" ;
261+ const excludeRegex = / ^ \{ " t " : \d + , " c " : \[ \{ " [ ^ " ] + " : " [ ^ " ] * " } (?: , \{ " [ ^ " ] + " : " [ ^ " ] * " } ) * ] } $ / ;
263262 lyric = rawLyric
264263 . split ( "\n" )
265- . filter ( ( line ) => ! excludeRegex . test ( line . trim ( ) ) )
264+ . filter ( ( line : string ) => ! excludeRegex . test ( line . trim ( ) ) )
266265 . join ( "\n" ) ;
266+
267+ const lrc = lyricResult ?. lrc ?. lyric ;
268+ const tlyric = settingStore . downloadLyricTranslation ? lyricResult ?. tlyric ?. lyric : null ;
269+ const romalrc = settingStore . downloadLyricRomaji ? lyricResult ?. romalrc ?. lyric : null ;
270+
271+ if ( lrc ) {
272+ // 正则:匹配 [mm:ss.xx] 或 [mm:ss.xxx] 形式的时间标签
273+ const timeTagRe = / \[ ( \d { 2 } ) : ( \d { 2 } ) (?: \. ( \d { 1 , 3 } ) ) ? \] / g;
274+
275+ // 把时间字符串转成秒(用于模糊匹配),例如 "00:06.52" -> 6.52
276+ const timeStrToSeconds = ( timeStr : string ) => {
277+ // timeStr 形如 "00:06.52" 或 "00:06"(不带小数)
278+ const m = timeStr . match ( / ^ ( \d { 2 } ) : ( \d { 2 } ) (?: \. ( \d { 1 , 3 } ) ) ? $ / ) ;
279+ if ( ! m ) return 0 ;
280+ const minutes = parseInt ( m [ 1 ] , 10 ) ;
281+ const seconds = parseInt ( m [ 2 ] , 10 ) ;
282+ const frac = m [ 3 ] ? parseInt ( ( m [ 3 ] + "00" ) . slice ( 0 , 3 ) , 10 ) : 0 ; // 保证到毫秒位
283+ return minutes * 60 + seconds + frac / 1000 ;
284+ } ;
285+
286+ /**
287+ * 解析歌词字符串为映射表
288+ * @param lyricStr 歌词字符串
289+ * @returns 映射表
290+ */
291+ const parseToMap = ( lyricStr : string ) => {
292+ const map = new Map < string , string > ( ) ;
293+ if ( ! lyricStr ) return map ;
294+ const lines = lyricStr . split ( / \r ? \n / ) ;
295+ for ( const raw of lines ) {
296+ let m : RegExpExecArray | null ;
297+ const timeTags : string [ ] = [ ] ;
298+ timeTagRe . lastIndex = 0 ;
299+ while ( ( m = timeTagRe . exec ( raw ) ) !== null ) {
300+ const frac = m [ 3 ] ?? "" ;
301+ const tag = `[${ m [ 1 ] } :${ m [ 2 ] } ${ frac ? "." + frac : "" } ]` ;
302+ timeTags . push ( tag ) ;
303+ }
304+ const text = raw . replace ( timeTagRe , "" ) . trim ( ) ;
305+ for ( const tag of timeTags ) {
306+ if ( text ) {
307+ const prev = map . get ( tag ) ;
308+ map . set ( tag , prev ? prev + "\n" + text : text ) ;
309+ }
310+ }
311+ }
312+ return map ;
313+ } ;
314+
315+ /**
316+ * 查找匹配文本(精确或模糊)
317+ * @param map 映射表
318+ * @param currentTag 当前时间标签
319+ * @returns 匹配文本
320+ */
321+ const findMatch = ( map : Map < string , string > , currentTag : string ) => {
322+ // 精确匹配
323+ const exact = map . get ( currentTag ) ;
324+ if ( exact ) return exact ;
325+
326+ // 模糊匹配
327+ const tSec = timeStrToSeconds ( currentTag . slice ( 1 , - 1 ) ) ;
328+ let bestTag : string | null = null ;
329+ let bestDiff = Infinity ;
330+ for ( const key of Array . from ( map . keys ( ) ) ) {
331+ const kSec = timeStrToSeconds ( key . slice ( 1 , - 1 ) ) ;
332+ const diff = Math . abs ( kSec - tSec ) ;
333+ if ( diff < bestDiff ) {
334+ bestDiff = diff ;
335+ bestTag = key ;
336+ }
337+ }
338+ if ( bestTag && bestDiff < 0.5 ) {
339+ return map . get ( bestTag ) ;
340+ }
341+ return null ;
342+ } ;
343+
344+ // 解析翻译和音译歌词
345+ const tMap = parseToMap ( tlyric || "" ) ;
346+ const rMap = parseToMap ( romalrc || "" ) ;
347+
348+ const lines : string [ ] = [ ] ;
349+
350+ // 逐行解析原始 lrc 字符串
351+ const lrcLinesRaw = lrc . split ( / \r ? \n / ) ;
352+ for ( const raw of lrcLinesRaw ) {
353+ let m : RegExpExecArray | null ;
354+ const timeTags : string [ ] = [ ] ;
355+ timeTagRe . lastIndex = 0 ;
356+ while ( ( m = timeTagRe . exec ( raw ) ) !== null ) {
357+ const frac = m [ 3 ] ?? "" ;
358+ const tag = `[${ m [ 1 ] } :${ m [ 2 ] } ${ frac ? "." + frac : "" } ]` ;
359+ timeTags . push ( tag ) ;
360+ }
361+
362+ if ( timeTags . length === 0 ) continue ;
363+
364+ const text = raw . replace ( timeTagRe , "" ) . trim ( ) ;
365+ if ( ! text ) continue ;
366+
367+ for ( const timeTag of timeTags ) {
368+ lines . push ( `${ timeTag } ${ text } ` ) ;
369+
370+ // 添加翻译和音译歌词
371+ const lyricMaps = [
372+ { map : tMap , enabled : tlyric } ,
373+ { map : rMap , enabled : romalrc } ,
374+ ] ;
375+
376+ for ( const { map, enabled } of lyricMaps ) {
377+ if ( enabled ) {
378+ const matchedText = findMatch ( map , timeTag ) ;
379+ if ( matchedText ) {
380+ for ( const lt of matchedText . split ( "\n" ) ) {
381+ if ( lt . trim ( ) ) lines . push ( `${ timeTag } ${ lt } ` ) ;
382+ }
383+ }
384+ }
385+ }
386+ }
387+ }
388+
389+ lyric = lines . join ( "\n" ) ;
390+ }
267391 }
268392
269393 const config = {
0 commit comments