diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 34937f60e..b47ad54c5 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -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",
diff --git a/src/renderer/features/player/components/audio-players.tsx b/src/renderer/features/player/components/audio-players.tsx
index 9a3e039c9..d4d667a50 100644
--- a/src/renderer/features/player/components/audio-players.tsx
+++ b/src/renderer/features/player/components/audio-players.tsx
@@ -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 = () => {
+
{
+ const server = useCurrentServer();
+ const currentSong = usePlayerSong();
+ const priorSongId = useRef(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;
+};
diff --git a/src/renderer/features/player/hooks/use-queue-restore.ts b/src/renderer/features/player/hooks/use-queue-restore.ts
index 146018903..bd3b38ab3 100644
--- a/src/renderer/features/player/hooks/use-queue-restore.ts
+++ b/src/renderer/features/player/hooks/use-queue-restore.ts
@@ -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({
diff --git a/src/renderer/features/settings/components/general/application-settings.tsx b/src/renderer/features/settings/components/general/application-settings.tsx
index 5466548e7..5c4e9692c 100644
--- a/src/renderer/features/settings/components/general/application-settings.tsx
+++ b/src/renderer/features/settings/components/general/application-settings.tsx
@@ -688,6 +688,59 @@ export const ApplicationSettings = memo(() => {
isHidden: false,
title: t('setting.playerbarOpenDrawer', { postProcess: 'sentenceCase' }),
},
+ {
+ control: (
+ {
+ 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: (
+ {
+ 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 (
diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts
index ba3246af7..e49342834 100644
--- a/src/renderer/store/settings.store.ts
+++ b/src/renderer/store/settings.store.ts
@@ -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: [],