Skip to content

Commit 91bec96

Browse files
committed
refactor(synced-lyrics): optimize artist matching and merge consecutive empty lines
1 parent b7c19d8 commit 91bec96

File tree

1 file changed

+65
-48
lines changed

1 file changed

+65
-48
lines changed

src/plugins/synced-lyrics/providers/LRCLib.ts

Lines changed: 65 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)