persist lyrics offset per song

This commit is contained in:
jeffvli
2025-11-30 17:55:12 -08:00
parent b99bc62065
commit 0b8ae55150
4 changed files with 82 additions and 25 deletions
+10 -17
View File
@@ -2,12 +2,7 @@ import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import { openLyricSearchModal } from '/@/renderer/features/lyrics/components/lyrics-search-form';
import {
useLyricsSettings,
usePlayerSong,
useSettingsStore,
useSettingsStoreActions,
} from '/@/renderer/store';
import { useLyricsSettings, 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';
@@ -20,9 +15,11 @@ import { LyricsOverride } from '/@/shared/types/domain-types';
interface LyricsActionsProps {
index: number;
languages: { label: string; value: string }[];
offsetMs: number;
onRemoveLyric: () => void;
onSearchOverride: (params: LyricsOverride) => void;
onTranslateLyric?: () => void;
onUpdateOffset: (offsetMs: number) => void;
setIndex: (idx: number) => void;
synced?: boolean;
}
@@ -30,23 +27,19 @@ interface LyricsActionsProps {
export const LyricsActions = ({
index,
languages,
offsetMs,
onRemoveLyric,
onSearchOverride,
onTranslateLyric,
onUpdateOffset,
setIndex,
}: LyricsActionsProps) => {
const { t } = useTranslation();
const currentSong = usePlayerSong();
const { setSettings } = useSettingsStoreActions();
const { delayMs, sources } = useLyricsSettings();
const { sources } = useLyricsSettings();
const handleLyricOffset = (e: number | string) => {
setSettings({
lyrics: {
...useSettingsStore.getState().lyrics,
delayMs: Number(e),
},
});
onUpdateOffset(Number(e));
};
const isActionsDisabled = !currentSong;
@@ -86,7 +79,7 @@ export const LyricsActions = ({
<ActionIcon
aria-label="Decrease lyric offset"
icon="minus"
onClick={() => handleLyricOffset(delayMs - 50)}
onClick={() => handleLyricOffset(offsetMs - 50)}
tooltip={{
label: t('common.slower', { postProcess: 'sentenceCase' }),
openDelay: 0,
@@ -101,14 +94,14 @@ export const LyricsActions = ({
aria-label="Lyric offset"
onChange={handleLyricOffset}
styles={{ input: { textAlign: 'center' } }}
value={delayMs || 0}
value={offsetMs || 0}
width={70}
/>
</Tooltip>
<ActionIcon
aria-label="Increase lyric offset"
icon="plus"
onClick={() => handleLyricOffset(delayMs + 50)}
onClick={() => handleLyricOffset(offsetMs + 50)}
tooltip={{
label: t('common.faster', { postProcess: 'sentenceCase' }),
openDelay: 0,
+62 -4
View File
@@ -25,7 +25,12 @@ import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text';
import { FullLyricsMetadata, LyricSource, LyricsOverride } from '/@/shared/types/domain-types';
import {
FullLyricsMetadata,
LyricSource,
LyricsOverride,
StructuredLyric,
} from '/@/shared/types/domain-types';
export const Lyrics = () => {
const currentSong = usePlayerSong();
@@ -66,6 +71,19 @@ export const Lyrics = () => {
}),
);
// Get the current song's offset from persisted lyrics, default to 0
const currentOffsetMs = useMemo(() => {
if (Array.isArray(data)) {
if (data.length > 0) {
const selectedLyric = data[Math.min(index, data.length - 1)];
return selectedLyric.offsetMs ?? 0;
}
} else if (data?.offsetMs !== undefined) {
return data.offsetMs;
}
return 0;
}, [data, index]);
const [lyrics, synced] = useMemo(() => {
// If override data is available, use it
if (override && overrideData) {
@@ -73,6 +91,7 @@ export const Lyrics = () => {
artist: override.artist,
lyrics: overrideData,
name: override.name,
offsetMs: currentOffsetMs,
remote: override.remote ?? true,
source: override.source,
};
@@ -90,19 +109,20 @@ export const Lyrics = () => {
}
return [undefined, false];
}, [data, index, override, overrideData]);
}, [data, index, override, overrideData, currentOffsetMs]);
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
setOverride(params);
}, []);
// Persist override lyrics to cache
// Persist override lyrics to cache with current offset
useEffect(() => {
if (override && overrideData && currentSong) {
const persistedLyrics: FullLyricsMetadata = {
artist: override.artist,
lyrics: overrideData,
name: override.name,
offsetMs: currentOffsetMs,
remote: override.remote ?? true,
source: override.source,
};
@@ -112,7 +132,42 @@ export const Lyrics = () => {
persistedLyrics,
);
}
}, [override, overrideData, currentSong]);
}, [override, overrideData, currentSong, currentOffsetMs]);
// Callback to update the song's persisted offset
const handleUpdateOffset = useCallback(
(offsetMs: number) => {
if (!currentSong) return;
queryClient.setQueryData(
queryKeys.songs.lyrics(currentSong._serverId, { songId: currentSong.id }),
(prev: FullLyricsMetadata | null | StructuredLyric[] | undefined) => {
if (!prev) return prev;
// Handle array of structured lyrics
if (Array.isArray(prev)) {
if (prev.length > 0) {
const selectedIndex = Math.min(index, prev.length - 1);
const updated = [...prev];
updated[selectedIndex] = {
...updated[selectedIndex],
offsetMs,
};
return updated;
}
return prev;
}
// Handle single lyrics object
return {
...prev,
offsetMs,
};
},
);
},
[currentSong, index],
);
// const handleOnResetLyric = useCallback(() => {
// setOverride(undefined);
@@ -242,6 +297,7 @@ export const Lyrics = () => {
{synced ? (
<SynchronizedLyrics
{...(lyrics as SynchronizedLyricsProps)}
offsetMs={currentOffsetMs}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
) : (
@@ -258,6 +314,7 @@ export const Lyrics = () => {
<LyricsActions
index={index}
languages={languages}
offsetMs={currentOffsetMs}
onRemoveLyric={handleOnRemoveLyric}
onSearchOverride={handleOnSearchOverride}
onTranslateLyric={
@@ -265,6 +322,7 @@ export const Lyrics = () => {
? handleOnTranslateLyric
: undefined
}
onUpdateOffset={handleUpdateOffset}
setIndex={setIndex}
/>
</div>
@@ -21,6 +21,7 @@ const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null;
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: SynchronizedLyricsArray;
offsetMs?: number;
style?: React.CSSProperties;
translatedLyrics?: null | string;
}
@@ -29,6 +30,7 @@ export const SynchronizedLyrics = ({
artist,
lyrics,
name,
offsetMs,
remote,
source,
style,
@@ -40,6 +42,8 @@ export const SynchronizedLyrics = ({
const status = usePlayerStatus();
const timestamp = usePlayerTimestamp();
const effectiveOffsetMs = offsetMs ?? 0;
const handleSeek = useCallback(
(time: number) => {
if (playbackType === PlayerType.LOCAL && mpvPlayer) {
@@ -66,7 +70,7 @@ export const SynchronizedLyrics = ({
// whether to proceed or stop
const timerEpoch = useRef(0);
const delayMsRef = useRef(settings.delayMs);
const delayMsRef = useRef(effectiveOffsetMs);
const followRef = useRef(settings.follow);
const userScrollingRef = useRef(false);
const scrollTimeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null);
@@ -195,7 +199,8 @@ export const SynchronizedLyrics = ({
// This handler is used to deal with changes to the current delay. If the offset
// changes, we should immediately stop the current listening set and calculate
// the correct one using the new offset. Afterwards, timing can be calculated like normal
const changed = delayMsRef.current !== settings.delayMs;
const newOffset = offsetMs ?? 0;
const changed = delayMsRef.current !== newOffset;
if (!changed) {
return;
@@ -205,11 +210,11 @@ export const SynchronizedLyrics = ({
clearTimeout(lyricTimer.current);
}
delayMsRef.current = settings.delayMs;
delayMsRef.current = newOffset;
// Use the current timestamp from player events
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
}, [setCurrentLyric, settings.delayMs, timestamp]);
}, [setCurrentLyric, offsetMs, timestamp]);
useEffect(() => {
// This handler is used specifically for dealing with seeking and progress updates.
+1
View File
@@ -1060,6 +1060,7 @@ export type ArtistInfoQuery = {
export type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'> & {
lyrics: LyricsResponse;
offsetMs?: number;
remote: boolean;
source: string;
};