mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-09 05:42:57 +02:00
add album mode for autodj
- add selection modes: similar, random - add autodj settings in playerbar popover
This commit is contained in:
@@ -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];
|
||||
};
|
||||
Reference in New Issue
Block a user