mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add sync status to lrclib lyrics (#1568)
This commit is contained in:
@@ -150,6 +150,7 @@ export async function getSearchResults(
|
|||||||
return {
|
return {
|
||||||
artist: song.artist_names,
|
artist: song.artist_names,
|
||||||
id: song.url,
|
id: song.url,
|
||||||
|
isSync: null,
|
||||||
name: song.full_title,
|
name: song.full_title,
|
||||||
source: LyricSource.GENIUS,
|
source: LyricSource.GENIUS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,8 +17,12 @@ const TIMEOUT_MS = 5000;
|
|||||||
export interface LrcLibSearchResponse {
|
export interface LrcLibSearchResponse {
|
||||||
albumName: string;
|
albumName: string;
|
||||||
artistName: string;
|
artistName: string;
|
||||||
|
duration?: number;
|
||||||
id: number;
|
id: number;
|
||||||
|
instrumental?: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
plainLyrics: null | string;
|
||||||
|
syncedLyrics: null | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LrcLibTrackResponse {
|
export interface LrcLibTrackResponse {
|
||||||
@@ -75,6 +79,7 @@ export async function getSearchResults(
|
|||||||
return {
|
return {
|
||||||
artist: song.artistName,
|
artist: song.artistName,
|
||||||
id: String(song.id),
|
id: String(song.id),
|
||||||
|
isSync: song.syncedLyrics ? true : false,
|
||||||
name: song.name,
|
name: song.name,
|
||||||
source: LyricSource.LRCLIB,
|
source: LyricSource.LRCLIB,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export async function getSearchResults(
|
|||||||
return {
|
return {
|
||||||
artist,
|
artist,
|
||||||
id: String(song.id),
|
id: String(song.id),
|
||||||
|
isSync: null,
|
||||||
name: song.name,
|
name: song.name,
|
||||||
source: LyricSource.NETEASE,
|
source: LyricSource.NETEASE,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Fuse, { IFuseOptions } from 'fuse.js';
|
import Fuse, { FuseResult, IFuseOptions } from 'fuse.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
@@ -15,20 +15,81 @@ export const orderSearchResults = (args: {
|
|||||||
fieldNormWeight: 1,
|
fieldNormWeight: 1,
|
||||||
includeScore: true,
|
includeScore: true,
|
||||||
keys: [
|
keys: [
|
||||||
{ getFn: (song) => song.name, name: 'name', weight: 3 },
|
{ getFn: (song) => song.name, name: 'name', weight: 2 },
|
||||||
{ getFn: (song) => song.artist, name: 'artist' },
|
{ getFn: (song) => song.artist, name: 'artist', weight: 2 },
|
||||||
],
|
],
|
||||||
threshold: 1.0,
|
threshold: 0.6,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fuse = new Fuse(results, options);
|
const fuse = new Fuse(results, options);
|
||||||
|
|
||||||
const searchResults = fuse.search<InternetProviderLyricSearchResponse>({
|
let searchResults: Array<FuseResult<InternetProviderLyricSearchResponse>>;
|
||||||
|
|
||||||
|
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<string, FuseResult<InternetProviderLyricSearchResponse>>();
|
||||||
|
|
||||||
|
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<InternetProviderLyricSearchResponse>({
|
||||||
...(params.artist && { artist: params.artist }),
|
...(params.artist && { artist: params.artist }),
|
||||||
...(params.name && { name: params.name }),
|
...(params.name && { name: params.name }),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return searchResults.map((result) => ({
|
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 sortedResults.map((result) => ({
|
||||||
...result.item,
|
...result.item,
|
||||||
score: result.score,
|
score: result.score,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
UnsynchronizedLyricsProps,
|
UnsynchronizedLyricsProps,
|
||||||
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
||||||
import { usePlayerSong } from '/@/renderer/store';
|
import { usePlayerSong } from '/@/renderer/store';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Center } from '/@/shared/components/center/center';
|
import { Center } from '/@/shared/components/center/center';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
@@ -43,7 +44,8 @@ interface SearchResultProps {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
const SearchResult = ({ data, isSelected, onClick }: SearchResultProps) => {
|
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(() => {
|
const percentageScore = useMemo(() => {
|
||||||
if (!score) return 0;
|
if (!score) return 0;
|
||||||
@@ -53,6 +55,21 @@ const SearchResult = ({ data, isSelected, onClick }: SearchResultProps) => {
|
|||||||
const cleanId =
|
const cleanId =
|
||||||
source === LyricSource.GENIUS ? id.replace(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)/g, '') : id;
|
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 (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx(styles.searchItem, {
|
className={clsx(styles.searchItem, {
|
||||||
@@ -68,7 +85,7 @@ const SearchResult = ({ data, isSelected, onClick }: SearchResultProps) => {
|
|||||||
<Text isMuted>{artist}</Text>
|
<Text isMuted>{artist}</Text>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Text isMuted size="sm">
|
<Text isMuted size="sm">
|
||||||
{[source, cleanId].join(' — ')}
|
{[source, cleanId, syncStatus].join(' — ')}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -171,6 +188,16 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
context: 'name',
|
context: 'name',
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
})}
|
})}
|
||||||
|
rightSection={
|
||||||
|
form.values.name ? (
|
||||||
|
<ActionIcon
|
||||||
|
icon="x"
|
||||||
|
onClick={() => form.setFieldValue('name', '')}
|
||||||
|
size="sm"
|
||||||
|
variant="transparent"
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -178,13 +205,23 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
context: 'artist',
|
context: 'artist',
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
})}
|
})}
|
||||||
|
rightSection={
|
||||||
|
form.values.artist ? (
|
||||||
|
<ActionIcon
|
||||||
|
icon="x"
|
||||||
|
onClick={() => form.setFieldValue('artist', '')}
|
||||||
|
size="sm"
|
||||||
|
variant="transparent"
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
{...form.getInputProps('artist')}
|
{...form.getInputProps('artist')}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Group align="flex-start" grow style={{ flex: 1, minHeight: 0 }}>
|
<Group align="flex-start" grow style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
||||||
<Stack style={{ flex: 1, height: '100%', minHeight: 0 }}>
|
<Stack style={{ flex: 1, height: '100%', minHeight: 0, overflow: 'hidden' }}>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -211,11 +248,22 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Stack>
|
</Stack>
|
||||||
{selectedResult && (
|
{selectedResult && (
|
||||||
<Stack style={{ flex: 1, height: '100%', minHeight: 0 }}>
|
<Stack style={{ flex: 1, height: '100%', minHeight: 0, overflow: 'hidden' }}>
|
||||||
|
<ScrollArea
|
||||||
|
className={styles['lyrics-preview']}
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
paddingRight: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{isPreviewLoading ? (
|
{isPreviewLoading ? (
|
||||||
<Spinner container />
|
<Spinner container />
|
||||||
) : previewData ? (
|
) : previewData ? (
|
||||||
Array.isArray(previewData) ? (
|
<div
|
||||||
|
className={styles['lyrics-content-wrapper']}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{Array.isArray(previewData) ? (
|
||||||
<SynchronizedLyrics
|
<SynchronizedLyrics
|
||||||
style={{ padding: 0 }}
|
style={{ padding: 0 }}
|
||||||
{...({
|
{...({
|
||||||
@@ -236,7 +284,8 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
source: selectedResult.source,
|
source: selectedResult.source,
|
||||||
} as UnsynchronizedLyricsProps)}
|
} as UnsynchronizedLyricsProps)}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Center>
|
<Center>
|
||||||
<Text isMuted>
|
<Text isMuted>
|
||||||
@@ -246,6 +295,7 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
</Text>
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
|
</ScrollArea>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -1200,6 +1200,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;
|
||||||
|
|||||||
Reference in New Issue
Block a user