@@ -77,61 +77,59 @@ export class LRCLib implements LyricProvider {
7777 }
7878
7979 const filteredResults = [ ] ;
80+ const SIM_THRESHOLD = 0.9 ;
8081 for ( const item of data ) {
81- const { artistName } = item ;
82-
83- const artists = artist . split ( / [ & , ] / g) . map ( ( i ) => i . trim ( ) ) ;
84- const itemArtists = artistName . split ( / [ & , ] / g) . map ( ( i ) => i . trim ( ) ) ;
82+ // quick duration guard to avoid expensive similarity on far-off matches
83+ if ( Math . abs ( item . duration - songDuration ) > 15 ) continue ;
84+ if ( item . instrumental ) continue ;
8585
86- // Try to match using artist name first
87- const permutations = [ ] ;
88- for ( const artistA of artists ) {
89- for ( const artistB of itemArtists ) {
90- permutations . push ( [ artistA . toLowerCase ( ) , artistB . toLowerCase ( ) ] ) ;
91- }
92- }
86+ const { artistName } = item ;
9387
94- for ( const artistA of itemArtists ) {
95- for ( const artistB of artists ) {
96- permutations . push ( [ artistA . toLowerCase ( ) , artistB . toLowerCase ( ) ] ) ;
88+ const artists = artist
89+ . split ( / [ & , ] / g)
90+ . map ( ( i ) => i . trim ( ) . toLowerCase ( ) )
91+ . filter ( Boolean ) ;
92+ const itemArtists = artistName
93+ . split ( / [ & , ] / g)
94+ . map ( ( i ) => i . trim ( ) . toLowerCase ( ) )
95+ . filter ( Boolean ) ;
96+
97+ // fast path: any exact artist match
98+ let ratio = 0 ;
99+ if ( artists . some ( ( a ) => itemArtists . includes ( a ) ) ) {
100+ ratio = 1 ;
101+ } else {
102+ // compute best pairwise similarity with early exit
103+ outer: for ( const a of artists ) {
104+ for ( const b of itemArtists ) {
105+ const r = jaroWinkler ( a , b ) ;
106+ if ( r > ratio ) ratio = r ;
107+ if ( ratio >= 0.97 ) break outer; // good enough, stop early
108+ }
97109 }
98110 }
99111
100- let ratio = Math . max ( ...permutations . map ( ( [ x , y ] ) => jaroWinkler ( x , y ) ) ) ;
101-
102- // If direct artist match is below threshold and we have tags, try matching with tags
103- if ( ratio <= 0.9 && tags && tags . length > 0 ) {
104- // Filter out the artist from tags to avoid duplicate comparisons
105- const filteredTags = tags . filter (
106- ( tag ) => tag . toLowerCase ( ) !== artist . toLowerCase ( ) ,
112+ // If direct artist match is below threshold and we have tags, compare tags too
113+ if ( ratio <= SIM_THRESHOLD && tags && tags . length > 0 ) {
114+ const artistSet = new Set ( artists ) ;
115+ const filteredTags = Array . from (
116+ new Set (
117+ tags
118+ . map ( ( t ) => t . trim ( ) . toLowerCase ( ) )
119+ . filter ( ( t ) => t && ! artistSet . has ( t ) ) ,
120+ ) ,
107121 ) ;
108122
109- const tagPermutations = [ ] ;
110- // Compare each tag with each item artist
111- for ( const tag of filteredTags ) {
112- for ( const itemArtist of itemArtists ) {
113- tagPermutations . push ( [ tag . toLowerCase ( ) , itemArtist . toLowerCase ( ) ] ) ;
123+ outerTags: for ( const t of filteredTags ) {
124+ for ( const b of itemArtists ) {
125+ const r = jaroWinkler ( t , b ) ;
126+ if ( r > ratio ) ratio = r ;
127+ if ( ratio >= 0.97 ) break outerTags ;
114128 }
115129 }
116-
117- // Compare each item artist with each tag
118- for ( const itemArtist of itemArtists ) {
119- for ( const tag of filteredTags ) {
120- tagPermutations . push ( [ itemArtist . toLowerCase ( ) , tag . toLowerCase ( ) ] ) ;
121- }
122- }
123-
124- if ( tagPermutations . length > 0 ) {
125- const tagRatio = Math . max (
126- ...tagPermutations . map ( ( [ x , y ] ) => jaroWinkler ( x , y ) ) ,
127- ) ;
128-
129- // Use the best match ratio between direct artist match and tag match
130- ratio = Math . max ( ratio , tagRatio ) ;
131- }
132130 }
133131
134- if ( ratio <= 0.9 ) continue ;
132+ if ( ratio <= SIM_THRESHOLD ) continue ;
135133 filteredResults . push ( item ) ;
136134 }
137135
@@ -165,9 +163,28 @@ export class LRCLib implements LyricProvider {
165163 status : 'upcoming' as const ,
166164 } ) ) ;
167165
168- // If the final parsed line is not empty, append a computed empty line
169- if ( parsed . length > 0 ) {
170- const last = parsed [ parsed . length - 1 ] ;
166+ // Merge consecutive empty lines into a single empty line
167+ const merged : typeof parsed = [ ] ;
168+ for ( const line of parsed ) {
169+ const isEmpty = ! line . text || ! line . text . trim ( ) ;
170+ if ( isEmpty && merged . length > 0 ) {
171+ const prev = merged [ merged . length - 1 ] ;
172+ const prevEmpty = ! prev . text || ! prev . text . trim ( ) ;
173+ if ( prevEmpty ) {
174+ // extend previous duration to cover this line
175+ const prevEnd = prev . timeInMs + prev . duration ;
176+ const thisEnd = line . timeInMs + line . duration ;
177+ const newEnd = Math . max ( prevEnd , thisEnd ) ;
178+ prev . duration = newEnd - prev . timeInMs ;
179+ continue ; // skip adding this line
180+ }
181+ }
182+ merged . push ( line ) ;
183+ }
184+
185+ // If the final merged line is not empty, append a computed empty line
186+ if ( merged . length > 0 ) {
187+ const last = merged [ merged . length - 1 ] ;
171188 const lastIsEmpty = ! last . text || ! last . text . trim ( ) ;
172189 if ( lastIsEmpty ) {
173190 // last line already empty, don't append another
@@ -191,7 +208,7 @@ export class LRCLib implements LyricProvider {
191208 . toString ( )
192209 . padStart ( 2 , '0' ) } .${ centiseconds . toString ( ) . padStart ( 2 , '0' ) } `;
193210
194- parsed . push ( {
211+ merged . push ( {
195212 timeInMs : midpoint ,
196213 time : timeStr ,
197214 duration : songEnd - midpoint ,
@@ -205,7 +222,7 @@ export class LRCLib implements LyricProvider {
205222 return {
206223 title : closestResult . trackName ,
207224 artists : closestResult . artistName . split ( / [ & , ] / g) ,
208- lines : parsed ,
225+ lines : merged ,
209226 } ;
210227 } else if ( plain ) {
211228 // Fallback to plain if no synced
0 commit comments