Improve lyrics persistence
- Search overrides existing lyrics query - Separate reset / clear commands
This commit is contained in:
parent
80fb844d3c
commit
f6d239d87c
4 changed files with 133 additions and 71 deletions
|
@ -1111,6 +1111,7 @@ export type LyricSearchQuery = {
|
||||||
export type LyricGetQuery = {
|
export type LyricGetQuery = {
|
||||||
remoteSongId: string;
|
remoteSongId: string;
|
||||||
remoteSource: LyricSource;
|
remoteSource: LyricSource;
|
||||||
|
song: Song;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum LyricSource {
|
export enum LyricSource {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Box, Group } from '@mantine/core';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { RiAddFill, RiSubtractFill } from 'react-icons/ri';
|
import { RiAddFill, RiSubtractFill } from 'react-icons/ri';
|
||||||
import { LyricsOverride } from '/@/renderer/api/types';
|
import { LyricsOverride } from '/@/renderer/api/types';
|
||||||
|
@ -12,10 +13,15 @@ import {
|
||||||
|
|
||||||
interface LyricsActionsProps {
|
interface LyricsActionsProps {
|
||||||
onRemoveLyric: () => void;
|
onRemoveLyric: () => void;
|
||||||
|
onResetLyric: () => void;
|
||||||
onSearchOverride: (params: LyricsOverride) => void;
|
onSearchOverride: (params: LyricsOverride) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LyricsActions = ({ onRemoveLyric, onSearchOverride }: LyricsActionsProps) => {
|
export const LyricsActions = ({
|
||||||
|
onRemoveLyric,
|
||||||
|
onResetLyric,
|
||||||
|
onSearchOverride,
|
||||||
|
}: LyricsActionsProps) => {
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
const { delayMs, sources } = useLyricsSettings();
|
const { delayMs, sources } = useLyricsSettings();
|
||||||
|
@ -33,59 +39,75 @@ export const LyricsActions = ({ onRemoveLyric, onSearchOverride }: LyricsActions
|
||||||
const isDesktop = isElectron();
|
const isDesktop = isElectron();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box style={{ position: 'relative', width: '100%' }}>
|
||||||
{isDesktop && sources.length ? (
|
<Group position="center">
|
||||||
|
{isDesktop && sources.length ? (
|
||||||
|
<Button
|
||||||
|
uppercase
|
||||||
|
disabled={isActionsDisabled}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() =>
|
||||||
|
openLyricSearchModal({
|
||||||
|
artist: currentSong?.artistName,
|
||||||
|
name: currentSong?.name,
|
||||||
|
onSearchOverride,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
uppercase
|
aria-label="Decrease lyric offset"
|
||||||
disabled={isActionsDisabled}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() =>
|
onClick={() => handleLyricOffset(delayMs - 50)}
|
||||||
openLyricSearchModal({
|
|
||||||
artist: currentSong?.artistName,
|
|
||||||
name: currentSong?.name,
|
|
||||||
onSearchOverride,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Search
|
<RiSubtractFill />
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
<Tooltip
|
||||||
<Button
|
label="Offset (ms)"
|
||||||
aria-label="Decrease lyric offset"
|
openDelay={500}
|
||||||
variant="subtle"
|
>
|
||||||
onClick={() => handleLyricOffset(delayMs - 50)}
|
<NumberInput
|
||||||
>
|
aria-label="Lyric offset"
|
||||||
<RiSubtractFill />
|
styles={{ input: { textAlign: 'center' } }}
|
||||||
</Button>
|
value={delayMs || 0}
|
||||||
<Tooltip
|
width={55}
|
||||||
label="Offset (ms)"
|
onChange={handleLyricOffset}
|
||||||
openDelay={500}
|
/>
|
||||||
>
|
</Tooltip>
|
||||||
<NumberInput
|
|
||||||
aria-label="Lyric offset"
|
|
||||||
styles={{ input: { textAlign: 'center' } }}
|
|
||||||
value={delayMs || 0}
|
|
||||||
width={55}
|
|
||||||
onChange={handleLyricOffset}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Button
|
|
||||||
aria-label="Increase lyric offset"
|
|
||||||
variant="subtle"
|
|
||||||
onClick={() => handleLyricOffset(delayMs + 50)}
|
|
||||||
>
|
|
||||||
<RiAddFill />
|
|
||||||
</Button>
|
|
||||||
{isDesktop && sources.length ? (
|
|
||||||
<Button
|
<Button
|
||||||
uppercase
|
aria-label="Increase lyric offset"
|
||||||
disabled={isActionsDisabled}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={onRemoveLyric}
|
onClick={() => handleLyricOffset(delayMs + 50)}
|
||||||
>
|
>
|
||||||
Clear
|
<RiAddFill />
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
{isDesktop && sources.length ? (
|
||||||
</>
|
<Button
|
||||||
|
uppercase
|
||||||
|
disabled={isActionsDisabled}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={onResetLyric}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Box style={{ position: 'absolute', right: 0, top: 0 }}>
|
||||||
|
{isDesktop && sources.length ? (
|
||||||
|
<Button
|
||||||
|
uppercase
|
||||||
|
color="red"
|
||||||
|
disabled={isActionsDisabled}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={onRemoveLyric}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Center, Group } from '@mantine/core';
|
import { Center, Group } from '@mantine/core';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { clear } from 'idb-keyval';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { RiInformationFill } from 'react-icons/ri';
|
import { RiInformationFill } from 'react-icons/ri';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
@ -9,7 +10,7 @@ import { SynchronizedLyrics } from './synchronized-lyrics';
|
||||||
import { Spinner, TextTitle } from '/@/renderer/components';
|
import { Spinner, TextTitle } from '/@/renderer/components';
|
||||||
import { ErrorFallback } from '/@/renderer/features/action-required';
|
import { ErrorFallback } from '/@/renderer/features/action-required';
|
||||||
import { UnsynchronizedLyrics } from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
import { UnsynchronizedLyrics } from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
||||||
import { getServerById, useCurrentSong, usePlayerStore } from '/@/renderer/store';
|
import { useCurrentSong, usePlayerStore } from '/@/renderer/store';
|
||||||
import {
|
import {
|
||||||
FullLyricsMetadata,
|
FullLyricsMetadata,
|
||||||
LyricsOverride,
|
LyricsOverride,
|
||||||
|
@ -17,6 +18,8 @@ import {
|
||||||
UnsynchronizedLyricMetadata,
|
UnsynchronizedLyricMetadata,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { queryClient } from '/@/renderer/lib/react-query';
|
||||||
|
|
||||||
const ActionsContainer = styled.div`
|
const ActionsContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -93,14 +96,11 @@ function isSynchronized(
|
||||||
|
|
||||||
export const Lyrics = () => {
|
export const Lyrics = () => {
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const currentServer = getServerById(currentSong?.serverId);
|
|
||||||
|
|
||||||
const [clear, setClear] = useState(false);
|
|
||||||
|
|
||||||
const { data, isInitialLoading } = useSongLyricsBySong(
|
const { data, isInitialLoading } = useSongLyricsBySong(
|
||||||
{
|
{
|
||||||
query: { songId: currentSong?.id || '' },
|
query: { songId: currentSong?.id || '' },
|
||||||
serverId: currentServer?.id,
|
serverId: currentSong?.serverId || '',
|
||||||
},
|
},
|
||||||
currentSong,
|
currentSong,
|
||||||
);
|
);
|
||||||
|
@ -109,18 +109,41 @@ export const Lyrics = () => {
|
||||||
|
|
||||||
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
|
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
|
||||||
setOverride(params);
|
setOverride(params);
|
||||||
setClear(false);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { data: overrideLyrics, isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({
|
const handleOnResetLyric = useCallback(() => {
|
||||||
|
queryClient.resetQueries({
|
||||||
|
exact: true,
|
||||||
|
queryKey: queryKeys.songs.lyrics(currentSong?.serverId, { songId: currentSong?.id }),
|
||||||
|
});
|
||||||
|
}, [currentSong?.id, currentSong?.serverId]);
|
||||||
|
|
||||||
|
const handleOnRemoveLyric = useCallback(() => {
|
||||||
|
queryClient.setQueryData(
|
||||||
|
queryKeys.songs.lyrics(currentSong?.serverId, { songId: currentSong?.id }),
|
||||||
|
(prev: FullLyricsMetadata | undefined) => {
|
||||||
|
if (!prev) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
lyrics: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [currentSong?.id, currentSong?.serverId]);
|
||||||
|
|
||||||
|
const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({
|
||||||
options: {
|
options: {
|
||||||
enabled: !!override,
|
enabled: !!override,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
remoteSongId: override?.id,
|
remoteSongId: override?.id,
|
||||||
remoteSource: override?.source,
|
remoteSource: override?.source,
|
||||||
|
song: currentSong,
|
||||||
},
|
},
|
||||||
serverId: currentServer?.id,
|
serverId: currentSong?.serverId,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -128,7 +151,6 @@ export const Lyrics = () => {
|
||||||
(state) => state.current.song,
|
(state) => state.current.song,
|
||||||
() => {
|
() => {
|
||||||
setOverride(undefined);
|
setOverride(undefined);
|
||||||
setClear(false);
|
|
||||||
},
|
},
|
||||||
{ equalityFn: (a, b) => a?.id === b?.id },
|
{ equalityFn: (a, b) => a?.id === b?.id },
|
||||||
);
|
);
|
||||||
|
@ -140,20 +162,12 @@ export const Lyrics = () => {
|
||||||
|
|
||||||
const isLoadingLyrics = isInitialLoading || isOverrideLoading;
|
const isLoadingLyrics = isInitialLoading || isOverrideLoading;
|
||||||
|
|
||||||
const hasNoLyrics = (!data?.lyrics && !overrideLyrics) || clear;
|
const hasNoLyrics = !data?.lyrics || clear;
|
||||||
|
|
||||||
const lyricsMetadata:
|
const lyricsMetadata:
|
||||||
| Partial<SynchronizedLyricMetadata>
|
| Partial<SynchronizedLyricMetadata>
|
||||||
| Partial<UnsynchronizedLyricMetadata>
|
| Partial<UnsynchronizedLyricMetadata>
|
||||||
| undefined = overrideLyrics
|
| undefined = data;
|
||||||
? {
|
|
||||||
artist: override?.artist,
|
|
||||||
lyrics: overrideLyrics,
|
|
||||||
name: override?.name,
|
|
||||||
remote: true,
|
|
||||||
source: override?.source,
|
|
||||||
}
|
|
||||||
: data;
|
|
||||||
|
|
||||||
const isSynchronizedLyrics = isSynchronized(lyricsMetadata);
|
const isSynchronizedLyrics = isSynchronized(lyricsMetadata);
|
||||||
|
|
||||||
|
@ -198,7 +212,8 @@ export const Lyrics = () => {
|
||||||
)}
|
)}
|
||||||
<ActionsContainer>
|
<ActionsContainer>
|
||||||
<LyricsActions
|
<LyricsActions
|
||||||
onRemoveLyric={() => setClear(true)}
|
onRemoveLyric={handleOnRemoveLyric}
|
||||||
|
onResetLyric={handleOnResetLyric}
|
||||||
onSearchOverride={handleOnSearchOverride}
|
onSearchOverride={handleOnSearchOverride}
|
||||||
/>
|
/>
|
||||||
</ActionsContainer>
|
</ActionsContainer>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { UseQueryResult, useQuery } from '@tanstack/react-query';
|
import { UseQueryResult, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
LyricsQuery,
|
LyricsQuery,
|
||||||
QueueSong,
|
QueueSong,
|
||||||
|
@ -93,6 +93,8 @@ export const useSongLyricsBySong = (
|
||||||
if (!server) throw new Error('Server not found');
|
if (!server) throw new Error('Server not found');
|
||||||
if (!song) return null;
|
if (!song) return null;
|
||||||
|
|
||||||
|
console.log('refetching song lyrics');
|
||||||
|
|
||||||
if (song.lyrics) {
|
if (song.lyrics) {
|
||||||
return {
|
return {
|
||||||
artist: song.artists?.[0]?.name,
|
artist: song.artists?.[0]?.name,
|
||||||
|
@ -145,12 +147,35 @@ export const useSongLyricsBySong = (
|
||||||
export const useSongLyricsByRemoteId = (
|
export const useSongLyricsByRemoteId = (
|
||||||
args: QueryHookArgs<Partial<LyricGetQuery>>,
|
args: QueryHookArgs<Partial<LyricGetQuery>>,
|
||||||
): UseQueryResult<string | null> => {
|
): UseQueryResult<string | null> => {
|
||||||
const { query } = args;
|
const queryClient = useQueryClient();
|
||||||
|
const { query, serverId } = args;
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
cacheTime: 1000 * 60 * 10,
|
|
||||||
enabled: !!query.remoteSongId && !!query.remoteSource,
|
enabled: !!query.remoteSongId && !!query.remoteSource,
|
||||||
onError: () => {},
|
onError: () => {},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (!data || !query.song) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lyricsResult = {
|
||||||
|
artist: query.song.artists?.[0]?.name,
|
||||||
|
lyrics: data,
|
||||||
|
name: query.song.name,
|
||||||
|
remote: false,
|
||||||
|
source: query.remoteSource,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'queryKeys.songs.lyrics(serverId, { songId: query.song.id }) :>> ',
|
||||||
|
queryKeys.songs.lyrics(serverId, { songId: query.song.id }),
|
||||||
|
);
|
||||||
|
|
||||||
|
queryClient.setQueryData(
|
||||||
|
queryKeys.songs.lyrics(serverId, { songId: query.song.id }),
|
||||||
|
lyricsResult,
|
||||||
|
);
|
||||||
|
},
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(
|
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(
|
||||||
query as LyricGetQuery,
|
query as LyricGetQuery,
|
||||||
|
@ -163,6 +188,5 @@ export const useSongLyricsByRemoteId = (
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.songs.lyricsByRemoteId(query),
|
queryKey: queryKeys.songs.lyricsByRemoteId(query),
|
||||||
staleTime: 1000 * 60 * 5,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
Reference in a new issue