diff --git a/src/renderer/features/player/components/audio-players.tsx b/src/renderer/features/player/components/audio-players.tsx
index cb5485e83..1ada81279 100644
--- a/src/renderer/features/player/components/audio-players.tsx
+++ b/src/renderer/features/player/components/audio-players.tsx
@@ -14,7 +14,10 @@ import { MediaSessionHook } from '/@/renderer/features/player/hooks/use-media-se
import { MPRISHook } from '/@/renderer/features/player/hooks/use-mpris';
import { PlaybackHotkeysHook } from '/@/renderer/features/player/hooks/use-playback-hotkeys';
import { PowerSaveBlockerHook } from '/@/renderer/features/player/hooks/use-power-save-blocker';
-import { QueueRestoreTimestampHook } from '/@/renderer/features/player/hooks/use-queue-restore';
+import {
+ InitialTimestampRestoreHook,
+ QueueRestoreTimestampHook,
+} from '/@/renderer/features/player/hooks/use-queue-restore';
import { ScrobbleHook } from '/@/renderer/features/player/hooks/use-scrobble';
import { UpdateCurrentSongHook } from '/@/renderer/features/player/hooks/use-update-current-song';
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
@@ -134,6 +137,7 @@ export const AudioPlayers = () => {
+
diff --git a/src/renderer/features/player/hooks/use-queue-restore.ts b/src/renderer/features/player/hooks/use-queue-restore.ts
index 74c280e7a..9b06feebf 100644
--- a/src/renderer/features/player/hooks/use-queue-restore.ts
+++ b/src/renderer/features/player/hooks/use-queue-restore.ts
@@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { t } from 'i18next';
-import { useCallback } from 'react';
+import { useCallback, useEffect, useRef } from 'react';
import { api } from '/@/renderer/api';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
@@ -9,13 +9,18 @@ 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';
export const useQueueRestoreTimestamp = () => {
- const player = usePlayerStore();
+ const { mediaSeekToTimestamp } = usePlayerActions();
usePlayerEvents(
{
@@ -24,7 +29,7 @@ export const useQueueRestoreTimestamp = () => {
setTimeout(() => {
setTimestamp(position);
- player.mediaSeekToTimestamp(position);
+ mediaSeekToTimestamp(position);
}, 100);
},
},
@@ -37,6 +42,72 @@ export const QueueRestoreTimestampHook = () => {
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);
+ const startupSeekAppliedRef = useRef(false);
+
+ const applyStartupSeek = useCallback(() => {
+ if (startupSeekAppliedRef.current) {
+ return;
+ }
+
+ const seekTimestamp = startupSeekArmedRef.current;
+ if (!seekTimestamp || seekTimestamp <= 0) {
+ return;
+ }
+
+ startupSeekAppliedRef.current = true;
+ startupSeekArmedRef.current = null;
+
+ setTimeout(() => {
+ mediaSeekToTimestamp(seekTimestamp);
+ }, 100);
+ }, [mediaSeekToTimestamp]);
+
+ useEffect(() => {
+ if (startupRestoreInitializedRef.current) {
+ return;
+ }
+
+ if (!playerHydrated || !currentSong) {
+ return;
+ }
+
+ startupRestoreInitializedRef.current = true;
+
+ if (timestamp > 0) {
+ startupSeekArmedRef.current = timestamp;
+ }
+
+ 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();
diff --git a/src/renderer/store/timestamp.store.ts b/src/renderer/store/timestamp.store.ts
index 9e6581158..3b729eff0 100644
--- a/src/renderer/store/timestamp.store.ts
+++ b/src/renderer/store/timestamp.store.ts
@@ -1,4 +1,5 @@
-import { subscribeWithSelector } from 'zustand/middleware';
+import { del, get, set } from 'idb-keyval';
+import { persist, subscribeWithSelector } from 'zustand/middleware';
import { createWithEqualityFn } from 'zustand/traditional';
interface TimestampState {
@@ -6,13 +7,36 @@ interface TimestampState {
timestamp: number;
}
+const timestampStorage = {
+ getItem: async (name: string) => {
+ const value = await get(name);
+ if (value === undefined) {
+ return null;
+ }
+ return { state: { timestamp: value }, version: 1 } as const;
+ },
+ removeItem: async (name: string) => {
+ await del(name);
+ },
+ setItem: async (name: string, value: { state: { timestamp: number }; version?: number }) => {
+ await set(name, value.state.timestamp);
+ },
+};
+
export const useTimestampStoreBase = createWithEqualityFn()(
- subscribeWithSelector((set) => ({
- setTimestamp: (timestamp: number) => {
- set({ timestamp });
+ persist(
+ subscribeWithSelector((set) => ({
+ setTimestamp: (timestamp: number) => {
+ set({ timestamp });
+ },
+ timestamp: 0,
+ })),
+ {
+ name: 'player-timestamp',
+ storage: timestampStorage,
+ version: 1,
},
- timestamp: 0,
- })),
+ ),
);
export const subscribePlayerProgress = (