rework auto lyrics matcher (#1569)

- remove priority order in favor of best match from all selected providers (lrclib synchronized)
- improve % match for title / artist
This commit is contained in:
jeffvli
2026-01-18 16:02:37 -08:00
parent 7f1c4a4d18
commit d10e4a3d68
2 changed files with 78 additions and 51 deletions
+1 -1
View File
@@ -848,7 +848,7 @@
"lastfmApiKey": "{{lastfm}} API key",
"lyricFetch_description": "fetch lyrics from various internet sources",
"lyricFetch": "fetch lyrics from the internet",
"lyricFetchProvider_description": "select the providers to fetch lyrics from. the order of the providers is the order in which they will be queried",
"lyricFetchProvider_description": "select the providers to fetch lyrics from",
"lyricFetchProvider": "providers to fetch lyrics from",
"lyricOffset_description": "offset the lyric by the specified amount of milliseconds",
"lyricOffset": "lyric offset (ms)",
+77 -50
View File
@@ -1,21 +1,10 @@
import { ipcMain } from 'electron';
import { store } from '../settings';
import {
getLyricsBySongId as getGenius,
query as queryGenius,
getSearchResults as searchGenius,
} from './genius';
import {
getLyricsBySongId as getLrcLib,
query as queryLrclib,
getSearchResults as searchLrcLib,
} from './lrclib';
import {
getLyricsBySongId as getNetease,
query as queryNetease,
getSearchResults as searchNetease,
} from './netease';
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease';
import { orderSearchResults } from './shared';
import { Song } from '/@/shared/types/domain-types';
@@ -42,6 +31,7 @@ export type InternetProviderLyricResponse = {
export type InternetProviderLyricSearchResponse = {
artist: string;
id: string;
isSync: boolean | null;
name: string;
score?: number;
source: LyricSource;
@@ -72,14 +62,6 @@ type SearchFetcher = (
params: LyricSearchQuery,
) => Promise<InternetProviderLyricSearchResponse[] | null>;
type SongFetcher = (params: LyricSearchQuery) => Promise<InternetProviderLyricResponse | null>;
const FETCHERS: Record<LyricSource, SongFetcher> = {
[LyricSource.GENIUS]: queryGenius,
[LyricSource.LRCLIB]: queryLrclib,
[LyricSource.NETEASE]: queryNetease,
};
const SEARCH_FETCHERS: Record<LyricSource, SearchFetcher> = {
[LyricSource.GENIUS]: searchGenius,
[LyricSource.LRCLIB]: searchLrcLib,
@@ -108,39 +90,84 @@ const getRemoteLyrics = async (song: Song) => {
}
}
let lyricsFromSource: InternetProviderLyricResponse | null = null;
const params: LyricSearchQuery = {
album: song.album || song.name,
artist: song.artists[0].name,
duration: song.duration / 1000.0,
name: song.name,
};
const allSearchResults: InternetProviderLyricSearchResponse[] = [];
for (const source of sources) {
const params = {
album: song.album || song.name,
artist: song.artists[0].name,
duration: song.duration / 1000.0,
name: song.name,
};
const response = await FETCHERS[source](params as unknown as LyricSearchQuery);
if (response) {
const newResult = cached
? {
...cached,
[source]: response,
}
: ({ [source]: response } as CachedLyrics);
if (lyricCache.size === MAX_CACHED_ITEMS && cached === undefined) {
const toRemove = lyricCache.keys().next().value;
if (toRemove) {
lyricCache.delete(toRemove);
}
try {
const searchResults = await SEARCH_FETCHERS[source](params);
if (searchResults) {
allSearchResults.push(...searchResults);
}
lyricCache.set(song.id.toString(), newResult);
lyricsFromSource = response;
break;
} catch (error) {
console.error(`Error searching ${source} for lyrics:`, error);
}
}
if (allSearchResults.length === 0) {
return null;
}
const rankedResults = orderSearchResults({
params,
results: allSearchResults,
});
const bestMatch = rankedResults[0];
if (!bestMatch) {
return null;
}
// Score is 0-1 where 0 = perfect match, 1 = worst match
const matchThreshold = 0.55;
const matchScore = bestMatch.score ?? 1;
if (matchScore > matchThreshold) {
return null;
}
let lyricsFromSource: InternetProviderLyricResponse | null = null;
try {
const lyrics = await GET_FETCHERS[bestMatch.source](bestMatch.id);
if (lyrics) {
lyricsFromSource = {
artist: bestMatch.artist,
id: bestMatch.id,
lyrics,
name: bestMatch.name,
source: bestMatch.source,
};
}
} catch (error) {
console.error(`Error fetching lyrics from ${bestMatch.source}:`, error);
}
if (lyricsFromSource) {
const newResult = cached
? {
...cached,
[lyricsFromSource.source]: lyricsFromSource,
}
: ({ [lyricsFromSource.source]: lyricsFromSource } as CachedLyrics);
if (lyricCache.size === MAX_CACHED_ITEMS && cached === undefined) {
const toRemove = lyricCache.keys().next().value;
if (toRemove) {
lyricCache.delete(toRemove);
}
}
lyricCache.set(song.id.toString(), newResult);
}
return lyricsFromSource;
};