mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-19 01:44:00 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db79d1a71e |
@@ -114,8 +114,11 @@ These variables override app settings **on first run** when no persisted setting
|
|||||||
|
|
||||||
| Setting path | Default | Env variable | Available values / Description |
|
| Setting path | Default | Env variable | Available values / Description |
|
||||||
|-------------|---------|--------------|--------------------------------|
|
|-------------|---------|--------------|--------------------------------|
|
||||||
|
| `autoDJ.albumStrategy` | `similar` | `FS_AUTO_DJ_ALBUM_STRATEGY` | `similar` / `library_random`. |
|
||||||
| `autoDJ.enabled` | `false` | `FS_AUTO_DJ_ENABLED` | `true` / `false`. |
|
| `autoDJ.enabled` | `false` | `FS_AUTO_DJ_ENABLED` | `true` / `false`. |
|
||||||
| `autoDJ.itemCount` | `5` | `FS_AUTO_DJ_ITEM_COUNT` | Number of items to add. |
|
| `autoDJ.itemCount` | `5` | `FS_AUTO_DJ_ITEM_COUNT` | Number of items to add. |
|
||||||
|
| `autoDJ.mode` | `songs` | `FS_AUTO_DJ_MODE` | `songs` / `albums`. |
|
||||||
|
| `autoDJ.songStrategy` | `similar` | `FS_AUTO_DJ_SONG_STRATEGY` | `similar` / `library_random`. |
|
||||||
| `autoDJ.timing` | `1` | `FS_AUTO_DJ_TIMING` | Timing value (number). |
|
| `autoDJ.timing` | `1` | `FS_AUTO_DJ_TIMING` | Timing value (number). |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -88,8 +88,11 @@ window.FS_LYRICS_TRANSLATION_API_KEY = "${FS_LYRICS_TRANSLATION_API_KEY}";
|
|||||||
window.FS_LYRICS_TRANSLATION_TARGET_LANGUAGE = "${FS_LYRICS_TRANSLATION_TARGET_LANGUAGE}";
|
window.FS_LYRICS_TRANSLATION_TARGET_LANGUAGE = "${FS_LYRICS_TRANSLATION_TARGET_LANGUAGE}";
|
||||||
window.FS_LYRICS_ALIGNMENT = "${FS_LYRICS_ALIGNMENT}";
|
window.FS_LYRICS_ALIGNMENT = "${FS_LYRICS_ALIGNMENT}";
|
||||||
|
|
||||||
|
window.FS_AUTO_DJ_ALBUM_STRATEGY = "${FS_AUTO_DJ_ALBUM_STRATEGY}";
|
||||||
window.FS_AUTO_DJ_ENABLED = "${FS_AUTO_DJ_ENABLED}";
|
window.FS_AUTO_DJ_ENABLED = "${FS_AUTO_DJ_ENABLED}";
|
||||||
window.FS_AUTO_DJ_ITEM_COUNT = "${FS_AUTO_DJ_ITEM_COUNT}";
|
window.FS_AUTO_DJ_ITEM_COUNT = "${FS_AUTO_DJ_ITEM_COUNT}";
|
||||||
|
window.FS_AUTO_DJ_MODE = "${FS_AUTO_DJ_MODE}";
|
||||||
|
window.FS_AUTO_DJ_SONG_STRATEGY = "${FS_AUTO_DJ_SONG_STRATEGY}";
|
||||||
window.FS_AUTO_DJ_TIMING = "${FS_AUTO_DJ_TIMING}";
|
window.FS_AUTO_DJ_TIMING = "${FS_AUTO_DJ_TIMING}";
|
||||||
|
|
||||||
window.FS_CSS_CONTENT = "${FS_CSS_CONTENT}";
|
window.FS_CSS_CONTENT = "${FS_CSS_CONTENT}";
|
||||||
|
|||||||
@@ -415,6 +415,11 @@
|
|||||||
},
|
},
|
||||||
"shuffleAll": {
|
"shuffleAll": {
|
||||||
"title": "Play random",
|
"title": "Play random",
|
||||||
|
"input_kind_albums": "Albums",
|
||||||
|
"input_kind_songs": "Songs",
|
||||||
|
"input_kind": "Random picks",
|
||||||
|
"input_limit_albums": "How many albums?",
|
||||||
|
"input_limit_songs": "How many songs?",
|
||||||
"input_genre": "$t(entity.genre, {\"count\": 1})",
|
"input_genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
"input_limit": "How many songs?",
|
"input_limit": "How many songs?",
|
||||||
"input_minYear": "From year",
|
"input_minYear": "From year",
|
||||||
@@ -731,11 +736,19 @@
|
|||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"autoDJ": "Auto DJ",
|
"autoDJ": "Auto DJ",
|
||||||
"autoDJ_description": "Automatically add similar songs to the queue",
|
|
||||||
"autoDJ_itemCount": "Item count",
|
"autoDJ_itemCount": "Item count",
|
||||||
"autoDJ_itemCount_description": "The number of items attempted to be added to the queue when auto DJ is enabled",
|
"autoDJ_itemCount_description": "The number of items attempted to be added to the queue",
|
||||||
"autoDJ_timing": "Timing",
|
"autoDJ_timing": "Timing",
|
||||||
"autoDJ_timing_description": "The number of songs remaining in the queue before auto DJ is triggered",
|
"autoDJ_timing_description": "The number of songs remaining in the queue before auto DJ is triggered",
|
||||||
|
"autoDJ_mode": "Mode",
|
||||||
|
"autoDJ_mode_albums": "Albums",
|
||||||
|
"autoDJ_mode_description": "Choose to add either songs or entire albums to the queue",
|
||||||
|
"autoDJ_mode_songs": "Songs",
|
||||||
|
"autoDJ_enabled": "Enable Auto DJ",
|
||||||
|
"autoDJ_albumStrategy": "Album selection mode",
|
||||||
|
"autoDJ_songStrategy": "Song selection mode",
|
||||||
|
"autoDJ_strategy_option_library_random": "Random",
|
||||||
|
"autoDJ_strategy_option_similar": "Similar",
|
||||||
"autosave": "Automatically save play queue",
|
"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.",
|
"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": "Automatic play queue save frequency",
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import type { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { autoDjGenreIdsForSongGenre, autoDjPushUniqueAlbumIds } from './auto-dj-utils';
|
||||||
|
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
|
import { AUTO_DJ_STRATEGY, type AutoDJStrategy } from '/@/renderer/store/settings.store';
|
||||||
|
import { shuffle } from '/@/renderer/utils/shuffle';
|
||||||
|
import {
|
||||||
|
AlbumListSort,
|
||||||
|
type QueueSong,
|
||||||
|
type ServerListItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export type AutoDjAlbumCollectArgs = {
|
||||||
|
albumStrategy: AutoDJStrategy;
|
||||||
|
currentSong: QueueSong;
|
||||||
|
itemCount: number;
|
||||||
|
musicFolderId: string | string[] | undefined;
|
||||||
|
queryClient: QueryClient;
|
||||||
|
queueAlbumIdSet: Set<string>;
|
||||||
|
server: null | ServerListItem | undefined;
|
||||||
|
serverId: string;
|
||||||
|
trySimilarSongs: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runAutoDjAlbumIds = async (args: AutoDjAlbumCollectArgs): Promise<string[]> => {
|
||||||
|
switch (args.albumStrategy) {
|
||||||
|
case AUTO_DJ_STRATEGY.LIBRARY_RANDOM: {
|
||||||
|
return collectAlbumsLibraryRandom(args);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return collectAlbumsSimilar(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectAlbumsLibraryRandom = async (args: AutoDjAlbumCollectArgs): Promise<string[]> => {
|
||||||
|
const page = await args.queryClient.fetchQuery({
|
||||||
|
...albumQueries.list({
|
||||||
|
query: {
|
||||||
|
limit: Math.max(args.itemCount, 1),
|
||||||
|
musicFolderId: args.musicFolderId,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({ autoDjAlbumLibraryRandom: args.currentSong?.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ids = page.items.map((a) => a.id).filter((id) => id && !args.queueAlbumIdSet.has(id));
|
||||||
|
return shuffle(ids).slice(0, args.itemCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectAlbumsSimilar = async (args: AutoDjAlbumCollectArgs): Promise<string[]> => {
|
||||||
|
const targetAlbumCount = args.itemCount;
|
||||||
|
const candidateAlbumIds: string[] = [];
|
||||||
|
const seenAlbumCandidates = new Set<string>();
|
||||||
|
|
||||||
|
if (args.trySimilarSongs && args.currentSong?.id) {
|
||||||
|
const similarSongsFromSimilarApi = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.similar({
|
||||||
|
query: {
|
||||||
|
count: args.itemCount * 4,
|
||||||
|
songId: args.currentSong.id,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
similarSongAlbumDj: args.currentSong.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
autoDjPushUniqueAlbumIds(
|
||||||
|
candidateAlbumIds,
|
||||||
|
seenAlbumCandidates,
|
||||||
|
args.queueAlbumIdSet,
|
||||||
|
...similarSongsFromSimilarApi.map((s) => s.albumId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateAlbumIds.length < targetAlbumCount && args.currentSong && args.server) {
|
||||||
|
const genre = args.currentSong.genres?.[0];
|
||||||
|
if (genre) {
|
||||||
|
const genreIds = autoDjGenreIdsForSongGenre(genre, args.server.type);
|
||||||
|
|
||||||
|
const genreAlbums = await args.queryClient.fetchQuery({
|
||||||
|
...albumQueries.list({
|
||||||
|
query: {
|
||||||
|
genreIds,
|
||||||
|
limit: 50,
|
||||||
|
musicFolderId: args.musicFolderId,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
genreAlbumDj: genreIds,
|
||||||
|
song: args.currentSong.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
autoDjPushUniqueAlbumIds(
|
||||||
|
candidateAlbumIds,
|
||||||
|
seenAlbumCandidates,
|
||||||
|
args.queueAlbumIdSet,
|
||||||
|
...genreAlbums.items.map((album) => album.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!args.trySimilarSongs) {
|
||||||
|
const randomAlbumMixCount = Math.max(1, Math.ceil(50 * 0.2));
|
||||||
|
const randomAlbumsMix = await args.queryClient.fetchQuery({
|
||||||
|
...albumQueries.list({
|
||||||
|
query: {
|
||||||
|
limit: randomAlbumMixCount,
|
||||||
|
musicFolderId: args.musicFolderId,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
genreAlbumDjMixRandom: args.currentSong.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
autoDjPushUniqueAlbumIds(
|
||||||
|
candidateAlbumIds,
|
||||||
|
seenAlbumCandidates,
|
||||||
|
args.queueAlbumIdSet,
|
||||||
|
...randomAlbumsMix.items.map((album) => album.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateAlbumIds.length < targetAlbumCount && args.currentSong) {
|
||||||
|
const albumArtist = args.currentSong.albumArtists?.[0];
|
||||||
|
|
||||||
|
if (albumArtist) {
|
||||||
|
const albumsByArtist = await args.queryClient.fetchQuery({
|
||||||
|
...albumQueries.list({
|
||||||
|
query: {
|
||||||
|
artistIds: [albumArtist.id],
|
||||||
|
limit: 50,
|
||||||
|
musicFolderId: args.musicFolderId,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
artistAlbumDj: albumArtist.id,
|
||||||
|
song: args.currentSong.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
autoDjPushUniqueAlbumIds(
|
||||||
|
candidateAlbumIds,
|
||||||
|
seenAlbumCandidates,
|
||||||
|
args.queueAlbumIdSet,
|
||||||
|
...albumsByArtist.items.map((album) => album.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateAlbumIds.length < targetAlbumCount && args.currentSong) {
|
||||||
|
const randomAlbumsFallback = await args.queryClient.fetchQuery({
|
||||||
|
...albumQueries.list({
|
||||||
|
query: {
|
||||||
|
limit: 80,
|
||||||
|
musicFolderId: args.musicFolderId,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
fallbackAlbumDj: args.currentSong.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
autoDjPushUniqueAlbumIds(
|
||||||
|
candidateAlbumIds,
|
||||||
|
seenAlbumCandidates,
|
||||||
|
args.queueAlbumIdSet,
|
||||||
|
...randomAlbumsFallback.items.map((album) => album.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shuffledAlbums = shuffle(candidateAlbumIds);
|
||||||
|
return shuffledAlbums.slice(0, targetAlbumCount);
|
||||||
|
};
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import type { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
|
import { AUTO_DJ_STRATEGY, type AutoDJStrategy } from '/@/renderer/store/settings.store';
|
||||||
|
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
|
||||||
|
import {
|
||||||
|
Played,
|
||||||
|
type QueueSong,
|
||||||
|
type ServerListItem,
|
||||||
|
Song,
|
||||||
|
SongListSort,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export type AutoDjSongCollectArgs = {
|
||||||
|
currentSong: QueueSong;
|
||||||
|
itemCount: number;
|
||||||
|
musicFolderId: string | string[] | undefined;
|
||||||
|
queryClient: QueryClient;
|
||||||
|
queueSongIdSet: Set<string>;
|
||||||
|
server: null | ServerListItem | undefined;
|
||||||
|
serverId: string;
|
||||||
|
songStrategy: AutoDJStrategy;
|
||||||
|
trySimilarSongs: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runAutoDjSongs = async (args: AutoDjSongCollectArgs): Promise<Song[]> => {
|
||||||
|
switch (args.songStrategy) {
|
||||||
|
case AUTO_DJ_STRATEGY.LIBRARY_RANDOM: {
|
||||||
|
return collectSongsLibraryRandom(args);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return collectSongsSimilar(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectSongsLibraryRandom = async (args: AutoDjSongCollectArgs): Promise<Song[]> => {
|
||||||
|
const randomSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.random({
|
||||||
|
query: {
|
||||||
|
limit: Math.max(args.itemCount * 3, 50),
|
||||||
|
played: Played.All,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({ autoDjLibraryRandomSongs: args.currentSong.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pool = randomSongs.items.filter((song) => !args.queueSongIdSet.has(song.id));
|
||||||
|
const shuffled = shuffleInPlace(pool);
|
||||||
|
return shuffled.slice(0, args.itemCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectSongsSimilar = async (args: AutoDjSongCollectArgs): Promise<Song[]> => {
|
||||||
|
let uniqueSimilarSongs: Song[] = [];
|
||||||
|
|
||||||
|
if (args.trySimilarSongs) {
|
||||||
|
const similarSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.similar({
|
||||||
|
query: {
|
||||||
|
count: args.itemCount,
|
||||||
|
songId: args.currentSong?.id,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({ similarSongs: args.currentSong?.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueSimilarSongs = similarSongs.filter((song) => !args.queueSongIdSet.has(song.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueSimilarSongs.length < args.itemCount) {
|
||||||
|
const genre = args.currentSong?.genres?.[0];
|
||||||
|
|
||||||
|
if (genre) {
|
||||||
|
const genreLimit = 50;
|
||||||
|
const genreSimilarSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.random({
|
||||||
|
query: {
|
||||||
|
genre: genre.id,
|
||||||
|
limit: genreLimit,
|
||||||
|
played: Played.All,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
genre,
|
||||||
|
similarSongs: args.currentSong?.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const genreSongs = genreSimilarSongs.items.filter(
|
||||||
|
(song) => !args.queueSongIdSet.has(song.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!args.trySimilarSongs) {
|
||||||
|
const randomSongCount = Math.max(1, Math.ceil(genreLimit * 0.2));
|
||||||
|
|
||||||
|
const randomSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.random({
|
||||||
|
query: { limit: randomSongCount, played: Played.All },
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniqueRandomSongs = randomSongs.items.filter(
|
||||||
|
(song) => !args.queueSongIdSet.has(song.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const randomSongsToAdd = uniqueRandomSongs.slice(0, randomSongCount);
|
||||||
|
uniqueSimilarSongs.push(...randomSongsToAdd, ...genreSongs);
|
||||||
|
} else {
|
||||||
|
uniqueSimilarSongs.push(...genreSongs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueSimilarSongs.length < args.itemCount) {
|
||||||
|
const albumArtist = args.currentSong?.albumArtists?.[0];
|
||||||
|
|
||||||
|
if (albumArtist) {
|
||||||
|
const albumArtistSimilarSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.list({
|
||||||
|
query: {
|
||||||
|
albumArtistIds: [albumArtist.id],
|
||||||
|
limit: 50,
|
||||||
|
sortBy: SongListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
albumArtist,
|
||||||
|
similarSongs: args.currentSong?.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueSimilarSongs.push(
|
||||||
|
...albumArtistSimilarSongs.items.filter(
|
||||||
|
(song) => !args.queueSongIdSet.has(song.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueSimilarSongs.length < args.itemCount) {
|
||||||
|
const randomSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.random({
|
||||||
|
query: { limit: 50, played: Played.All },
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueSimilarSongs.push(
|
||||||
|
...randomSongs.items.filter((song) => !args.queueSongIdSet.has(song.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shuffledSongs = shuffleInPlace(uniqueSimilarSongs);
|
||||||
|
return shuffledSongs.slice(0, args.itemCount);
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Genre } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
import { ServerType } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const autoDjPushUniqueAlbumIds = (
|
||||||
|
accumulator: string[],
|
||||||
|
seenAlbums: Set<string>,
|
||||||
|
queueAlbumIdSet: Set<string>,
|
||||||
|
...ids: (string | undefined)[]
|
||||||
|
) => {
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!id || queueAlbumIdSet.has(id) || seenAlbums.has(id)) continue;
|
||||||
|
seenAlbums.add(id);
|
||||||
|
accumulator.push(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const autoDjGenreIdsForSongGenre = (genre: Genre, serverType: ServerType): string[] => {
|
||||||
|
if (serverType === ServerType.JELLYFIN) {
|
||||||
|
return [genre.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverType === ServerType.NAVIDROME || serverType === ServerType.SUBSONIC) {
|
||||||
|
return [genre.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [genre.id];
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { useCallback, useEffect, useState, WheelEvent } from 'react';
|
import { useCallback, useEffect, useMemo, useState, WheelEvent } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PopoverPlayQueue } from '/@/renderer/features/now-playing/components/popover-play-queue';
|
import { PopoverPlayQueue } from '/@/renderer/features/now-playing/components/popover-play-queue';
|
||||||
@@ -12,6 +12,9 @@ import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-
|
|||||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||||
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import {
|
import {
|
||||||
|
AUTO_DJ_MODE,
|
||||||
|
AUTO_DJ_STRATEGY,
|
||||||
|
type AutoDJStrategy,
|
||||||
useAppStoreActions,
|
useAppStoreActions,
|
||||||
useAutoDJSettings,
|
useAutoDJSettings,
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
@@ -34,7 +37,15 @@ import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
|||||||
import { Button } from '/@/shared/components/button/button';
|
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 { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
|
import { Paper } from '/@/shared/components/paper/paper';
|
||||||
|
import { Popover } from '/@/shared/components/popover/popover';
|
||||||
import { Rating } from '/@/shared/components/rating/rating';
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
|
import { Select } from '/@/shared/components/select/select';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { useMediaQuery } from '/@/shared/hooks/use-media-query';
|
import { useMediaQuery } from '/@/shared/hooks/use-media-query';
|
||||||
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
||||||
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
|
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
|
||||||
@@ -90,28 +101,148 @@ const AutoDJButton = () => {
|
|||||||
const settings = useAutoDJSettings();
|
const settings = useAutoDJSettings();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
const toggleAutoDJ = () => {
|
const itemLabels = useMemo(() => {
|
||||||
setSettings({
|
return {
|
||||||
autoDJ: {
|
description: t('setting.autoDJ_itemCount_description'),
|
||||||
...settings,
|
title: t('setting.autoDJ_itemCount'),
|
||||||
enabled: !settings.enabled,
|
};
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const strategySelectData = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: t('setting.autoDJ_strategy_option_similar'),
|
||||||
|
value: AUTO_DJ_STRATEGY.SIMILAR,
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
};
|
label: t('setting.autoDJ_strategy_option_library_random'),
|
||||||
|
value: AUTO_DJ_STRATEGY.LIBRARY_RANDOM,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const strategyLabels =
|
||||||
|
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||||
|
? {
|
||||||
|
description: '',
|
||||||
|
title: t('setting.autoDJ_albumStrategy'),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
description: '',
|
||||||
|
title: t('setting.autoDJ_songStrategy'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const strategyValue =
|
||||||
|
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||||
|
? (settings.albumStrategy ?? AUTO_DJ_STRATEGY.SIMILAR)
|
||||||
|
: (settings.songStrategy ?? AUTO_DJ_STRATEGY.SIMILAR);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Popover position="top-end" withArrow>
|
||||||
onClick={(e) => {
|
<Popover.Target>
|
||||||
e.stopPropagation();
|
<Button
|
||||||
toggleAutoDJ();
|
onClick={(e) => {
|
||||||
}}
|
e.stopPropagation();
|
||||||
size="compact-xs"
|
}}
|
||||||
style={{ color: settings.enabled ? 'var(--theme-colors-primary)' : undefined }}
|
size="compact-xs"
|
||||||
uppercase
|
style={{ color: settings.enabled ? 'var(--theme-colors-primary)' : undefined }}
|
||||||
variant="transparent"
|
uppercase
|
||||||
>
|
variant="transparent"
|
||||||
{t('setting.autoDJ')}
|
>
|
||||||
</Button>
|
{t('setting.autoDJ')}
|
||||||
|
</Button>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown maw={320} miw={260} onClick={(e) => e.stopPropagation()} p="sm">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Paper p="md" radius="md">
|
||||||
|
<Group align="center" gap="xs" justify="space-between" wrap="nowrap">
|
||||||
|
<Text fw={600} isNoSelect size="sm">
|
||||||
|
{t('setting.autoDJ_enabled')}
|
||||||
|
</Text>
|
||||||
|
<Switch
|
||||||
|
checked={settings.enabled}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
autoDJ: { enabled: e.currentTarget.checked },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{ label: t('setting.autoDJ_mode_songs'), value: AUTO_DJ_MODE.SONGS },
|
||||||
|
{
|
||||||
|
label: t('setting.autoDJ_mode_albums'),
|
||||||
|
value: AUTO_DJ_MODE.ALBUMS,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
mode: value as 'albums' | 'songs',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={settings.mode}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
|
data={strategySelectData}
|
||||||
|
description={strategyLabels.description}
|
||||||
|
label={strategyLabels.title}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!value) return;
|
||||||
|
setSettings({
|
||||||
|
autoDJ:
|
||||||
|
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||||
|
? { albumStrategy: value as AutoDJStrategy }
|
||||||
|
: { songStrategy: value as AutoDJStrategy },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
value={strategyValue}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
aria-label={itemLabels.title}
|
||||||
|
description={itemLabels.description}
|
||||||
|
hideControls={false}
|
||||||
|
label={itemLabels.title}
|
||||||
|
max={50}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
itemCount: Number(e),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size="md"
|
||||||
|
value={Number(settings.itemCount)}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
aria-label={t('setting.autoDJ_timing')}
|
||||||
|
description={t('setting.autoDJ_timing_description')}
|
||||||
|
hideControls={false}
|
||||||
|
label={t('setting.autoDJ_timing')}
|
||||||
|
max={5}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
timing: Number(e),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size="md"
|
||||||
|
value={Number(settings.timing)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
|||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
|
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group';
|
import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group';
|
||||||
@@ -18,9 +19,18 @@ import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
|||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
import { Select } from '/@/shared/components/select/select';
|
import { Select } from '/@/shared/components/select/select';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Played, RandomSongListQuery, ServerType } from '/@/shared/types/domain-types';
|
import {
|
||||||
|
AlbumListQuery,
|
||||||
|
AlbumListSort,
|
||||||
|
LibraryItem,
|
||||||
|
Played,
|
||||||
|
RandomSongListQuery,
|
||||||
|
ServerType,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface ShuffleAllSlice extends RandomSongListQuery {
|
interface ShuffleAllSlice extends RandomSongListQuery {
|
||||||
@@ -29,6 +39,7 @@ interface ShuffleAllSlice extends RandomSongListQuery {
|
|||||||
};
|
};
|
||||||
enableMaxYear: boolean;
|
enableMaxYear: boolean;
|
||||||
enableMinYear: boolean;
|
enableMinYear: boolean;
|
||||||
|
playbackKind: 'albums' | 'songs';
|
||||||
}
|
}
|
||||||
|
|
||||||
const useShuffleAllStore = createWithEqualityFn<ShuffleAllSlice>()(
|
const useShuffleAllStore = createWithEqualityFn<ShuffleAllSlice>()(
|
||||||
@@ -42,16 +53,28 @@ const useShuffleAllStore = createWithEqualityFn<ShuffleAllSlice>()(
|
|||||||
enableMaxYear: false,
|
enableMaxYear: false,
|
||||||
enableMinYear: false,
|
enableMinYear: false,
|
||||||
genre: '',
|
genre: '',
|
||||||
|
limit: 100,
|
||||||
maxYear: 2020,
|
maxYear: 2020,
|
||||||
minYear: 2000,
|
minYear: 2000,
|
||||||
musicFolder: '',
|
musicFolder: '',
|
||||||
|
playbackKind: 'songs',
|
||||||
played: Played.All,
|
played: Played.All,
|
||||||
songCount: 100,
|
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
merge: (persistedState, currentState) => merge(currentState, persistedState),
|
merge: (persistedState, currentState) => merge(currentState, persistedState),
|
||||||
|
migrate: (persisted, version: number) => {
|
||||||
|
if (!persisted) {
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version >= 2) {
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return persisted;
|
||||||
|
},
|
||||||
name: 'store_shuffle_all',
|
name: 'store_shuffle_all',
|
||||||
version: 1,
|
version: 2,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -66,13 +89,24 @@ export const useShuffleAllStoreActions = () => useShuffleAllStore((state) => sta
|
|||||||
|
|
||||||
export const ShuffleAllContextModal = () => {
|
export const ShuffleAllContextModal = () => {
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { addToQueueByData } = usePlayer();
|
const { addToQueueByData, addToQueueByFetch } = usePlayer();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { enableMaxYear, enableMinYear, genre, limit, maxYear, minYear, musicFolderId, played } =
|
const {
|
||||||
useShuffleAllStore();
|
enableMaxYear,
|
||||||
|
enableMinYear,
|
||||||
|
genre,
|
||||||
|
limit,
|
||||||
|
maxYear,
|
||||||
|
minYear,
|
||||||
|
musicFolderId,
|
||||||
|
playbackKind,
|
||||||
|
played,
|
||||||
|
} = useShuffleAllStore();
|
||||||
const { setStore } = useShuffleAllStoreActions();
|
const { setStore } = useShuffleAllStoreActions();
|
||||||
|
|
||||||
const { isFetching, refetch } = useQuery({
|
const clampedLimit = Math.min(500, Math.max(1, limit || 100));
|
||||||
|
|
||||||
|
const { isFetching: isFetchingSongs, refetch: refetchSongs } = useQuery({
|
||||||
...randomFetchQuery({
|
...randomFetchQuery({
|
||||||
query: {
|
query: {
|
||||||
genre: genre || undefined,
|
genre: genre || undefined,
|
||||||
@@ -89,22 +123,75 @@ export const ShuffleAllContextModal = () => {
|
|||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isFetching: isFetchingAlbums, refetch: refetchAlbums } = useQuery({
|
||||||
|
...shuffleAlbumListQuery({
|
||||||
|
query: {
|
||||||
|
genreIds: genre ? [genre] : undefined,
|
||||||
|
limit: clampedLimit,
|
||||||
|
minYear: enableMinYear ? minYear || undefined : undefined,
|
||||||
|
musicFolderId: musicFolderId || undefined,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: server.id,
|
||||||
|
}),
|
||||||
|
enabled: false,
|
||||||
|
gcTime: 0,
|
||||||
|
staleTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const fetchTypeRef = useRef<Play>(null);
|
const fetchTypeRef = useRef<Play>(null);
|
||||||
|
|
||||||
const handlePlay = async (playType: Play) => {
|
const handlePlay = async (playType: Play) => {
|
||||||
fetchTypeRef.current = playType;
|
fetchTypeRef.current = playType;
|
||||||
|
|
||||||
const { data } = await refetch();
|
if (playbackKind === 'albums') {
|
||||||
|
const { data } = await refetchAlbums();
|
||||||
|
|
||||||
addToQueueByData(data?.items || [], playType);
|
addToQueueByFetch(
|
||||||
|
server.id,
|
||||||
|
data?.items.map((a) => a.id) ?? [],
|
||||||
|
LibraryItem.ALBUM,
|
||||||
|
playType,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { data } = await refetchSongs();
|
||||||
|
|
||||||
|
addToQueueByData(data?.items || [], playType);
|
||||||
|
}
|
||||||
|
|
||||||
closeAllModals();
|
closeAllModals();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: t('form.shuffleAll.input_kind_songs'),
|
||||||
|
value: 'songs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('form.shuffleAll.input_kind_albums'),
|
||||||
|
value: 'albums',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
setStore({
|
||||||
|
playbackKind: value as 'albums' | 'songs',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
value={playbackKind}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label={t('form.shuffleAll.input_limit')}
|
label={
|
||||||
|
playbackKind === 'albums'
|
||||||
|
? t('form.shuffleAll.input_limit_albums')
|
||||||
|
: t('form.shuffleAll.input_limit_songs')
|
||||||
|
}
|
||||||
max={500}
|
max={500}
|
||||||
min={1}
|
min={1}
|
||||||
onChange={(e) => setStore({ limit: e ? Number(e) : 500 })}
|
onChange={(e) => setStore({ limit: e ? Number(e) : 500 })}
|
||||||
@@ -127,6 +214,7 @@ export const ShuffleAllContextModal = () => {
|
|||||||
value={minYear}
|
value={minYear}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
disabled={playbackKind === 'albums'}
|
||||||
label={t('form.shuffleAll.input_maxYear')}
|
label={t('form.shuffleAll.input_maxYear')}
|
||||||
max={2050}
|
max={2050}
|
||||||
min={1850}
|
min={1850}
|
||||||
@@ -134,6 +222,7 @@ export const ShuffleAllContextModal = () => {
|
|||||||
rightSection={
|
rightSection={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={enableMaxYear}
|
checked={enableMaxYear}
|
||||||
|
disabled={playbackKind === 'albums'}
|
||||||
onChange={(e) => setStore({ enableMaxYear: e.currentTarget.checked })}
|
onChange={(e) => setStore({ enableMaxYear: e.currentTarget.checked })}
|
||||||
style={{ marginRight: '0.5rem' }}
|
style={{ marginRight: '0.5rem' }}
|
||||||
/>
|
/>
|
||||||
@@ -144,7 +233,7 @@ export const ShuffleAllContextModal = () => {
|
|||||||
<Suspense fallback={<Select data={[]} />}>
|
<Suspense fallback={<Select data={[]} />}>
|
||||||
<GenreSelect />
|
<GenreSelect />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{server?.type === ServerType.JELLYFIN && (
|
{server?.type === ServerType.JELLYFIN && playbackKind === 'songs' && (
|
||||||
<Select
|
<Select
|
||||||
clearable
|
clearable
|
||||||
data={PLAYED_DATA}
|
data={PLAYED_DATA}
|
||||||
@@ -156,10 +245,7 @@ export const ShuffleAllContextModal = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
<PlayButtonGroup
|
<PlayButtonGroup loading={isFetchingSongs || isFetchingAlbums} onPlay={handlePlay} />
|
||||||
loading={(isFetching && fetchTypeRef.current) || false}
|
|
||||||
onPlay={handlePlay}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -186,6 +272,13 @@ const randomFetchQuery = (args: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shuffleAlbumListQuery = (args: { query: AlbumListQuery; serverId: string }) => {
|
||||||
|
return albumQueries.list({
|
||||||
|
query: args.query,
|
||||||
|
serverId: args.serverId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const openShuffleAllModal = async () => {
|
export const openShuffleAllModal = async () => {
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: {},
|
innerProps: {},
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
|
import { runAutoDjAlbumIds } from '/@/renderer/features/player/auto-dj/auto-dj-albums';
|
||||||
|
import { runAutoDjSongs } from '/@/renderer/features/player/auto-dj/auto-dj-songs';
|
||||||
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
|
||||||
import {
|
import {
|
||||||
|
AUTO_DJ_STRATEGY,
|
||||||
isShuffleEnabled,
|
isShuffleEnabled,
|
||||||
mapShuffledToQueueIndex,
|
mapShuffledToQueueIndex,
|
||||||
useAutoDJSettings,
|
useAutoDJSettings,
|
||||||
@@ -17,9 +18,8 @@ import {
|
|||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||||
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
|
|
||||||
import { hasFeature } from '/@/shared/api/utils';
|
import { hasFeature } from '/@/shared/api/utils';
|
||||||
import { Played, Song, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { ServerFeature } from '/@/shared/types/features-types';
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -34,6 +34,9 @@ export const useAutoDJ = () => {
|
|||||||
const hasSimilarSongsMusicFolder = hasFeature(server, ServerFeature.SIMILAR_SONGS_MUSIC_FOLDER);
|
const hasSimilarSongsMusicFolder = hasFeature(server, ServerFeature.SIMILAR_SONGS_MUSIC_FOLDER);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const albumStrategy = settings.albumStrategy ?? AUTO_DJ_STRATEGY.SIMILAR;
|
||||||
|
const songStrategy = settings.songStrategy ?? AUTO_DJ_STRATEGY.SIMILAR;
|
||||||
|
|
||||||
const unsubscribe = usePlayerStoreBase.subscribe(
|
const unsubscribe = usePlayerStoreBase.subscribe(
|
||||||
(state) => {
|
(state) => {
|
||||||
const queue = state.getQueue();
|
const queue = state.getQueue();
|
||||||
@@ -54,7 +57,6 @@ export const useAutoDJ = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no current song, don't autoplay
|
|
||||||
if (!properties.song?.id) {
|
if (!properties.song?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -70,142 +72,76 @@ export const useAutoDJ = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const queue = usePlayerStore.getState().getQueue();
|
const queue = usePlayerStore.getState().getQueue();
|
||||||
const queueSongIdSet = new Set(queue.items.map((item) => item.id));
|
|
||||||
let uniqueSimilarSongs: Song[] = [];
|
|
||||||
|
|
||||||
const hasMusicFolder = server?.musicFolderId && server.musicFolderId.length > 0;
|
const hasMusicFolder = server?.musicFolderId && server.musicFolderId.length > 0;
|
||||||
|
const musicFolderId =
|
||||||
|
hasMusicFolder && server?.musicFolderId ? server.musicFolderId : undefined;
|
||||||
const trySimilarSongs =
|
const trySimilarSongs =
|
||||||
!hasMusicFolder || (hasMusicFolder && hasSimilarSongsMusicFolder);
|
!hasMusicFolder || (hasMusicFolder && hasSimilarSongsMusicFolder);
|
||||||
|
|
||||||
// Skip similar songs fetch if a music folder is selected and does not support musicFolderId on similar songs
|
const runnerDepsBase = {
|
||||||
if (trySimilarSongs) {
|
itemCount: settings.itemCount,
|
||||||
// First, try to fetch similar songs based on the current song
|
musicFolderId,
|
||||||
const similarSongs = await queryClient.fetchQuery({
|
queryClient,
|
||||||
...songsQueries.similar({
|
server,
|
||||||
query: {
|
serverId,
|
||||||
count: settings.itemCount,
|
trySimilarSongs,
|
||||||
songId: properties.song?.id,
|
};
|
||||||
},
|
|
||||||
serverId,
|
if (settings.mode === 'albums') {
|
||||||
}),
|
if (!serverId) {
|
||||||
queryKey: queryKeys.player.fetch({ similarSongs: properties.song?.id }),
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueAlbumIdSet = new Set(
|
||||||
|
queue.items
|
||||||
|
.map((item) => item.albumId)
|
||||||
|
.filter((id): id is string => Boolean(id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const albumsToAdd = await runAutoDjAlbumIds({
|
||||||
|
...runnerDepsBase,
|
||||||
|
albumStrategy,
|
||||||
|
currentSong: properties.song,
|
||||||
|
queueAlbumIdSet,
|
||||||
});
|
});
|
||||||
|
|
||||||
uniqueSimilarSongs = similarSongs.filter(
|
if (albumsToAdd.length > 0) {
|
||||||
(song) => !queueSongIdSet.has(song.id),
|
await player.addToQueueByFetch(
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 genreLimit = 50;
|
|
||||||
const genreSimilarSongs = await queryClient.fetchQuery({
|
|
||||||
...songsQueries.random({
|
|
||||||
query: {
|
|
||||||
genre: genre.id,
|
|
||||||
limit: genreLimit,
|
|
||||||
played: Played.All,
|
|
||||||
},
|
|
||||||
serverId,
|
|
||||||
}),
|
|
||||||
queryKey: queryKeys.player.fetch({
|
|
||||||
genre,
|
|
||||||
similarSongs: properties.song?.id,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const genreSongs = genreSimilarSongs.items.filter(
|
|
||||||
(song) => !queueSongIdSet.has(song.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If trySimilarSongs is false, add variation by mixing in random songs
|
|
||||||
if (!trySimilarSongs) {
|
|
||||||
// Calculate how many random songs we need: 20% or at least 1
|
|
||||||
const randomSongCount = Math.max(1, Math.ceil(genreLimit * 0.2));
|
|
||||||
|
|
||||||
const randomSongs = await queryClient.fetchQuery({
|
|
||||||
...songsQueries.random({
|
|
||||||
query: { limit: randomSongCount, played: Played.All },
|
|
||||||
serverId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const uniqueRandomSongs = randomSongs.items.filter(
|
|
||||||
(song) => !queueSongIdSet.has(song.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add minimum required random songs for variation
|
|
||||||
const randomSongsToAdd = uniqueRandomSongs.slice(
|
|
||||||
0,
|
|
||||||
randomSongCount,
|
|
||||||
);
|
|
||||||
uniqueSimilarSongs.push(...randomSongsToAdd, ...genreSongs);
|
|
||||||
} else {
|
|
||||||
uniqueSimilarSongs.push(...genreSongs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
serverId,
|
||||||
}),
|
albumsToAdd,
|
||||||
});
|
LibraryItem.ALBUM,
|
||||||
|
Play.LAST,
|
||||||
|
);
|
||||||
|
|
||||||
uniqueSimilarSongs.push(
|
eventEmitter.emit('AUTODJ_QUEUE_ADDED', {
|
||||||
...randomSongs.items.filter((song) => !queueSongIdSet.has(song.id)),
|
songCount: albumsToAdd.length,
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shuffle the songs and then add to the queue
|
if (!serverId) {
|
||||||
const shuffledSongs = shuffleInPlace(uniqueSimilarSongs);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Splice the first itemCount songs and add to the queue
|
const queueSongIdSet = new Set(queue.items.map((item) => item.id));
|
||||||
const songsToAdd = shuffledSongs.slice(0, settings.itemCount);
|
|
||||||
|
|
||||||
// Add to the end of the queue
|
const songsToAdd = await runAutoDjSongs({
|
||||||
player.addToQueueByData(songsToAdd, Play.LAST);
|
...runnerDepsBase,
|
||||||
|
currentSong: properties.song,
|
||||||
// Emit event to trigger queue follow
|
queueSongIdSet,
|
||||||
eventEmitter.emit('AUTODJ_QUEUE_ADDED', {
|
songStrategy,
|
||||||
songCount: songsToAdd.length,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (songsToAdd.length > 0) {
|
||||||
|
player.addToQueueByData(songsToAdd, Play.LAST);
|
||||||
|
|
||||||
|
eventEmitter.emit('AUTODJ_QUEUE_ADDED', {
|
||||||
|
songCount: songsToAdd.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logFn.error(logMsg[LogCategory.PLAYER].autoPlayFailed, {
|
logFn.error(logMsg[LogCategory.PLAYER].autoPlayFailed, {
|
||||||
category: LogCategory.PLAYER,
|
category: LogCategory.PLAYER,
|
||||||
@@ -229,7 +165,10 @@ export const useAutoDJ = () => {
|
|||||||
server,
|
server,
|
||||||
serverId,
|
serverId,
|
||||||
settings.enabled,
|
settings.enabled,
|
||||||
|
settings.albumStrategy,
|
||||||
settings.itemCount,
|
settings.itemCount,
|
||||||
|
settings.mode,
|
||||||
|
settings.songStrategy,
|
||||||
settings.timing,
|
settings.timing,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,112 @@
|
|||||||
import { memo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SettingOption,
|
SettingOption,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from '/@/renderer/features/settings/components/settings-section';
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
import { useAutoDJSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import {
|
||||||
|
AUTO_DJ_MODE,
|
||||||
|
AUTO_DJ_STRATEGY,
|
||||||
|
type AutoDJStrategy,
|
||||||
|
useAutoDJSettings,
|
||||||
|
useSettingsStoreActions,
|
||||||
|
} from '/@/renderer/store/settings.store';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
|
import { Select } from '/@/shared/components/select/select';
|
||||||
|
|
||||||
export const AutoDJSettings = memo(() => {
|
export const AutoDJSettings = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const settings = useAutoDJSettings();
|
const settings = useAutoDJSettings();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const itemLabels = useMemo(() => {
|
||||||
|
return {
|
||||||
|
description: t('setting.autoDJ_itemCount_description'),
|
||||||
|
title: t('setting.autoDJ_itemCount'),
|
||||||
|
};
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const strategySelectData = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: t('setting.autoDJ_strategy_option_similar'),
|
||||||
|
value: AUTO_DJ_STRATEGY.SIMILAR,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('setting.autoDJ_strategy_option_library_random'),
|
||||||
|
value: AUTO_DJ_STRATEGY.LIBRARY_RANDOM,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
const autoDJOptions: SettingOption[] = [
|
const autoDJOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{ label: t('setting.autoDJ_mode_songs'), value: AUTO_DJ_MODE.SONGS },
|
||||||
|
{ label: t('setting.autoDJ_mode_albums'), value: AUTO_DJ_MODE.ALBUMS },
|
||||||
|
]}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
mode: value as 'albums' | 'songs',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
value={settings.mode}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.autoDJ_mode_description'),
|
||||||
|
title: t('setting.autoDJ_mode'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={strategySelectData}
|
||||||
|
onChange={(value) =>
|
||||||
|
value &&
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
songStrategy: value as AutoDJStrategy,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={settings.songStrategy ?? AUTO_DJ_STRATEGY.SIMILAR}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: '',
|
||||||
|
title: t('setting.autoDJ_songStrategy'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={strategySelectData}
|
||||||
|
onChange={(value) =>
|
||||||
|
value &&
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
albumStrategy: value as AutoDJStrategy,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={settings.albumStrategy ?? AUTO_DJ_STRATEGY.SIMILAR}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: '',
|
||||||
|
title: t('setting.autoDJ_albumStrategy'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
aria-label="Auto DJ item count"
|
aria-label={itemLabels.title}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
max={50}
|
max={50}
|
||||||
min={1}
|
min={1}
|
||||||
@@ -31,10 +120,8 @@ export const AutoDJSettings = memo(() => {
|
|||||||
value={Number(settings.itemCount)}
|
value={Number(settings.itemCount)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
description: t('setting.autoDJ_itemCount', {
|
description: itemLabels.description,
|
||||||
context: 'description',
|
title: itemLabels.title,
|
||||||
}),
|
|
||||||
title: t('setting.autoDJ_itemCount'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
|
|||||||
Vendored
+3
@@ -1,8 +1,11 @@
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
ANALYTICS_DISABLED?: boolean | string;
|
ANALYTICS_DISABLED?: boolean | string;
|
||||||
|
FS_AUTO_DJ_ALBUM_STRATEGY?: string;
|
||||||
FS_AUTO_DJ_ENABLED?: string;
|
FS_AUTO_DJ_ENABLED?: string;
|
||||||
FS_AUTO_DJ_ITEM_COUNT?: string;
|
FS_AUTO_DJ_ITEM_COUNT?: string;
|
||||||
|
FS_AUTO_DJ_MODE?: string;
|
||||||
|
FS_AUTO_DJ_SONG_STRATEGY?: string;
|
||||||
FS_AUTO_DJ_TIMING?: string;
|
FS_AUTO_DJ_TIMING?: string;
|
||||||
FS_CSS_CONTENT?: string;
|
FS_CSS_CONTENT?: string;
|
||||||
FS_CSS_ENABLED?: string;
|
FS_CSS_ENABLED?: string;
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ const SIDE_QUEUE_TYPES = new Set(['sideDrawerQueue', 'sideQueue']);
|
|||||||
const SIDE_QUEUE_LAYOUTS = new Set(['horizontal', 'vertical']);
|
const SIDE_QUEUE_LAYOUTS = new Set(['horizontal', 'vertical']);
|
||||||
const SIDEBAR_PLAYLIST_FOLDER_VIEWS = new Set(['navigation', 'single', 'tree']);
|
const SIDEBAR_PLAYLIST_FOLDER_VIEWS = new Set(['navigation', 'single', 'tree']);
|
||||||
const SIDEBAR_PLAYLIST_MODES = new Set(['compact', 'expanded']);
|
const SIDEBAR_PLAYLIST_MODES = new Set(['compact', 'expanded']);
|
||||||
|
const AUTO_DJ_MODES = new Set(['albums', 'songs']);
|
||||||
|
const AUTO_DJ_STRATEGIES = new Set(['library_random', 'similar']);
|
||||||
|
|
||||||
export type EnvSettingsOverrides = DeepPartial<
|
export type EnvSettingsOverrides = DeepPartial<
|
||||||
Pick<SettingsState, 'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'playback'>
|
Pick<SettingsState, 'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'playback'>
|
||||||
@@ -422,8 +424,21 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
|
|||||||
path: ['lyrics', 'alignment'],
|
path: ['lyrics', 'alignment'],
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
enumSet: AUTO_DJ_STRATEGIES,
|
||||||
|
key: 'FS_AUTO_DJ_ALBUM_STRATEGY',
|
||||||
|
path: ['autoDJ', 'albumStrategy'],
|
||||||
|
type: 'enum',
|
||||||
|
},
|
||||||
{ key: 'FS_AUTO_DJ_ENABLED', path: ['autoDJ', 'enabled'], type: 'bool' },
|
{ key: 'FS_AUTO_DJ_ENABLED', path: ['autoDJ', 'enabled'], type: 'bool' },
|
||||||
{ key: 'FS_AUTO_DJ_ITEM_COUNT', path: ['autoDJ', 'itemCount'], type: 'num' },
|
{ key: 'FS_AUTO_DJ_ITEM_COUNT', path: ['autoDJ', 'itemCount'], type: 'num' },
|
||||||
|
{ enumSet: AUTO_DJ_MODES, key: 'FS_AUTO_DJ_MODE', path: ['autoDJ', 'mode'], type: 'enum' },
|
||||||
|
{
|
||||||
|
enumSet: AUTO_DJ_STRATEGIES,
|
||||||
|
key: 'FS_AUTO_DJ_SONG_STRATEGY',
|
||||||
|
path: ['autoDJ', 'songStrategy'],
|
||||||
|
type: 'enum',
|
||||||
|
},
|
||||||
{ key: 'FS_AUTO_DJ_TIMING', path: ['autoDJ', 'timing'], type: 'num' },
|
{ key: 'FS_AUTO_DJ_TIMING', path: ['autoDJ', 'timing'], type: 'num' },
|
||||||
{
|
{
|
||||||
key: 'FS_CSS_CONTENT',
|
key: 'FS_CSS_CONTENT',
|
||||||
|
|||||||
@@ -675,9 +675,28 @@ const QueryBuilderSettingsSchema = z.object({
|
|||||||
tag: z.array(QueryBuilderCustomFieldSchema),
|
tag: z.array(QueryBuilderCustomFieldSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AUTO_DJ_MODE = {
|
||||||
|
ALBUMS: 'albums',
|
||||||
|
SONGS: 'songs',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AutoDJMode = (typeof AUTO_DJ_MODE)[keyof typeof AUTO_DJ_MODE];
|
||||||
|
|
||||||
|
export const AUTO_DJ_STRATEGY = {
|
||||||
|
LIBRARY_RANDOM: 'library_random',
|
||||||
|
SIMILAR: 'similar',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AutoDJStrategy = (typeof AUTO_DJ_STRATEGY)[keyof typeof AUTO_DJ_STRATEGY];
|
||||||
|
|
||||||
|
const autoDjStrategyEnum = z.enum(['similar', 'library_random']);
|
||||||
|
|
||||||
const AutoDJSettingsSchema = z.object({
|
const AutoDJSettingsSchema = z.object({
|
||||||
|
albumStrategy: autoDjStrategyEnum,
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
itemCount: z.number(),
|
itemCount: z.number(),
|
||||||
|
mode: z.enum(['songs', 'albums']),
|
||||||
|
songStrategy: autoDjStrategyEnum,
|
||||||
timing: z.number(),
|
timing: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1091,8 +1110,11 @@ const platformDefaultWindowBarStyle: Platform = getPlatformDefaultWindowBarStyle
|
|||||||
|
|
||||||
const initialState: SettingsState = {
|
const initialState: SettingsState = {
|
||||||
autoDJ: {
|
autoDJ: {
|
||||||
|
albumStrategy: AUTO_DJ_STRATEGY.SIMILAR,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
itemCount: 5,
|
itemCount: 5,
|
||||||
|
mode: 'songs',
|
||||||
|
songStrategy: AUTO_DJ_STRATEGY.SIMILAR,
|
||||||
timing: 1,
|
timing: 1,
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
@@ -2427,10 +2449,41 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version < 28) {
|
||||||
|
if (!state.autoDJ) {
|
||||||
|
state.autoDJ = { ...initialState.autoDJ };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.autoDJ.mode !== 'albums' && state.autoDJ.mode !== 'songs') {
|
||||||
|
state.autoDJ.mode = initialState.autoDJ.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeAutoDjStrategy = (stored: unknown) => {
|
||||||
|
if (stored === 'library_random') {
|
||||||
|
return AUTO_DJ_STRATEGY.LIBRARY_RANDOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
stored === 'similar' ||
|
||||||
|
stored === 'default' ||
|
||||||
|
stored === 'similar_forward'
|
||||||
|
) {
|
||||||
|
return AUTO_DJ_STRATEGY.SIMILAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialState.autoDJ.songStrategy;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.autoDJ.songStrategy = normalizeAutoDjStrategy(state.autoDJ.songStrategy);
|
||||||
|
state.autoDJ.albumStrategy = normalizeAutoDjStrategy(
|
||||||
|
state.autoDJ.albumStrategy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return persistedState;
|
return persistedState;
|
||||||
},
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 27,
|
version: 28,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user