feat(player): add server-side autosave capability

This commit is contained in:
Kendall Garner
2026-03-06 20:01:35 -08:00
parent e603048a80
commit 7c4cbaad9a
6 changed files with 104 additions and 2 deletions
+4
View File
@@ -721,6 +721,10 @@
"autoDJ_itemCount_description": "the number of items attempted to be added to the queue when auto DJ is enabled",
"autoDJ_timing": "timing",
"autoDJ_timing_description": "the number of songs remaining in the queue before auto DJ is triggered",
"autosave": "automatically save play queue",
"autosave_description": "enable automatically saving the play queue to your server. this is only possible when using Navidrome/Subsonic, and you cannot have a mixed play queue.",
"autosaveCount": "automatic play queue save frequency",
"autosaveCount_description": "how many track changes before the queue is saved. 1 (minimum) means every song change",
"accentColor_description": "sets the accent color for the application",
"accentColor": "accent color",
"useThemeAccentColor": "use theme accent color",
@@ -9,6 +9,7 @@ import { MpvPlayer } from '/@/renderer/features/player/audio-player/mpv-player';
import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player';
import { SleepTimerHook } from '/@/renderer/features/player/components/sleep-timer-button';
import { AutoDJHook } from '/@/renderer/features/player/hooks/use-auto-dj';
import { AutosaveHook } from '/@/renderer/features/player/hooks/use-autosave';
import { MediaSessionHook } from '/@/renderer/features/player/hooks/use-media-session';
import { MPRISHook } from '/@/renderer/features/player/hooks/use-mpris';
import { PlaybackHotkeysHook } from '/@/renderer/features/player/hooks/use-playback-hotkeys';
@@ -64,6 +65,7 @@ export const AudioPlayers = () => {
<UpdateCurrentSongHook />
<RadioAudioInstanceHook />
<RadioMetadataHook />
<AutosaveHook />
<AudioPlayersContent
audioContext={audioContext}
audioDeviceId={audioDeviceId}
@@ -0,0 +1,34 @@
import { useEffect, useRef } from 'react';
import { useSaveQueue } from '/@/renderer/features/player/hooks/use-queue-restore';
import { useCurrentServer, usePlayerSong, useSettingsStore } from '/@/renderer/store';
import { ServerType } from '/@/shared/types/domain-types';
export const useAutosave = () => {
const server = useCurrentServer();
const currentSong = usePlayerSong();
const priorSongId = useRef<string | undefined>(undefined);
const songCount = useRef(0);
const { count, enabled } = useSettingsStore((state) => state.general.autoSave);
const { mutate: savePlayQueue } = useSaveQueue();
useEffect(() => {
if (enabled && server.type !== ServerType.JELLYFIN) {
if (currentSong?._uniqueId !== priorSongId.current) {
if (songCount.current === count) {
savePlayQueue();
songCount.current = 1;
} else {
songCount.current += 1;
}
priorSongId.current = currentSong?._uniqueId;
}
}
}, [enabled, count, currentSong?._uniqueId, savePlayQueue, server.type]);
};
export const AutosaveHook = () => {
useAutosave();
return null;
};
@@ -89,8 +89,7 @@ export const useSaveQueue = () => {
});
toast.success({
message: '',
title: t('form.saveQueue.success', { postProcess: 'sentenceCase' }),
message: t('form.saveQueue.success', { postProcess: 'sentenceCase' }),
});
} catch (error) {
toast.error({
@@ -688,6 +688,59 @@ export const ApplicationSettings = memo(() => {
isHidden: false,
title: t('setting.playerbarOpenDrawer', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label={t('setting.autosave', { postProcess: 'sentenceCase' })}
defaultChecked={settings.autoSave.enabled}
onChange={(e) => {
setSettings({
general: {
...settings,
autoSave: {
...settings.autoSave,
enabled: e.currentTarget.checked,
},
},
});
}}
/>
),
description: t('setting.autosave', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.autosave', { postProcess: 'sentenceCase' }),
},
{
control: (
<NumberInput
min={1}
onBlur={(e) => {
if (!e) return;
const newVal = e.currentTarget.value
? Math.max(Number(e.currentTarget.value), 1)
: settings.autoSave.count;
setSettings({
general: {
...settings,
autoSave: {
...settings.autoSave,
count: newVal,
},
},
});
}}
value={settings.autoSave.count}
/>
),
description: t('setting.autosaveCount', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !settings.autoSave.enabled,
title: t('setting.autosaveCount', { postProcess: 'sentenceCase' }),
},
];
return (
+10
View File
@@ -430,6 +430,11 @@ export enum HomeFeatureStyle {
SINGLE = 'single',
}
const AutoSaveSchema = z.object({
count: z.number().min(0),
enabled: z.boolean(),
});
export const GeneralSettingsSchema = z.object({
accent: z
.string()
@@ -446,6 +451,7 @@ export const GeneralSettingsSchema = z.object({
artistItems: z.array(SortableItemSchema(ArtistItemSchema)),
artistRadioCount: z.number(),
artistReleaseTypeItems: z.array(SortableItemSchema(ArtistReleaseTypeItemSchema)),
autoSave: AutoSaveSchema,
blurExplicitImages: z.boolean(),
buttonSize: z.number(),
collections: z.array(CollectionSchema),
@@ -1094,6 +1100,10 @@ const initialState: SettingsState = {
artistItems,
artistRadioCount: 20,
artistReleaseTypeItems,
autoSave: {
count: 10,
enabled: false,
},
blurExplicitImages: false,
buttonSize: 15,
collections: [],