From 8f40894926148fe23b42692963d7677039af18f4 Mon Sep 17 00:00:00 2001
From: Tiago Simionato <57016670+TiagoSimionato@users.noreply.github.com>
Date: Thu, 21 May 2026 04:16:46 -0300
Subject: [PATCH 1/4] feat: persist player timestamp (#2043)
* feat: persist player timestamp
---
.../player/components/audio-players.tsx | 6 +-
.../player/hooks/use-queue-restore.ts | 77 ++++++++++++++++++-
src/renderer/store/timestamp.store.ts | 36 +++++++--
3 files changed, 109 insertions(+), 10 deletions(-)
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 = (
From edfc64d790e083932dafa2921066930fbcc3c677 Mon Sep 17 00:00:00 2001
From: jeffvli
Date: Thu, 21 May 2026 11:17:15 -0700
Subject: [PATCH 2/4] update to electron v41 (#2047)
---
electron-builder-alpha.yml | 4 ++--
electron-builder-beta.yml | 4 ++--
electron-builder.yml | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/electron-builder-alpha.yml b/electron-builder-alpha.yml
index 9d7a54881..63bcc8d67 100644
--- a/electron-builder-alpha.yml
+++ b/electron-builder-alpha.yml
@@ -1,7 +1,7 @@
appId: org.jeffvli.feishin
productName: Feishin
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
-electronVersion: 39.4.0
+electronVersion: 41.7.0
directories:
buildResources: assets
files:
@@ -47,7 +47,7 @@ mac:
gatekeeperAssess: false
notarize: false
extendInfo:
- NSAudioCaptureUsageDescription: "System audio access is required for mpv visualizer capture in Feishin"
+ NSAudioCaptureUsageDescription: 'System audio access is required for mpv visualizer capture in Feishin'
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
dmg:
diff --git a/electron-builder-beta.yml b/electron-builder-beta.yml
index d88f7ca9b..bbd185431 100644
--- a/electron-builder-beta.yml
+++ b/electron-builder-beta.yml
@@ -1,7 +1,7 @@
appId: org.jeffvli.feishin
productName: Feishin
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
-electronVersion: 39.4.0
+electronVersion: 41.7.0
directories:
buildResources: assets
files:
@@ -47,7 +47,7 @@ mac:
gatekeeperAssess: false
notarize: false
extendInfo:
- NSAudioCaptureUsageDescription: "System audio access is required for mpv visualizer capture in Feishin"
+ NSAudioCaptureUsageDescription: 'System audio access is required for mpv visualizer capture in Feishin'
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
dmg:
diff --git a/electron-builder.yml b/electron-builder.yml
index 55050f5fa..c3024c5c7 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -1,7 +1,7 @@
appId: org.jeffvli.feishin
productName: Feishin
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
-electronVersion: 39.4.0
+electronVersion: 41.7.0
directories:
buildResources: assets
files:
From fad2534d4441a4bbac923d4a34466880acf5ca58 Mon Sep 17 00:00:00 2001
From: jeffvli
Date: Thu, 21 May 2026 11:28:13 -0700
Subject: [PATCH 3/4] update to electron v41 in deps
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 3379f5233..10d1af25a 100644
--- a/package.json
+++ b/package.json
@@ -160,7 +160,7 @@
"babel-plugin-react-compiler": "^1.0.0",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
- "electron": "^39.8.6",
+ "electron": "^41.7.0",
"electron-builder": "^26.8.2",
"electron-devtools-installer": "^4.0.0",
"electron-vite": "^4.0.1",
From 0e163543fc32d5099d17edad9b1e59922da41db1 Mon Sep 17 00:00:00 2001
From: jeffvli
Date: Thu, 21 May 2026 11:30:09 -0700
Subject: [PATCH 4/4] update lockfile for electron v41
---
pnpm-lock.yaml | 38 +++++++++++++-------------------------
1 file changed, 13 insertions(+), 25 deletions(-)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6b67d5172..a5415464b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,10 +23,10 @@ importers:
version: 1.1.0
'@electron-toolkit/preload':
specifier: ^3.0.2
- version: 3.0.2(electron@39.8.6)
+ version: 3.0.2(electron@41.7.0)
'@electron-toolkit/utils':
specifier: ^4.0.0
- version: 4.0.0(electron@39.8.6)
+ version: 4.0.0(electron@41.7.0)
'@mantine/colors-generator':
specifier: ^9.1.1
version: 9.1.1(chroma-js@3.1.2)
@@ -269,8 +269,8 @@ importers:
specifier: ^10.1.0
version: 10.1.0
electron:
- specifier: ^39.8.6
- version: 39.8.6
+ specifier: ^41.7.0
+ version: 41.7.0
electron-builder:
specifier: ^26.8.2
version: 26.8.2(electron-builder-squirrel-windows@26.8.2)
@@ -2126,9 +2126,6 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
- '@types/node@22.19.17':
- resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
-
'@types/node@24.12.2':
resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}
@@ -3001,8 +2998,8 @@ packages:
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
engines: {node: '>=8.0.0'}
- electron@39.8.6:
- resolution: {integrity: sha512-uWX6Jh5LmwL13VwOSKBjebI+ck+03GOwc8V2Sgbmr9pJVJ/cHfli/PkjXuRDr+hq+SLHQuT9mGHSIfScebApRA==}
+ electron@41.7.0:
+ resolution: {integrity: sha512-U6KAKivjk6YQ0Z5+eloJBjwhbHRE206gvy1UBMw2bSluWtMh5waeXMvX6AT/Ujm5ymYXVJOp7g9N7vOFw16wBQ==}
engines: {node: '>= 12.20.55'}
hasBin: true
@@ -5507,9 +5504,6 @@ packages:
underscore.string@3.3.6:
resolution: {integrity: sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==}
- undici-types@6.21.0:
- resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
-
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
@@ -6684,17 +6678,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@electron-toolkit/preload@3.0.2(electron@39.8.6)':
+ '@electron-toolkit/preload@3.0.2(electron@41.7.0)':
dependencies:
- electron: 39.8.6
+ electron: 41.7.0
'@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)':
dependencies:
'@types/node': 24.12.2
- '@electron-toolkit/utils@4.0.0(electron@39.8.6)':
+ '@electron-toolkit/utils@4.0.0(electron@41.7.0)':
dependencies:
- electron: 39.8.6
+ electron: 41.7.0
'@electron/asar@3.4.1':
dependencies:
@@ -7675,7 +7669,7 @@ snapshots:
'@types/electron-localshortcut@3.1.3':
dependencies:
- electron: 39.8.6
+ electron: 41.7.0
transitivePeerDependencies:
- supports-color
@@ -7703,10 +7697,6 @@ snapshots:
'@types/ms@2.1.0': {}
- '@types/node@22.19.17':
- dependencies:
- undici-types: 6.21.0
-
'@types/node@24.12.2':
dependencies:
undici-types: 7.16.0
@@ -8800,10 +8790,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
- electron@39.8.6:
+ electron@41.7.0:
dependencies:
'@electron/get': 2.0.3
- '@types/node': 22.19.17
+ '@types/node': 24.12.2
extract-zip: 2.0.1
transitivePeerDependencies:
- supports-color
@@ -11588,8 +11578,6 @@ snapshots:
sprintf-js: 1.1.3
util-deprecate: 1.0.2
- undici-types@6.21.0: {}
-
undici-types@7.16.0: {}
undici@6.24.1: {}