autodj fixes and enhancements (#2271)

- add setting to allow duplicates
- add setting to only prefer similar sogs
- fix autodj algorithm to match by the priority order instead of incorrectly grouping when limit not reached
This commit is contained in:
jeffvli
2026-07-23 19:51:30 -07:00
parent e0ac1c0db6
commit a0bae0ffb0
12 changed files with 406 additions and 127 deletions
+2
View File
@@ -128,9 +128,11 @@ Applies to the default lyrics display profile (`lyricsDisplay.default`).
| 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.albumStrategy` | `similar` | `FS_AUTO_DJ_ALBUM_STRATEGY` | `similar` / `library_random`. |
| `autoDJ.allowDuplicates` | `false` | `FS_AUTO_DJ_ALLOW_DUPLICATES` | `true` / `false` — Allow songs or albums already in the queue to be added again. |
| `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.mode` | `songs` | `FS_AUTO_DJ_MODE` | `songs` / `albums`. |
| `autoDJ.onlySimilar` | `false` | `FS_AUTO_DJ_ONLY_SIMILAR` | `true` / `false` — Treat item count as a maximum; use the first non-empty source (similar, then genre, artist, or random) and do not fill from later sources. |
| `autoDJ.songStrategy` | `similar` | `FS_AUTO_DJ_SONG_STRATEGY` | `similar` / `library_random`. | | `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). |
+2
View File
@@ -93,9 +93,11 @@ window.FS_LYRICS_PADDING_LEFT = "${FS_LYRICS_PADDING_LEFT}";
window.FS_LYRICS_PADDING_RIGHT = "${FS_LYRICS_PADDING_RIGHT}"; window.FS_LYRICS_PADDING_RIGHT = "${FS_LYRICS_PADDING_RIGHT}";
window.FS_AUTO_DJ_ALBUM_STRATEGY = "${FS_AUTO_DJ_ALBUM_STRATEGY}"; window.FS_AUTO_DJ_ALBUM_STRATEGY = "${FS_AUTO_DJ_ALBUM_STRATEGY}";
window.FS_AUTO_DJ_ALLOW_DUPLICATES = "${FS_AUTO_DJ_ALLOW_DUPLICATES}";
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_MODE = "${FS_AUTO_DJ_MODE}";
window.FS_AUTO_DJ_ONLY_SIMILAR = "${FS_AUTO_DJ_ONLY_SIMILAR}";
window.FS_AUTO_DJ_SONG_STRATEGY = "${FS_AUTO_DJ_SONG_STRATEGY}"; 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}";
+4
View File
@@ -796,6 +796,10 @@
"autoDJ_songStrategy": "Song selection mode", "autoDJ_songStrategy": "Song selection mode",
"autoDJ_strategy_option_library_random": "Random", "autoDJ_strategy_option_library_random": "Random",
"autoDJ_strategy_option_similar": "Similar", "autoDJ_strategy_option_similar": "Similar",
"autoDJ_allowDuplicates": "Allow duplicates",
"autoDJ_allowDuplicates_description": "Allow songs or albums already in the queue to be added again by Auto DJ",
"autoDJ_onlySimilar": "Only similar",
"autoDJ_onlySimilar_description": "Only add items that are similar to the triggering song. The item count is treated as a maximum amount of items to add instead of being filled up to the limit by other sources",
"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",
@@ -1,6 +1,6 @@
import type { QueryClient } from '@tanstack/react-query'; import type { QueryClient } from '@tanstack/react-query';
import { autoDjGenreIdsForSongGenre, autoDjPushUniqueAlbumIds } from './auto-dj-utils'; import { autoDjGenreIdsForSongGenre } from './auto-dj-utils';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { albumQueries } from '/@/renderer/features/albums/api/album-api'; import { albumQueries } from '/@/renderer/features/albums/api/album-api';
@@ -16,9 +16,11 @@ import {
export type AutoDjAlbumCollectArgs = { export type AutoDjAlbumCollectArgs = {
albumStrategy: AutoDJStrategy; albumStrategy: AutoDJStrategy;
allowDuplicates: boolean;
currentSong: QueueSong; currentSong: QueueSong;
itemCount: number; itemCount: number;
musicFolderId: string | string[] | undefined; musicFolderId: string | string[] | undefined;
onlySimilar: boolean;
queryClient: QueryClient; queryClient: QueryClient;
queueAlbumIdSet: Set<string>; queueAlbumIdSet: Set<string>;
server: null | ServerListItem | undefined; server: null | ServerListItem | undefined;
@@ -37,6 +39,19 @@ export const runAutoDjAlbumIds = async (args: AutoDjAlbumCollectArgs): Promise<s
} }
}; };
const isAlbumIdAvailable = (
albumId: string,
allowDuplicates: boolean,
queueAlbumIdSet: Set<string>,
selectedAlbumIdSet: Set<string>,
) => {
if (allowDuplicates) {
return true;
}
return !queueAlbumIdSet.has(albumId) && !selectedAlbumIdSet.has(albumId);
};
const collectAlbumsLibraryRandom = async (args: AutoDjAlbumCollectArgs): Promise<string[]> => { const collectAlbumsLibraryRandom = async (args: AutoDjAlbumCollectArgs): Promise<string[]> => {
const page = await args.queryClient.fetchQuery({ const page = await args.queryClient.fetchQuery({
...albumQueries.list({ ...albumQueries.list({
@@ -52,14 +67,18 @@ const collectAlbumsLibraryRandom = async (args: AutoDjAlbumCollectArgs): Promise
queryKey: queryKeys.player.fetch({ autoDjAlbumLibraryRandom: args.currentSong?.id }), queryKey: queryKeys.player.fetch({ autoDjAlbumLibraryRandom: args.currentSong?.id }),
}); });
const ids = page.items.map((a) => a.id).filter((id) => id && !args.queueAlbumIdSet.has(id)); const ids = page.items
.map((album) => album.id)
.filter(
(albumId) => albumId && (args.allowDuplicates || !args.queueAlbumIdSet.has(albumId)),
);
return shuffle(ids).slice(0, args.itemCount); return shuffle(ids).slice(0, args.itemCount);
}; };
const collectAlbumsSimilar = async (args: AutoDjAlbumCollectArgs): Promise<string[]> => { const collectAlbumsSimilar = async (args: AutoDjAlbumCollectArgs): Promise<string[]> => {
const targetAlbumCount = args.itemCount; const selectedAlbumIds: string[] = [];
const candidateAlbumIds: string[] = []; const selectedAlbumIdSet = new Set<string>();
const seenAlbumCandidates = new Set<string>(); const remainingCount = () => args.itemCount - selectedAlbumIds.length;
if (args.trySimilarSongs && args.currentSong?.id) { if (args.trySimilarSongs && args.currentSong?.id) {
const similarSongsFromSimilarApi = await args.queryClient.fetchQuery({ const similarSongsFromSimilarApi = await args.queryClient.fetchQuery({
@@ -75,24 +94,46 @@ const collectAlbumsSimilar = async (args: AutoDjAlbumCollectArgs): Promise<strin
}), }),
}); });
autoDjPushUniqueAlbumIds( const similarSlotsToFill = remainingCount();
candidateAlbumIds, const shuffledSimilarAlbumIds = shuffle(
seenAlbumCandidates, similarSongsFromSimilarApi
args.queueAlbumIdSet, .map((song) => song.albumId)
...similarSongsFromSimilarApi.map((s) => s.albumId), .filter((albumId): albumId is string => {
); if (!albumId) {
return false;
} }
if (candidateAlbumIds.length < targetAlbumCount && args.currentSong && args.server) { return isAlbumIdAvailable(
albumId,
args.allowDuplicates,
args.queueAlbumIdSet,
selectedAlbumIdSet,
);
}),
);
for (const albumId of shuffledSimilarAlbumIds.slice(0, similarSlotsToFill)) {
selectedAlbumIdSet.add(albumId);
selectedAlbumIds.push(albumId);
}
if (args.onlySimilar && selectedAlbumIds.length > 0) {
return selectedAlbumIds;
}
}
if (remainingCount() > 0 && args.currentSong && args.server) {
const genre = args.currentSong.genres?.[0]; const genre = args.currentSong.genres?.[0];
if (genre) { if (genre) {
const genreIds = autoDjGenreIdsForSongGenre(genre, args.server.type); const genreIds = autoDjGenreIdsForSongGenre(genre, args.server.type);
const genreLimit = 50;
const genreAlbums = await args.queryClient.fetchQuery({ const genreAlbums = await args.queryClient.fetchQuery({
...albumQueries.list({ ...albumQueries.list({
query: { query: {
genreIds, genreIds,
limit: 50, limit: genreLimit,
musicFolderId: args.musicFolderId, musicFolderId: args.musicFolderId,
sortBy: AlbumListSort.RANDOM, sortBy: AlbumListSort.RANDOM,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
@@ -106,15 +147,8 @@ const collectAlbumsSimilar = async (args: AutoDjAlbumCollectArgs): Promise<strin
}), }),
}); });
autoDjPushUniqueAlbumIds(
candidateAlbumIds,
seenAlbumCandidates,
args.queueAlbumIdSet,
...genreAlbums.items.map((album) => album.id),
);
if (!args.trySimilarSongs) { if (!args.trySimilarSongs) {
const randomAlbumMixCount = Math.max(1, Math.ceil(50 * 0.2)); const randomAlbumMixCount = Math.max(1, Math.ceil(genreLimit * 0.2));
const randomAlbumsMix = await args.queryClient.fetchQuery({ const randomAlbumsMix = await args.queryClient.fetchQuery({
...albumQueries.list({ ...albumQueries.list({
query: { query: {
@@ -131,17 +165,56 @@ const collectAlbumsSimilar = async (args: AutoDjAlbumCollectArgs): Promise<strin
}), }),
}); });
autoDjPushUniqueAlbumIds( const spiceSlotsToFill = Math.min(randomAlbumMixCount, remainingCount());
candidateAlbumIds, const shuffledSpiceAlbumIds = shuffle(
seenAlbumCandidates, randomAlbumsMix.items
.map((album) => album.id)
.filter(
(albumId): albumId is string =>
Boolean(albumId) &&
isAlbumIdAvailable(
albumId,
args.allowDuplicates,
args.queueAlbumIdSet, args.queueAlbumIdSet,
...randomAlbumsMix.items.map((album) => album.id), selectedAlbumIdSet,
),
),
); );
for (const albumId of shuffledSpiceAlbumIds.slice(0, spiceSlotsToFill)) {
selectedAlbumIdSet.add(albumId);
selectedAlbumIds.push(albumId);
}
}
const genreSlotsToFill = remainingCount();
const shuffledGenreAlbumIds = shuffle(
genreAlbums.items
.map((album) => album.id)
.filter(
(albumId): albumId is string =>
Boolean(albumId) &&
isAlbumIdAvailable(
albumId,
args.allowDuplicates,
args.queueAlbumIdSet,
selectedAlbumIdSet,
),
),
);
for (const albumId of shuffledGenreAlbumIds.slice(0, genreSlotsToFill)) {
selectedAlbumIdSet.add(albumId);
selectedAlbumIds.push(albumId);
}
if (args.onlySimilar && selectedAlbumIds.length > 0) {
return selectedAlbumIds;
} }
} }
} }
if (candidateAlbumIds.length < targetAlbumCount && args.currentSong) { if (remainingCount() > 0 && args.currentSong) {
const albumArtist = args.currentSong.albumArtists?.[0]; const albumArtist = args.currentSong.albumArtists?.[0];
if (albumArtist) { if (albumArtist) {
@@ -163,16 +236,34 @@ const collectAlbumsSimilar = async (args: AutoDjAlbumCollectArgs): Promise<strin
}), }),
}); });
autoDjPushUniqueAlbumIds( const artistSlotsToFill = remainingCount();
candidateAlbumIds, const shuffledArtistAlbumIds = shuffle(
seenAlbumCandidates, albumsByArtist.items
.map((album) => album.id)
.filter(
(albumId): albumId is string =>
Boolean(albumId) &&
isAlbumIdAvailable(
albumId,
args.allowDuplicates,
args.queueAlbumIdSet, args.queueAlbumIdSet,
...albumsByArtist.items.map((album) => album.id), selectedAlbumIdSet,
),
),
); );
for (const albumId of shuffledArtistAlbumIds.slice(0, artistSlotsToFill)) {
selectedAlbumIdSet.add(albumId);
selectedAlbumIds.push(albumId);
}
if (args.onlySimilar && selectedAlbumIds.length > 0) {
return selectedAlbumIds;
}
} }
} }
if (candidateAlbumIds.length < targetAlbumCount && args.currentSong) { if (remainingCount() > 0 && args.currentSong) {
const randomAlbumsFallback = await args.queryClient.fetchQuery({ const randomAlbumsFallback = await args.queryClient.fetchQuery({
...albumQueries.list({ ...albumQueries.list({
query: { query: {
@@ -189,14 +280,27 @@ const collectAlbumsSimilar = async (args: AutoDjAlbumCollectArgs): Promise<strin
}), }),
}); });
autoDjPushUniqueAlbumIds( const randomSlotsToFill = remainingCount();
candidateAlbumIds, const shuffledRandomAlbumIds = shuffle(
seenAlbumCandidates, randomAlbumsFallback.items
.map((album) => album.id)
.filter(
(albumId): albumId is string =>
Boolean(albumId) &&
isAlbumIdAvailable(
albumId,
args.allowDuplicates,
args.queueAlbumIdSet, args.queueAlbumIdSet,
...randomAlbumsFallback.items.map((album) => album.id), selectedAlbumIdSet,
),
),
); );
for (const albumId of shuffledRandomAlbumIds.slice(0, randomSlotsToFill)) {
selectedAlbumIdSet.add(albumId);
selectedAlbumIds.push(albumId);
}
} }
const shuffledAlbums = shuffle(candidateAlbumIds); return selectedAlbumIds;
return shuffledAlbums.slice(0, targetAlbumCount);
}; };
@@ -14,9 +14,11 @@ import {
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
export type AutoDjSongCollectArgs = { export type AutoDjSongCollectArgs = {
allowDuplicates: boolean;
currentSong: QueueSong; currentSong: QueueSong;
itemCount: number; itemCount: number;
musicFolderId: string | string[] | undefined; musicFolderId: string | string[] | undefined;
onlySimilar: boolean;
queryClient: QueryClient; queryClient: QueryClient;
queueSongIdSet: Set<string>; queueSongIdSet: Set<string>;
server: null | ServerListItem | undefined; server: null | ServerListItem | undefined;
@@ -36,6 +38,19 @@ export const runAutoDjSongs = async (args: AutoDjSongCollectArgs): Promise<Song[
} }
}; };
const isSongAvailable = (
song: Song,
allowDuplicates: boolean,
queueSongIdSet: Set<string>,
selectedSongIds: Set<string>,
) => {
if (allowDuplicates) {
return true;
}
return !queueSongIdSet.has(song.id) && !selectedSongIds.has(song.id);
};
const collectSongsLibraryRandom = async (args: AutoDjSongCollectArgs): Promise<Song[]> => { const collectSongsLibraryRandom = async (args: AutoDjSongCollectArgs): Promise<Song[]> => {
const randomSongs = await args.queryClient.fetchQuery({ const randomSongs = await args.queryClient.fetchQuery({
...songsQueries.random({ ...songsQueries.random({
@@ -48,13 +63,17 @@ const collectSongsLibraryRandom = async (args: AutoDjSongCollectArgs): Promise<S
queryKey: queryKeys.player.fetch({ autoDjLibraryRandomSongs: args.currentSong.id }), queryKey: queryKeys.player.fetch({ autoDjLibraryRandomSongs: args.currentSong.id }),
}); });
const pool = randomSongs.items.filter((song) => !args.queueSongIdSet.has(song.id)); const pool = randomSongs.items.filter(
(song) => args.allowDuplicates || !args.queueSongIdSet.has(song.id),
);
const shuffled = shuffleInPlace(pool); const shuffled = shuffleInPlace(pool);
return shuffled.slice(0, args.itemCount); return shuffled.slice(0, args.itemCount);
}; };
const collectSongsSimilar = async (args: AutoDjSongCollectArgs): Promise<Song[]> => { const collectSongsSimilar = async (args: AutoDjSongCollectArgs): Promise<Song[]> => {
let uniqueSimilarSongs: Song[] = []; const selected: Song[] = [];
const selectedSongIds = new Set<string>();
const remainingCount = () => args.itemCount - selected.length;
if (args.trySimilarSongs) { if (args.trySimilarSongs) {
const similarSongs = await args.queryClient.fetchQuery({ const similarSongs = await args.queryClient.fetchQuery({
@@ -68,10 +87,24 @@ const collectSongsSimilar = async (args: AutoDjSongCollectArgs): Promise<Song[]>
queryKey: queryKeys.player.fetch({ similarSongs: args.currentSong?.id }), queryKey: queryKeys.player.fetch({ similarSongs: args.currentSong?.id }),
}); });
uniqueSimilarSongs = similarSongs.filter((song) => !args.queueSongIdSet.has(song.id)); const slotsToFill = remainingCount();
const shuffledSimilarSongs = shuffleInPlace(
similarSongs.filter((song) =>
isSongAvailable(song, args.allowDuplicates, args.queueSongIdSet, selectedSongIds),
),
);
for (const song of shuffledSimilarSongs.slice(0, slotsToFill)) {
selectedSongIds.add(song.id);
selected.push(song);
} }
if (uniqueSimilarSongs.length < args.itemCount) { if (args.onlySimilar && selected.length > 0) {
return selected;
}
}
if (remainingCount() > 0) {
const genre = args.currentSong?.genres?.[0]; const genre = args.currentSong?.genres?.[0];
if (genre) { if (genre) {
@@ -91,10 +124,6 @@ const collectSongsSimilar = async (args: AutoDjSongCollectArgs): Promise<Song[]>
}), }),
}); });
const genreSongs = genreSimilarSongs.items.filter(
(song) => !args.queueSongIdSet.has(song.id),
);
if (!args.trySimilarSongs) { if (!args.trySimilarSongs) {
const randomSongCount = Math.max(1, Math.ceil(genreLimit * 0.2)); const randomSongCount = Math.max(1, Math.ceil(genreLimit * 0.2));
@@ -105,19 +134,48 @@ const collectSongsSimilar = async (args: AutoDjSongCollectArgs): Promise<Song[]>
}), }),
}); });
const uniqueRandomSongs = randomSongs.items.filter( const spiceSlotsToFill = Math.min(randomSongCount, remainingCount());
(song) => !args.queueSongIdSet.has(song.id), const shuffledSpiceSongs = shuffleInPlace(
randomSongs.items.filter((song) =>
isSongAvailable(
song,
args.allowDuplicates,
args.queueSongIdSet,
selectedSongIds,
),
),
); );
const randomSongsToAdd = uniqueRandomSongs.slice(0, randomSongCount); for (const song of shuffledSpiceSongs.slice(0, spiceSlotsToFill)) {
uniqueSimilarSongs.push(...randomSongsToAdd, ...genreSongs); selectedSongIds.add(song.id);
} else { selected.push(song);
uniqueSimilarSongs.push(...genreSongs); }
}
const genreSlotsToFill = remainingCount();
const shuffledGenreSongs = shuffleInPlace(
genreSimilarSongs.items.filter((song) =>
isSongAvailable(
song,
args.allowDuplicates,
args.queueSongIdSet,
selectedSongIds,
),
),
);
for (const song of shuffledGenreSongs.slice(0, genreSlotsToFill)) {
selectedSongIds.add(song.id);
selected.push(song);
}
if (args.onlySimilar && selected.length > 0) {
return selected;
} }
} }
} }
if (uniqueSimilarSongs.length < args.itemCount) { if (remainingCount() > 0) {
const albumArtist = args.currentSong?.albumArtists?.[0]; const albumArtist = args.currentSong?.albumArtists?.[0];
if (albumArtist) { if (albumArtist) {
@@ -138,15 +196,30 @@ const collectSongsSimilar = async (args: AutoDjSongCollectArgs): Promise<Song[]>
}), }),
}); });
uniqueSimilarSongs.push( const artistSlotsToFill = remainingCount();
...albumArtistSimilarSongs.items.filter( const shuffledArtistSongs = shuffleInPlace(
(song) => !args.queueSongIdSet.has(song.id), albumArtistSimilarSongs.items.filter((song) =>
isSongAvailable(
song,
args.allowDuplicates,
args.queueSongIdSet,
selectedSongIds,
),
), ),
); );
for (const song of shuffledArtistSongs.slice(0, artistSlotsToFill)) {
selectedSongIds.add(song.id);
selected.push(song);
}
if (args.onlySimilar && selected.length > 0) {
return selected;
}
} }
} }
if (uniqueSimilarSongs.length < args.itemCount) { if (remainingCount() > 0) {
const randomSongs = await args.queryClient.fetchQuery({ const randomSongs = await args.queryClient.fetchQuery({
...songsQueries.random({ ...songsQueries.random({
query: { limit: 50, played: Played.All }, query: { limit: 50, played: Played.All },
@@ -154,11 +227,18 @@ const collectSongsSimilar = async (args: AutoDjSongCollectArgs): Promise<Song[]>
}), }),
}); });
uniqueSimilarSongs.push( const randomSlotsToFill = remainingCount();
...randomSongs.items.filter((song) => !args.queueSongIdSet.has(song.id)), const shuffledRandomSongs = shuffleInPlace(
randomSongs.items.filter((song) =>
isSongAvailable(song, args.allowDuplicates, args.queueSongIdSet, selectedSongIds),
),
); );
for (const song of shuffledRandomSongs.slice(0, randomSlotsToFill)) {
selectedSongIds.add(song.id);
selected.push(song);
}
} }
const shuffledSongs = shuffleInPlace(uniqueSimilarSongs); return selected;
return shuffledSongs.slice(0, args.itemCount);
}; };
@@ -2,19 +2,6 @@ import type { Genre } from '/@/shared/types/domain-types';
import { ServerType } 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[] => { export const autoDjGenreIdsForSongGenre = (genre: Genre, serverType: ServerType): string[] => {
if (serverType === ServerType.JELLYFIN) { if (serverType === ServerType.JELLYFIN) {
return [genre.id]; return [genre.id];
@@ -194,6 +194,7 @@ const AutoDJButton = () => {
value={settings.mode} value={settings.mode}
w="100%" w="100%"
/> />
<Paper p="md" radius="md">
<Select <Select
comboboxProps={{ withinPortal: false }} comboboxProps={{ withinPortal: false }}
data={strategySelectData} data={strategySelectData}
@@ -208,10 +209,13 @@ const AutoDJButton = () => {
: { songStrategy: value as AutoDJStrategy }, : { songStrategy: value as AutoDJStrategy },
}); });
}} }}
size="md" size="sm"
value={strategyValue} value={strategyValue}
variant="filled"
w="100%" w="100%"
/> />
</Paper>
<Paper p="md" radius="md">
<NumberInput <NumberInput
aria-label={itemLabels.title} aria-label={itemLabels.title}
description={itemLabels.description} description={itemLabels.description}
@@ -226,9 +230,12 @@ const AutoDJButton = () => {
}, },
}) })
} }
size="md" size="sm"
value={Number(settings.itemCount)} value={Number(settings.itemCount)}
variant="filled"
/> />
</Paper>
<Paper p="md" radius="md">
<NumberInput <NumberInput
aria-label={t('setting.autoDJ_timing')} aria-label={t('setting.autoDJ_timing')}
description={t('setting.autoDJ_timing_description')} description={t('setting.autoDJ_timing_description')}
@@ -243,9 +250,55 @@ const AutoDJButton = () => {
}, },
}) })
} }
size="md" size="sm"
value={Number(settings.timing)} value={Number(settings.timing)}
variant="filled"
/> />
</Paper>
<Paper p="md" radius="md">
<Group align="center" gap="sm" justify="space-between" wrap="nowrap">
<Stack gap="xs" style={{ flex: 1, minWidth: 0 }}>
<Text fw={600} isNoSelect size="sm">
{t('setting.autoDJ_allowDuplicates')}
</Text>
<Text isMuted isNoSelect size="xs">
{t('setting.autoDJ_allowDuplicates_description')}
</Text>
</Stack>
<Switch
checked={settings.allowDuplicates}
onChange={(e) =>
setSettings({
autoDJ: {
allowDuplicates: e.currentTarget.checked,
},
})
}
/>
</Group>
</Paper>
<Paper p="md" radius="md">
<Group align="center" gap="sm" justify="space-between" wrap="nowrap">
<Stack gap="xs" style={{ flex: 1, minWidth: 0 }}>
<Text fw={600} isNoSelect size="sm">
{t('setting.autoDJ_onlySimilar')}
</Text>
<Text isMuted isNoSelect size="xs">
{t('setting.autoDJ_onlySimilar_description')}
</Text>
</Stack>
<Switch
checked={settings.onlySimilar}
onChange={(e) =>
setSettings({
autoDJ: {
onlySimilar: e.currentTarget.checked,
},
})
}
/>
</Group>
</Paper>
</Stack> </Stack>
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
@@ -79,8 +79,10 @@ export const useAutoDJ = () => {
!hasMusicFolder || (hasMusicFolder && hasSimilarSongsMusicFolder); !hasMusicFolder || (hasMusicFolder && hasSimilarSongsMusicFolder);
const runnerDepsBase = { const runnerDepsBase = {
allowDuplicates: settings.allowDuplicates,
itemCount: settings.itemCount, itemCount: settings.itemCount,
musicFolderId, musicFolderId,
onlySimilar: settings.onlySimilar,
queryClient, queryClient,
server, server,
serverId, serverId,
@@ -165,8 +167,10 @@ export const useAutoDJ = () => {
serverId, serverId,
settings.enabled, settings.enabled,
settings.albumStrategy, settings.albumStrategy,
settings.allowDuplicates,
settings.itemCount, settings.itemCount,
settings.mode, settings.mode,
settings.onlySimilar,
settings.songStrategy, settings.songStrategy,
settings.timing, settings.timing,
]); ]);
@@ -15,6 +15,7 @@ import {
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 { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Select } from '/@/shared/components/select/select'; import { Select } from '/@/shared/components/select/select';
import { Switch } from '/@/shared/components/switch/switch';
export const AutoDJSettings = memo(() => { export const AutoDJSettings = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -145,6 +146,40 @@ export const AutoDJSettings = memo(() => {
}), }),
title: t('setting.autoDJ_timing'), title: t('setting.autoDJ_timing'),
}, },
{
control: (
<Switch
aria-label={t('setting.autoDJ_allowDuplicates')}
checked={settings.allowDuplicates}
onChange={(e) => {
setSettings({
autoDJ: {
allowDuplicates: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.autoDJ_allowDuplicates_description'),
title: t('setting.autoDJ_allowDuplicates'),
},
{
control: (
<Switch
aria-label={t('setting.autoDJ_onlySimilar')}
checked={settings.onlySimilar}
onChange={(e) => {
setSettings({
autoDJ: {
onlySimilar: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.autoDJ_onlySimilar_description'),
title: t('setting.autoDJ_onlySimilar'),
},
]; ];
return <SettingsSection options={autoDJOptions} title={t('setting.autoDJ')} />; return <SettingsSection options={autoDJOptions} title={t('setting.autoDJ')} />;
+2
View File
@@ -2,9 +2,11 @@ declare global {
interface Window { interface Window {
ANALYTICS_DISABLED?: boolean | string; ANALYTICS_DISABLED?: boolean | string;
FS_AUTO_DJ_ALBUM_STRATEGY?: string; FS_AUTO_DJ_ALBUM_STRATEGY?: string;
FS_AUTO_DJ_ALLOW_DUPLICATES?: 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_MODE?: string;
FS_AUTO_DJ_ONLY_SIMILAR?: string;
FS_AUTO_DJ_SONG_STRATEGY?: 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;
@@ -461,9 +461,11 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
path: ['autoDJ', 'albumStrategy'], path: ['autoDJ', 'albumStrategy'],
type: 'enum', type: 'enum',
}, },
{ key: 'FS_AUTO_DJ_ALLOW_DUPLICATES', path: ['autoDJ', 'allowDuplicates'], type: 'bool' },
{ 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_MODES, key: 'FS_AUTO_DJ_MODE', path: ['autoDJ', 'mode'], type: 'enum' },
{ key: 'FS_AUTO_DJ_ONLY_SIMILAR', path: ['autoDJ', 'onlySimilar'], type: 'bool' },
{ {
enumSet: AUTO_DJ_STRATEGIES, enumSet: AUTO_DJ_STRATEGIES,
key: 'FS_AUTO_DJ_SONG_STRATEGY', key: 'FS_AUTO_DJ_SONG_STRATEGY',
+4
View File
@@ -746,9 +746,11 @@ const autoDjStrategyEnum = z.enum(['similar', 'library_random']);
const AutoDJSettingsSchema = z.object({ const AutoDJSettingsSchema = z.object({
albumStrategy: autoDjStrategyEnum, albumStrategy: autoDjStrategyEnum,
allowDuplicates: z.boolean(),
enabled: z.boolean(), enabled: z.boolean(),
itemCount: z.number(), itemCount: z.number(),
mode: z.enum(['songs', 'albums']), mode: z.enum(['songs', 'albums']),
onlySimilar: z.boolean(),
songStrategy: autoDjStrategyEnum, songStrategy: autoDjStrategyEnum,
timing: z.number(), timing: z.number(),
}); });
@@ -1219,9 +1221,11 @@ const platformDefaultWindowBarStyle: Platform = getPlatformDefaultWindowBarStyle
const initialState: SettingsState = { const initialState: SettingsState = {
autoDJ: { autoDJ: {
albumStrategy: AUTO_DJ_STRATEGY.SIMILAR, albumStrategy: AUTO_DJ_STRATEGY.SIMILAR,
allowDuplicates: false,
enabled: false, enabled: false,
itemCount: 5, itemCount: 5,
mode: 'songs', mode: 'songs',
onlySimilar: false,
songStrategy: AUTO_DJ_STRATEGY.SIMILAR, songStrategy: AUTO_DJ_STRATEGY.SIMILAR,
timing: 1, timing: 1,
}, },