mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
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:
@@ -848,7 +848,7 @@
|
|||||||
"lastfmApiKey": "{{lastfm}} API key",
|
"lastfmApiKey": "{{lastfm}} API key",
|
||||||
"lyricFetch_description": "fetch lyrics from various internet sources",
|
"lyricFetch_description": "fetch lyrics from various internet sources",
|
||||||
"lyricFetch": "fetch lyrics from the internet",
|
"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",
|
"lyricFetchProvider": "providers to fetch lyrics from",
|
||||||
"lyricOffset_description": "offset the lyric by the specified amount of milliseconds",
|
"lyricOffset_description": "offset the lyric by the specified amount of milliseconds",
|
||||||
"lyricOffset": "lyric offset (ms)",
|
"lyricOffset": "lyric offset (ms)",
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
|
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
import {
|
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
|
||||||
getLyricsBySongId as getGenius,
|
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
|
||||||
query as queryGenius,
|
import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease';
|
||||||
getSearchResults as searchGenius,
|
import { orderSearchResults } from './shared';
|
||||||
} 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 { Song } from '/@/shared/types/domain-types';
|
import { Song } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
@@ -42,6 +31,7 @@ export type InternetProviderLyricResponse = {
|
|||||||
export type InternetProviderLyricSearchResponse = {
|
export type InternetProviderLyricSearchResponse = {
|
||||||
artist: string;
|
artist: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
isSync: boolean | null;
|
||||||
name: string;
|
name: string;
|
||||||
score?: number;
|
score?: number;
|
||||||
source: LyricSource;
|
source: LyricSource;
|
||||||
@@ -72,14 +62,6 @@ type SearchFetcher = (
|
|||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
) => Promise<InternetProviderLyricSearchResponse[] | null>;
|
) => 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> = {
|
const SEARCH_FETCHERS: Record<LyricSource, SearchFetcher> = {
|
||||||
[LyricSource.GENIUS]: searchGenius,
|
[LyricSource.GENIUS]: searchGenius,
|
||||||
[LyricSource.LRCLIB]: searchLrcLib,
|
[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) {
|
for (const source of sources) {
|
||||||
const params = {
|
try {
|
||||||
album: song.album || song.name,
|
const searchResults = await SEARCH_FETCHERS[source](params);
|
||||||
artist: song.artists[0].name,
|
if (searchResults) {
|
||||||
duration: song.duration / 1000.0,
|
allSearchResults.push(...searchResults);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
lyricCache.set(song.id.toString(), newResult);
|
console.error(`Error searching ${source} for lyrics:`, error);
|
||||||
|
|
||||||
lyricsFromSource = response;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return lyricsFromSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user