mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-08 11:40:03 +02:00
2ad4c58dcd
applyStartupSeek() compared the armed target song against getQueue().items[player.index], but player.index is a position in the shuffled order while getQueue().items is always the default order. With shuffle enabled the lookup resolves to the wrong song, the uniqueId check fails, and the startup resume-seek is silently cancelled — the track then plays from 0:00 and the progress poller overwrites the persisted timestamp. Use getCurrentSong() instead, which maps the index through queue.shuffled, matching how the seek target is armed. Fixes #2202 Co-authored-by: Maurits <WhoCarrot@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
221 lines
6.5 KiB
TypeScript
221 lines
6.5 KiB
TypeScript
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { t } from 'i18next';
|
|
import { useCallback, useEffect, useRef } from 'react';
|
|
|
|
import { api } from '/@/renderer/api';
|
|
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
|
import {
|
|
setTimestamp,
|
|
useCurrentServerId,
|
|
usePlayerActions,
|
|
usePlayerHydrated,
|
|
usePlayerSong,
|
|
usePlayerStatus,
|
|
usePlayerStore,
|
|
useTimestampStoreBase,
|
|
} from '/@/renderer/store';
|
|
import { toast } from '/@/shared/components/toast/toast';
|
|
import { PlayerStatus } from '/@/shared/types/types';
|
|
|
|
let startupRestoreSessionHandled = false;
|
|
|
|
export const useQueueRestoreTimestamp = () => {
|
|
const { mediaSeekToTimestamp } = usePlayerActions();
|
|
|
|
usePlayerEvents(
|
|
{
|
|
onQueueRestored: (properties) => {
|
|
const { position } = properties;
|
|
|
|
setTimeout(() => {
|
|
setTimestamp(position);
|
|
mediaSeekToTimestamp(position);
|
|
}, 100);
|
|
},
|
|
},
|
|
[],
|
|
);
|
|
};
|
|
|
|
export const QueueRestoreTimestampHook = () => {
|
|
useQueueRestoreTimestamp();
|
|
return null;
|
|
};
|
|
|
|
export const useInitialTimestampRestore = () => {
|
|
const { mediaSeekToTimestamp } = usePlayerActions();
|
|
const playerHydrated = usePlayerHydrated();
|
|
const currentSong = usePlayerSong();
|
|
const playerStatus = usePlayerStatus();
|
|
const timestamp = useTimestampStoreBase((state) => state.timestamp);
|
|
|
|
const startupRestoreInitializedRef = useRef(false);
|
|
const startupSeekArmedRef = useRef<null | number>(null);
|
|
const startupSeekTargetUniqueIdRef = useRef<null | string>(null);
|
|
const startupSeekAppliedRef = useRef(false);
|
|
|
|
const cancelStartupSeek = useCallback(() => {
|
|
if (startupSeekAppliedRef.current) {
|
|
return;
|
|
}
|
|
|
|
startupSeekAppliedRef.current = true;
|
|
startupSeekArmedRef.current = null;
|
|
startupSeekTargetUniqueIdRef.current = null;
|
|
}, []);
|
|
|
|
const applyStartupSeek = useCallback(() => {
|
|
const seekTimestamp = startupSeekArmedRef.current;
|
|
|
|
if (startupSeekAppliedRef.current) {
|
|
return;
|
|
}
|
|
|
|
if (!seekTimestamp || seekTimestamp <= 0) {
|
|
return;
|
|
}
|
|
|
|
const targetUniqueId = startupSeekTargetUniqueIdRef.current;
|
|
const currentUniqueId = usePlayerStore.getState().getCurrentSong()?._uniqueId;
|
|
|
|
if (targetUniqueId && currentUniqueId !== targetUniqueId) {
|
|
cancelStartupSeek();
|
|
return;
|
|
}
|
|
|
|
startupSeekAppliedRef.current = true;
|
|
startupSeekArmedRef.current = null;
|
|
startupSeekTargetUniqueIdRef.current = null;
|
|
|
|
setTimeout(() => {
|
|
mediaSeekToTimestamp(seekTimestamp);
|
|
}, 100);
|
|
}, [cancelStartupSeek, mediaSeekToTimestamp]);
|
|
|
|
useEffect(() => {
|
|
const targetUniqueId = startupSeekTargetUniqueIdRef.current;
|
|
if (
|
|
!targetUniqueId ||
|
|
startupSeekAppliedRef.current ||
|
|
!currentSong ||
|
|
currentSong._uniqueId === targetUniqueId
|
|
) {
|
|
return;
|
|
}
|
|
|
|
cancelStartupSeek();
|
|
}, [cancelStartupSeek, currentSong]);
|
|
|
|
useEffect(() => {
|
|
if (startupRestoreInitializedRef.current || startupRestoreSessionHandled) {
|
|
return;
|
|
}
|
|
|
|
if (!playerHydrated || !currentSong) {
|
|
return;
|
|
}
|
|
|
|
startupRestoreInitializedRef.current = true;
|
|
startupRestoreSessionHandled = true;
|
|
|
|
if (timestamp > 0) {
|
|
startupSeekArmedRef.current = timestamp;
|
|
startupSeekTargetUniqueIdRef.current = currentSong._uniqueId;
|
|
}
|
|
|
|
if (playerStatus === PlayerStatus.PLAYING) {
|
|
applyStartupSeek();
|
|
}
|
|
}, [applyStartupSeek, currentSong, playerHydrated, playerStatus, timestamp]);
|
|
|
|
usePlayerEvents(
|
|
{
|
|
onPlayerStatus: (properties) => {
|
|
if (properties.status === PlayerStatus.PLAYING) {
|
|
applyStartupSeek();
|
|
}
|
|
},
|
|
},
|
|
[applyStartupSeek],
|
|
);
|
|
};
|
|
|
|
export const InitialTimestampRestoreHook = () => {
|
|
useInitialTimestampRestore();
|
|
return null;
|
|
};
|
|
|
|
export const useSaveQueue = () => {
|
|
const serverId = useCurrentServerId();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: async () => {
|
|
if (!serverId) {
|
|
throw new Error(t('error.serverRequired'));
|
|
}
|
|
|
|
const state = usePlayerStore.getState();
|
|
const queue = state.getQueue();
|
|
|
|
if (queue.items.some((item) => item._serverId !== serverId)) {
|
|
toast.error({
|
|
message: t('error.multipleServerSaveQueueError'),
|
|
title: t('error.genericError'),
|
|
});
|
|
|
|
throw new Error(`${t('error.multipleServerSaveQueueError')}`);
|
|
}
|
|
|
|
return api.controller.savePlayQueue({
|
|
apiClientProps: { serverId },
|
|
query: {
|
|
currentIndex: queue.items.length > 0 ? state.player.index : undefined,
|
|
positionMs: useTimestampStoreBase.getState().timestamp * 1000,
|
|
songs: queue.items.map((item) => item.id),
|
|
},
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
toast.error({
|
|
message: (error as Error).message,
|
|
title: t('error.saveQueueFailed'),
|
|
});
|
|
},
|
|
});
|
|
|
|
return mutation;
|
|
};
|
|
|
|
export const useRestoreQueue = () => {
|
|
const serverId = useCurrentServerId();
|
|
const player = usePlayer();
|
|
const queryClient = useQueryClient();
|
|
|
|
const handleRestoreQueue = useCallback(async () => {
|
|
if (!serverId) return;
|
|
|
|
try {
|
|
const queue = await queryClient.fetchQuery(
|
|
songsQueries.getQueue({ query: {}, serverId }),
|
|
);
|
|
|
|
if (queue) {
|
|
player.setQueue(
|
|
queue.entry,
|
|
queue.currentIndex,
|
|
queue.positionMs !== undefined ? queue.positionMs / 1000 : undefined,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
toast.error({
|
|
message: (error as Error).message,
|
|
title: t('error.genericError'),
|
|
});
|
|
}
|
|
}, [player, queryClient, serverId]);
|
|
|
|
return handleRestoreQueue;
|
|
};
|