From 7f1c4a4d187489a7e78207d6776e8f6d31844edd Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 18 Jan 2026 15:47:07 -0800 Subject: [PATCH] add sync status to lrclib lyrics (#1568) --- src/main/features/core/lyrics/genius.ts | 1 + src/main/features/core/lyrics/lrclib.ts | 5 + src/main/features/core/lyrics/netease.ts | 1 + src/main/features/core/lyrics/shared.ts | 77 +++++++++-- .../components/lyrics-search-form.module.css | 19 +++ .../lyrics/components/lyrics-search-form.tsx | 126 ++++++++++++------ src/shared/types/domain-types.ts | 1 + 7 files changed, 184 insertions(+), 46 deletions(-) diff --git a/src/main/features/core/lyrics/genius.ts b/src/main/features/core/lyrics/genius.ts index 9e2d6c855..ff2029d21 100644 --- a/src/main/features/core/lyrics/genius.ts +++ b/src/main/features/core/lyrics/genius.ts @@ -150,6 +150,7 @@ export async function getSearchResults( return { artist: song.artist_names, id: song.url, + isSync: null, name: song.full_title, source: LyricSource.GENIUS, }; diff --git a/src/main/features/core/lyrics/lrclib.ts b/src/main/features/core/lyrics/lrclib.ts index 9bf49ac28..daa6e5576 100644 --- a/src/main/features/core/lyrics/lrclib.ts +++ b/src/main/features/core/lyrics/lrclib.ts @@ -17,8 +17,12 @@ const TIMEOUT_MS = 5000; export interface LrcLibSearchResponse { albumName: string; artistName: string; + duration?: number; id: number; + instrumental?: boolean; name: string; + plainLyrics: null | string; + syncedLyrics: null | string; } export interface LrcLibTrackResponse { @@ -75,6 +79,7 @@ export async function getSearchResults( return { artist: song.artistName, id: String(song.id), + isSync: song.syncedLyrics ? true : false, name: song.name, source: LyricSource.LRCLIB, }; diff --git a/src/main/features/core/lyrics/netease.ts b/src/main/features/core/lyrics/netease.ts index 97ccae515..b06cbf7ff 100644 --- a/src/main/features/core/lyrics/netease.ts +++ b/src/main/features/core/lyrics/netease.ts @@ -128,6 +128,7 @@ export async function getSearchResults( return { artist, id: String(song.id), + isSync: null, name: song.name, source: LyricSource.NETEASE, }; diff --git a/src/main/features/core/lyrics/shared.ts b/src/main/features/core/lyrics/shared.ts index 11a976bf6..0562a5522 100644 --- a/src/main/features/core/lyrics/shared.ts +++ b/src/main/features/core/lyrics/shared.ts @@ -1,4 +1,4 @@ -import Fuse, { IFuseOptions } from 'fuse.js'; +import Fuse, { FuseResult, IFuseOptions } from 'fuse.js'; import { InternetProviderLyricSearchResponse, @@ -15,20 +15,81 @@ export const orderSearchResults = (args: { fieldNormWeight: 1, includeScore: true, keys: [ - { getFn: (song) => song.name, name: 'name', weight: 3 }, - { getFn: (song) => song.artist, name: 'artist' }, + { getFn: (song) => song.name, name: 'name', weight: 2 }, + { getFn: (song) => song.artist, name: 'artist', weight: 2 }, ], - threshold: 1.0, + threshold: 0.6, }; const fuse = new Fuse(results, options); - const searchResults = fuse.search({ - ...(params.artist && { artist: params.artist }), - ...(params.name && { name: params.name }), + let searchResults: Array>; + + if (params.artist && params.name) { + const artistFuse = new Fuse(results, { + includeScore: true, + keys: [{ getFn: (song) => song.artist, name: 'artist' }], + threshold: 0.6, + }); + + const nameFuse = new Fuse(results, { + includeScore: true, + keys: [{ getFn: (song) => song.name, name: 'name' }], + threshold: 0.6, + }); + + const artistResults = artistFuse.search(params.artist); + const nameResults = nameFuse.search(params.name); + + const artistScores = new Map(artistResults.map((r) => [r.item.id, r.score ?? 1])); + const nameScores = new Map(nameResults.map((r) => [r.item.id, r.score ?? 1])); + + const combinedResults = new Map>(); + + artistResults.forEach((result) => { + const nameScore = nameScores.get(result.item.id); + if (nameScore !== undefined) { + const combinedScore = Math.max(result.score ?? 1, nameScore); + combinedResults.set(result.item.id, { + ...result, + score: combinedScore, + }); + } + }); + + nameResults.forEach((result) => { + if (!combinedResults.has(result.item.id)) { + const artistScore = artistScores.get(result.item.id); + if (artistScore !== undefined) { + const combinedScore = Math.max(result.score ?? 1, artistScore); + combinedResults.set(result.item.id, { + ...result, + score: combinedScore, + }); + } + } + }); + + searchResults = Array.from(combinedResults.values()); + } else { + searchResults = fuse.search({ + ...(params.artist && { artist: params.artist }), + ...(params.name && { name: params.name }), + }); + } + + const sortedResults = searchResults.sort((a, b) => { + const aIsSync = a.item.isSync === true ? 1 : 0; + const bIsSync = b.item.isSync === true ? 1 : 0; + + if (aIsSync !== bIsSync) { + return bIsSync - aIsSync; + } + + return (a.score || 0) - (b.score || 0); }); - return searchResults.map((result) => ({ + return sortedResults.map((result) => ({ ...result.item, score: result.score, })); diff --git a/src/renderer/features/lyrics/components/lyrics-search-form.module.css b/src/renderer/features/lyrics/components/lyrics-search-form.module.css index d5470d656..eabaca981 100644 --- a/src/renderer/features/lyrics/components/lyrics-search-form.module.css +++ b/src/renderer/features/lyrics/components/lyrics-search-form.module.css @@ -33,3 +33,22 @@ } } } + +.lyrics-preview { + :global(.synchronized-lyrics) { + height: auto !important; + padding: 1rem 0 !important; + overflow: visible !important; + transform: none !important; + } +} + +.lyrics-content-wrapper { + :global(> div) { + height: auto !important; + max-height: none !important; + padding: 1rem 0 !important; + overflow: visible !important; + transform: none !important; + } +} diff --git a/src/renderer/features/lyrics/components/lyrics-search-form.tsx b/src/renderer/features/lyrics/components/lyrics-search-form.tsx index 1475473ef..9adea447b 100644 --- a/src/renderer/features/lyrics/components/lyrics-search-form.tsx +++ b/src/renderer/features/lyrics/components/lyrics-search-form.tsx @@ -19,6 +19,7 @@ import { UnsynchronizedLyricsProps, } from '/@/renderer/features/lyrics/unsynchronized-lyrics'; import { usePlayerSong } from '/@/renderer/store'; +import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Button } from '/@/shared/components/button/button'; import { Center } from '/@/shared/components/center/center'; import { Divider } from '/@/shared/components/divider/divider'; @@ -43,7 +44,8 @@ interface SearchResultProps { onClick?: () => void; } const SearchResult = ({ data, isSelected, onClick }: SearchResultProps) => { - const { artist, id, name, score, source } = data; + const { t } = useTranslation(); + const { artist, id, isSync, name, score, source } = data; const percentageScore = useMemo(() => { if (!score) return 0; @@ -53,6 +55,21 @@ const SearchResult = ({ data, isSelected, onClick }: SearchResultProps) => { const cleanId = source === LyricSource.GENIUS ? id.replace(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)/g, '') : id; + const syncStatus = useMemo(() => { + if (isSync === true) { + return t('page.fullscreenPlayer.config.synchronized', { + postProcess: 'sentenceCase', + }); + } + if (isSync === false) { + return t('page.fullscreenPlayer.config.unsynchronized', { + postProcess: 'sentenceCase', + }); + } + + return t('common.unknown', { postProcess: 'titleCase' }); + }, [isSync, t]); + return (