mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-16 00:14:23 +02:00
add player autodj (#7)
This commit is contained in:
@@ -624,6 +624,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
|
"autoDJ": "auto DJ",
|
||||||
|
"autoDJ_description": "automatically add similar songs to the queue",
|
||||||
|
"autoDJ_itemCount": "item count",
|
||||||
|
"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",
|
||||||
"accentColor_description": "sets the accent color for the application",
|
"accentColor_description": "sets the accent color for the application",
|
||||||
"accentColor": "accent color",
|
"accentColor": "accent color",
|
||||||
"albumBackground_description": "adds a background image for album pages containing the album art",
|
"albumBackground_description": "adds a background image for album pages containing the album art",
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
PlaylistSongListArgs,
|
PlaylistSongListArgs,
|
||||||
PlaylistSongListResponse,
|
PlaylistSongListResponse,
|
||||||
ServerListItemWithCredential,
|
ServerListItemWithCredential,
|
||||||
Song,
|
|
||||||
songListSortMap,
|
songListSortMap,
|
||||||
sortOrderMap,
|
sortOrderMap,
|
||||||
tagListSortMap,
|
tagListSortMap,
|
||||||
@@ -575,42 +574,15 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.body.similarSongs?.song) {
|
if (res.status !== 200) {
|
||||||
const similar = res.body.similarSongs.song.reduce<Song[]>((acc, song) => {
|
|
||||||
if (song.id !== query.songId) {
|
|
||||||
acc.push(ssNormalize.song(song, apiClientProps.server));
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (similar.length > 0) {
|
|
||||||
return similar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallback = await ndApiClient(apiClientProps).getSongList({
|
|
||||||
query: {
|
|
||||||
_end: 50,
|
|
||||||
_order: 'ASC',
|
|
||||||
_sort: NDSongListSort.RANDOM,
|
|
||||||
_start: 0,
|
|
||||||
[getArtistSongKey(apiClientProps.server)]: query.albumArtistIds,
|
|
||||||
...excludeMissing(apiClientProps.server),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fallback.status !== 200) {
|
|
||||||
throw new Error('Failed to get similar songs');
|
throw new Error('Failed to get similar songs');
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallback.body.data.reduce<Song[]>((acc, song) => {
|
return (
|
||||||
if (song.id !== query.songId) {
|
(res.body.similarSongs?.song || [])
|
||||||
acc.push(ndNormalize.song(song, apiClientProps.server));
|
.filter((song) => song.id !== query.songId)
|
||||||
}
|
.map((song) => ssNormalize.song(song, apiClientProps.server)) || []
|
||||||
|
);
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
},
|
},
|
||||||
getSongDetail: async (args) => {
|
getSongDetail: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ interface PlayerEvents {
|
|||||||
|
|
||||||
interface PlayerEventsCallbacks {
|
interface PlayerEventsCallbacks {
|
||||||
onCurrentSongChange?: (
|
onCurrentSongChange?: (
|
||||||
properties: { index: number; song: QueueSong | undefined },
|
properties: { index: number; remaining: number; song: QueueSong | undefined },
|
||||||
prev: { index: number; song: QueueSong | undefined },
|
prev: { index: number; remaining: number; song: QueueSong | undefined },
|
||||||
) => void;
|
) => void;
|
||||||
onPlayerMute?: (properties: { muted: boolean }, prev: { muted: boolean }) => void;
|
onPlayerMute?: (properties: { muted: boolean }, prev: { muted: boolean }) => void;
|
||||||
onPlayerProgress?: (properties: { timestamp: number }, prev: { timestamp: number }) => void;
|
onPlayerProgress?: (properties: { timestamp: number }, prev: { timestamp: number }) => void;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc'
|
|||||||
import { useMainPlayerListener } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener';
|
import { useMainPlayerListener } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener';
|
||||||
import { MpvPlayer } from '/@/renderer/features/player/audio-player/mpv-player';
|
import { MpvPlayer } from '/@/renderer/features/player/audio-player/mpv-player';
|
||||||
import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player';
|
import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player';
|
||||||
|
import { useAutoDJ } from '/@/renderer/features/player/hooks/use-auto-dj';
|
||||||
import { useMediaSession } from '/@/renderer/features/player/hooks/use-media-session';
|
import { useMediaSession } from '/@/renderer/features/player/hooks/use-media-session';
|
||||||
import { useMPRIS } from '/@/renderer/features/player/hooks/use-mpris';
|
import { useMPRIS } from '/@/renderer/features/player/hooks/use-mpris';
|
||||||
import { usePlaybackHotkeys } from '/@/renderer/features/player/hooks/use-playback-hotkeys';
|
import { usePlaybackHotkeys } from '/@/renderer/features/player/hooks/use-playback-hotkeys';
|
||||||
@@ -44,6 +45,7 @@ export const AudioPlayers = () => {
|
|||||||
useMainPlayerListener();
|
useMainPlayerListener();
|
||||||
useMediaSession();
|
useMediaSession();
|
||||||
usePlaybackHotkeys();
|
usePlaybackHotkeys();
|
||||||
|
useAutoDJ();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (webAudio && 'AudioContext' in window) {
|
if (webAudio && 'AudioContext' in window) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-
|
|||||||
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||||
import {
|
import {
|
||||||
useAppStoreActions,
|
useAppStoreActions,
|
||||||
|
useAutoDJSettings,
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
useGeneralSettings,
|
useGeneralSettings,
|
||||||
useHotkeySettings,
|
useHotkeySettings,
|
||||||
@@ -19,9 +20,11 @@ import {
|
|||||||
usePlayerSong,
|
usePlayerSong,
|
||||||
usePlayerVolume,
|
usePlayerVolume,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
|
useSettingsStoreActions,
|
||||||
useSidebarRightExpanded,
|
useSidebarRightExpanded,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Rating } from '/@/shared/components/rating/rating';
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
@@ -58,6 +61,7 @@ export const RightControls = () => {
|
|||||||
<Flex align="flex-end" direction="column" h="100%" px="1rem" py="0.5rem">
|
<Flex align="flex-end" direction="column" h="100%" px="1rem" py="0.5rem">
|
||||||
<Group h="calc(100% / 3)">
|
<Group h="calc(100% / 3)">
|
||||||
<RatingButton />
|
<RatingButton />
|
||||||
|
<AutoDJButton />
|
||||||
</Group>
|
</Group>
|
||||||
<Group align="center" gap="xs" wrap="nowrap">
|
<Group align="center" gap="xs" wrap="nowrap">
|
||||||
<PlayerConfig />
|
<PlayerConfig />
|
||||||
@@ -70,6 +74,33 @@ export const RightControls = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AutoDJButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const settings = useAutoDJSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const toggleAutoDJ = () => {
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
...settings,
|
||||||
|
enabled: !settings.enabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={toggleAutoDJ}
|
||||||
|
size="compact-xs"
|
||||||
|
style={{ color: settings.enabled ? 'var(--theme-colors-primary)' : undefined }}
|
||||||
|
uppercase
|
||||||
|
variant="transparent"
|
||||||
|
>
|
||||||
|
{t('setting.autoDJ')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const QueueButton = () => {
|
const QueueButton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isSidebarRightExpanded = useSidebarRightExpanded();
|
const isSidebarRightExpanded = useSidebarRightExpanded();
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
|
import {
|
||||||
|
isShuffleEnabled,
|
||||||
|
mapShuffledToQueueIndex,
|
||||||
|
useAutoDJSettings,
|
||||||
|
useCurrentServerId,
|
||||||
|
usePlayerStore,
|
||||||
|
usePlayerStoreBase,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
|
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||||
|
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
|
||||||
|
import { Played, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export const useAutoDJ = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const serverId = useCurrentServerId();
|
||||||
|
const player = usePlayer();
|
||||||
|
const settings = useAutoDJSettings();
|
||||||
|
const isFetching = useIsPlayerFetching();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = usePlayerStoreBase.subscribe(
|
||||||
|
(state) => {
|
||||||
|
const queue = state.getQueue();
|
||||||
|
let index = state.player.index;
|
||||||
|
let remaining: number;
|
||||||
|
|
||||||
|
if (isShuffleEnabled(state)) {
|
||||||
|
remaining = state.queue.shuffled.length - index - 1;
|
||||||
|
index = mapShuffledToQueueIndex(index, state.queue.shuffled);
|
||||||
|
} else {
|
||||||
|
remaining = queue.items.slice(index + 1).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { index, remaining, song: queue.items[index] };
|
||||||
|
},
|
||||||
|
async (properties) => {
|
||||||
|
if (!settings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the player is fetching, don't autoplay
|
||||||
|
if (isFetching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no current song, don't autoplay
|
||||||
|
if (!properties.song?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.remaining >= settings.timing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logFn.debug(logMsg[LogCategory.PLAYER].autoPlayTriggered, {
|
||||||
|
category: LogCategory.PLAYER,
|
||||||
|
meta: { remaining: properties.remaining, songId: properties.song?.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, try to fetch similar songs based on the current song
|
||||||
|
const similarSongs = await queryClient.fetchQuery({
|
||||||
|
...songsQueries.similar({
|
||||||
|
query: {
|
||||||
|
count: settings.itemCount,
|
||||||
|
songId: properties.song?.id,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({ similarSongs: properties.song?.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const queue = usePlayerStore.getState().getQueue();
|
||||||
|
|
||||||
|
const queueSongIdSet = new Set(queue.items.map((item) => item.id));
|
||||||
|
const uniqueSimilarSongs = similarSongs.filter(
|
||||||
|
(song) => !queueSongIdSet.has(song.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If not enough songs, try to fetch more similar songs based on the genre of the current song
|
||||||
|
if (uniqueSimilarSongs.length < settings.itemCount) {
|
||||||
|
const genre = properties.song?.genres?.[0];
|
||||||
|
|
||||||
|
if (genre) {
|
||||||
|
const genreSimilarSongs = await queryClient.fetchQuery({
|
||||||
|
...songsQueries.random({
|
||||||
|
query: {
|
||||||
|
genre: genre.id,
|
||||||
|
limit: 50,
|
||||||
|
played: Played.All,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
genre,
|
||||||
|
similarSongs: properties.song?.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueSimilarSongs.push(
|
||||||
|
...genreSimilarSongs.items.filter(
|
||||||
|
(song) => !queueSongIdSet.has(song.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not enough songs, try to fetch more similar songs based on the album artist of the current song
|
||||||
|
if (uniqueSimilarSongs.length < settings.itemCount) {
|
||||||
|
const albumArtist = properties.song?.albumArtists?.[0];
|
||||||
|
|
||||||
|
if (albumArtist) {
|
||||||
|
const albumArtistSimilarSongs = await queryClient.fetchQuery({
|
||||||
|
...songsQueries.list({
|
||||||
|
query: {
|
||||||
|
albumArtistIds: [albumArtist.id],
|
||||||
|
limit: 50,
|
||||||
|
sortBy: SongListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
albumArtist,
|
||||||
|
similarSongs: properties.song?.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueSimilarSongs.push(
|
||||||
|
...albumArtistSimilarSongs.items.filter(
|
||||||
|
(song) => !queueSongIdSet.has(song.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not enough songs, just fetch fully random songs
|
||||||
|
if (uniqueSimilarSongs.length < settings.itemCount) {
|
||||||
|
const randomSongs = await queryClient.fetchQuery({
|
||||||
|
...songsQueries.random({
|
||||||
|
query: { limit: 50, played: Played.All },
|
||||||
|
serverId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueSimilarSongs.push(
|
||||||
|
...randomSongs.items.filter((song) => !queueSongIdSet.has(song.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle the songs and then add to the queue
|
||||||
|
const shuffledSongs = shuffleInPlace(uniqueSimilarSongs);
|
||||||
|
|
||||||
|
// Splice the first itemCount songs and add to the queue
|
||||||
|
const songsToAdd = shuffledSongs.slice(0, settings.itemCount);
|
||||||
|
|
||||||
|
// Add to the end of the queue
|
||||||
|
player.addToQueueByData(songsToAdd, Play.LAST);
|
||||||
|
} catch (error) {
|
||||||
|
logFn.error(logMsg[LogCategory.PLAYER].autoPlayFailed, {
|
||||||
|
category: LogCategory.PLAYER,
|
||||||
|
meta: { error: (error as Error).message, songId: properties.song?.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
equalityFn: (a, b) => {
|
||||||
|
return a.song?._uniqueId === b.song?._uniqueId;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, [
|
||||||
|
isFetching,
|
||||||
|
player,
|
||||||
|
queryClient,
|
||||||
|
serverId,
|
||||||
|
settings.enabled,
|
||||||
|
settings.itemCount,
|
||||||
|
settings.timing,
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SettingOption,
|
||||||
|
SettingsSection,
|
||||||
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
|
import { useAutoDJSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
|
|
||||||
|
export const AutoDJSettings = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const settings = useAutoDJSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const autoDJOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<NumberInput
|
||||||
|
aria-label="Auto DJ item count"
|
||||||
|
hideControls={false}
|
||||||
|
max={50}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
...settings,
|
||||||
|
itemCount: Number(e),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
value={Number(settings.itemCount)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.autoDJ_itemCount', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
title: t('setting.autoDJ_itemCount', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<NumberInput
|
||||||
|
aria-label="Auto DJ timing"
|
||||||
|
hideControls={false}
|
||||||
|
max={5}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
...settings,
|
||||||
|
timing: Number(e),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
value={Number(settings.timing)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.autoDJ_timing', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
title: t('setting.autoDJ_timing', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection
|
||||||
|
options={autoDJOptions}
|
||||||
|
title={t('setting.autoDJ', { postProcess: 'upperCase' })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import isElectron from 'is-electron';
|
|||||||
import { lazy, Suspense, useMemo } from 'react';
|
import { lazy, Suspense, useMemo } from 'react';
|
||||||
|
|
||||||
import { AudioSettings } from '/@/renderer/features/settings/components/playback/audio-settings';
|
import { AudioSettings } from '/@/renderer/features/settings/components/playback/audio-settings';
|
||||||
|
import { AutoDJSettings } from '/@/renderer/features/settings/components/playback/auto-dj-settings';
|
||||||
import { PlayerFilterSettings } from '/@/renderer/features/settings/components/playback/player-filter-settings';
|
import { PlayerFilterSettings } from '/@/renderer/features/settings/components/playback/player-filter-settings';
|
||||||
import { TranscodeSettings } from '/@/renderer/features/settings/components/playback/transcode-settings';
|
import { TranscodeSettings } from '/@/renderer/features/settings/components/playback/transcode-settings';
|
||||||
import { useSettingsStore } from '/@/renderer/store';
|
import { useSettingsStore } from '/@/renderer/store';
|
||||||
@@ -34,6 +35,8 @@ export const PlaybackTab = () => {
|
|||||||
<TranscodeSettings />
|
<TranscodeSettings />
|
||||||
<Divider />
|
<Divider />
|
||||||
<PlayerFilterSettings />
|
<PlayerFilterSettings />
|
||||||
|
<Divider />
|
||||||
|
<AutoDJSettings />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { api } from '/@/renderer/api';
|
|||||||
import { controller } from '/@/renderer/api/controller';
|
import { controller } from '/@/renderer/api/controller';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
import { ListCountQuery, SimilarSongsQuery, SongListQuery } from '/@/shared/types/domain-types';
|
import {
|
||||||
|
ListCountQuery,
|
||||||
|
RandomSongListQuery,
|
||||||
|
SimilarSongsQuery,
|
||||||
|
SongListQuery,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const songsQueries = {
|
export const songsQueries = {
|
||||||
list: (args: QueryHookArgs<SongListQuery>, imageSize?: number) => {
|
list: (args: QueryHookArgs<SongListQuery>, imageSize?: number) => {
|
||||||
@@ -36,13 +41,24 @@ export const songsQueries = {
|
|||||||
...args.options,
|
...args.options,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
random: (args: QueryHookArgs<RandomSongListQuery>) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: ({ signal }) => {
|
||||||
|
return api.controller.getRandomSongList({
|
||||||
|
apiClientProps: { serverId: args.serverId, signal },
|
||||||
|
query: args.query,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.songs.randomSongList(args.serverId, args.query),
|
||||||
|
...args.options,
|
||||||
|
});
|
||||||
|
},
|
||||||
similar: (args: QueryHookArgs<SimilarSongsQuery>) => {
|
similar: (args: QueryHookArgs<SimilarSongsQuery>) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
queryFn: ({ signal }) => {
|
queryFn: ({ signal }) => {
|
||||||
return api.controller.getSimilarSongs({
|
return api.controller.getSimilarSongs({
|
||||||
apiClientProps: { serverId: args.serverId, signal },
|
apiClientProps: { serverId: args.serverId, signal },
|
||||||
query: {
|
query: {
|
||||||
albumArtistIds: args.query.albumArtistIds,
|
|
||||||
count: args.query.count ?? 50,
|
count: args.query.count ?? 50,
|
||||||
songId: args.query.songId,
|
songId: args.query.songId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -130,6 +130,26 @@ export function calculateNextSong(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to check if shuffle is enabled and not in priority mode
|
||||||
|
export function isShuffleEnabled(state: {
|
||||||
|
player: { queueType: PlayerQueueType; shuffle: PlayerShuffle };
|
||||||
|
queue: { shuffled: number[] };
|
||||||
|
}): boolean {
|
||||||
|
return (
|
||||||
|
state.player.shuffle === PlayerShuffle.TRACK &&
|
||||||
|
state.queue.shuffled.length > 0 &&
|
||||||
|
state.player.queueType !== PlayerQueueType.PRIORITY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to map shuffled position to actual queue position
|
||||||
|
export function mapShuffledToQueueIndex(shuffledIndex: number, shuffled: number[]): number {
|
||||||
|
if (shuffledIndex >= 0 && shuffledIndex < shuffled.length) {
|
||||||
|
return shuffled[shuffledIndex];
|
||||||
|
}
|
||||||
|
return shuffledIndex;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to add new indexes to shuffled array after current position
|
// Helper function to add new indexes to shuffled array after current position
|
||||||
function addIndexesToShuffled(
|
function addIndexesToShuffled(
|
||||||
shuffled: number[],
|
shuffled: number[],
|
||||||
@@ -206,26 +226,6 @@ function getCombinedQueueLength(priority: string[], defaultQueue: string[]): num
|
|||||||
return priority.length + defaultQueue.length;
|
return priority.length + defaultQueue.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if shuffle is enabled and not in priority mode
|
|
||||||
function isShuffleEnabled(state: {
|
|
||||||
player: { queueType: PlayerQueueType; shuffle: PlayerShuffle };
|
|
||||||
queue: { shuffled: number[] };
|
|
||||||
}): boolean {
|
|
||||||
return (
|
|
||||||
state.player.shuffle === PlayerShuffle.TRACK &&
|
|
||||||
state.queue.shuffled.length > 0 &&
|
|
||||||
state.player.queueType !== PlayerQueueType.PRIORITY
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to map shuffled position to actual queue position
|
|
||||||
function mapShuffledToQueueIndex(shuffledIndex: number, shuffled: number[]): number {
|
|
||||||
if (shuffledIndex >= 0 && shuffledIndex < shuffled.length) {
|
|
||||||
return shuffled[shuffledIndex];
|
|
||||||
}
|
|
||||||
return shuffledIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to regenerate shuffled indexes if shuffle is enabled
|
// Helper function to regenerate shuffled indexes if shuffle is enabled
|
||||||
function regenerateShuffledIndexesIfNeeded(state: {
|
function regenerateShuffledIndexesIfNeeded(state: {
|
||||||
player: { queueType: PlayerQueueType; shuffle: PlayerShuffle };
|
player: { queueType: PlayerQueueType; shuffle: PlayerShuffle };
|
||||||
|
|||||||
@@ -401,10 +401,17 @@ const QueryBuilderSettingsSchema = z.object({
|
|||||||
tag: z.array(QueryBuilderCustomFieldSchema),
|
tag: z.array(QueryBuilderCustomFieldSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const AutoDJSettingsSchema = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
itemCount: z.number(),
|
||||||
|
timing: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This schema is used for validation of the imported settings json
|
* This schema is used for validation of the imported settings json
|
||||||
*/
|
*/
|
||||||
export const ValidationSettingsStateSchema = z.object({
|
export const ValidationSettingsStateSchema = z.object({
|
||||||
|
autoDJ: AutoDJSettingsSchema,
|
||||||
css: CssSettingsSchema,
|
css: CssSettingsSchema,
|
||||||
discord: DiscordSettingsSchema,
|
discord: DiscordSettingsSchema,
|
||||||
font: FontSettingsSchema,
|
font: FontSettingsSchema,
|
||||||
@@ -659,6 +666,11 @@ const getPlatformDefaultWindowBarStyle = (): Platform => {
|
|||||||
const platformDefaultWindowBarStyle: Platform = getPlatformDefaultWindowBarStyle();
|
const platformDefaultWindowBarStyle: Platform = getPlatformDefaultWindowBarStyle();
|
||||||
|
|
||||||
const initialState: SettingsState = {
|
const initialState: SettingsState = {
|
||||||
|
autoDJ: {
|
||||||
|
enabled: false,
|
||||||
|
itemCount: 10,
|
||||||
|
timing: 3,
|
||||||
|
},
|
||||||
css: {
|
css: {
|
||||||
content: '',
|
content: '',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -1529,3 +1541,5 @@ export const usePrimaryColor = () => useSettingsStore((store) => store.general.a
|
|||||||
export const usePlayerbarSlider = () => useSettingsStore((store) => store.general.playerbarSlider);
|
export const usePlayerbarSlider = () => useSettingsStore((store) => store.general.playerbarSlider);
|
||||||
|
|
||||||
export const useGenreTarget = () => useSettingsStore((store) => store.general.genreTarget);
|
export const useGenreTarget = () => useSettingsStore((store) => store.general.genreTarget);
|
||||||
|
|
||||||
|
export const useAutoDJSettings = () => useSettingsStore((store) => store.autoDJ, shallow);
|
||||||
|
|||||||
@@ -30,3 +30,149 @@ export const idbStateStorage: StateStorage = {
|
|||||||
await set(name, value);
|
await set(name, value);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const settingsKeys = [
|
||||||
|
'store_settings_autoDJ',
|
||||||
|
'store_settings_general',
|
||||||
|
'store_settings_lists',
|
||||||
|
'store_settings_hotkeys',
|
||||||
|
'store_settings_playback',
|
||||||
|
'store_settings_lyrics',
|
||||||
|
'store_settings_window',
|
||||||
|
'store_settings_discord',
|
||||||
|
'store_settings_font',
|
||||||
|
'store_settings_css',
|
||||||
|
'store_settings_remote',
|
||||||
|
'store_settings_queryBuilder',
|
||||||
|
'store_settings_tab',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const splitSettingsStorage: StateStorage = {
|
||||||
|
getItem: (name: string): null | string => {
|
||||||
|
if (name !== 'store_settings') {
|
||||||
|
return localStorage.getItem(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from all split keys and merge them
|
||||||
|
const keys = settingsKeys;
|
||||||
|
|
||||||
|
// Check if old single key exists (for migration)
|
||||||
|
const oldKeyRaw = localStorage.getItem('store_settings');
|
||||||
|
if (oldKeyRaw && !localStorage.getItem('store_settings_general')) {
|
||||||
|
// Only migrate if split keys don't exist yet
|
||||||
|
try {
|
||||||
|
const oldData = JSON.parse(oldKeyRaw);
|
||||||
|
const splitData: Record<string, unknown> = {};
|
||||||
|
const state = oldData.state || oldData;
|
||||||
|
|
||||||
|
if (state && typeof state === 'object') {
|
||||||
|
splitData.general = state.general;
|
||||||
|
splitData.lists = state.lists;
|
||||||
|
splitData.hotkeys = state.hotkeys;
|
||||||
|
splitData.playback = state.playback;
|
||||||
|
splitData.lyrics = state.lyrics;
|
||||||
|
splitData.window = state.window;
|
||||||
|
splitData.discord = state.discord;
|
||||||
|
splitData.font = state.font;
|
||||||
|
splitData.css = state.css;
|
||||||
|
splitData.remote = state.remote;
|
||||||
|
splitData.queryBuilder = state.queryBuilder;
|
||||||
|
splitData.tab = state.tab;
|
||||||
|
|
||||||
|
// Save to new split keys
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const keyName = key.replace('store_settings_', '');
|
||||||
|
if (splitData[keyName] !== undefined) {
|
||||||
|
localStorage.setItem(key, JSON.stringify(splitData[keyName]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store version if it exists
|
||||||
|
if (oldData.version !== undefined) {
|
||||||
|
localStorage.setItem('store_settings_version', oldData.version.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If parsing fails, continue with reading from split keys
|
||||||
|
console.warn('Failed to migrate old settings format:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from all split keys
|
||||||
|
const mergedState: Record<string, unknown> = {};
|
||||||
|
let hasData = false;
|
||||||
|
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
if (value) {
|
||||||
|
try {
|
||||||
|
const keyName = key.replace('store_settings_', '');
|
||||||
|
mergedState[keyName] = JSON.parse(value);
|
||||||
|
hasData = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to parse ${key}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionKey = localStorage.getItem('store_settings_version');
|
||||||
|
const version = versionKey ? parseInt(versionKey, 10) : 14;
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
state: mergedState,
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem: (name: string): void => {
|
||||||
|
if (name !== 'store_settings') {
|
||||||
|
localStorage.removeItem(name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all split keys
|
||||||
|
const keys = settingsKeys;
|
||||||
|
|
||||||
|
keys.forEach((key) => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also remove old key if it exists
|
||||||
|
localStorage.removeItem('store_settings');
|
||||||
|
},
|
||||||
|
|
||||||
|
setItem: (name: string, value: string): void => {
|
||||||
|
if (name !== 'store_settings') {
|
||||||
|
localStorage.setItem(name, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(value);
|
||||||
|
const state = data.state || data;
|
||||||
|
|
||||||
|
const keys = settingsKeys.map((key) => ({
|
||||||
|
key,
|
||||||
|
value: state[key as keyof typeof state],
|
||||||
|
}));
|
||||||
|
|
||||||
|
keys.forEach(({ key, value: keyValue }) => {
|
||||||
|
if (keyValue !== undefined) {
|
||||||
|
localStorage.setItem(key, JSON.stringify(keyValue));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store version separately
|
||||||
|
if (data.version !== undefined) {
|
||||||
|
localStorage.setItem('store_settings_version', data.version.toString());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to split settings storage:', e);
|
||||||
|
localStorage.setItem(name, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export const logMsg = {
|
|||||||
addToQueueByFetch: 'Added to queue by fetch',
|
addToQueueByFetch: 'Added to queue by fetch',
|
||||||
addToQueueByListQuery: 'Added to queue by list query',
|
addToQueueByListQuery: 'Added to queue by list query',
|
||||||
addToQueueByType: 'Added to queue by type',
|
addToQueueByType: 'Added to queue by type',
|
||||||
|
autoPlayFailed: 'Auto play failed',
|
||||||
|
autoPlayTriggered: 'Auto play triggered',
|
||||||
cancelledFetch: 'Cancelled fetch',
|
cancelledFetch: 'Cancelled fetch',
|
||||||
clearQueue: 'Cleared queue',
|
clearQueue: 'Cleared queue',
|
||||||
clearSelected: 'Cleared selected',
|
clearSelected: 'Cleared selected',
|
||||||
|
|||||||
@@ -1449,7 +1449,6 @@ export type SimilarSongsArgs = BaseEndpointArgs & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SimilarSongsQuery = {
|
export type SimilarSongsQuery = {
|
||||||
albumArtistIds: string[];
|
|
||||||
count?: number;
|
count?: number;
|
||||||
songId: string;
|
songId: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user