mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
feat(player): build infinite queue using instant-mix strategy (#163)
Prefer artist-driven candidates (Top + Similar) when extending infinite queue, with random songs as a fallback when mix sources are empty.
This commit is contained in:
@@ -64,6 +64,52 @@ export function songToTrack(song: SubsonicSong): Track {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shuffleArray<T>(items: T[]): T[] {
|
||||||
|
const arr = [...items];
|
||||||
|
for (let i = arr.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infinite queue source strategy (Instant Mix-like):
|
||||||
|
* 1) Prefer artist-driven candidates (Top + Similar) around the current track.
|
||||||
|
* 2) Fallback to random songs when artist-driven fetches are empty.
|
||||||
|
*/
|
||||||
|
async function buildInfiniteQueueCandidates(
|
||||||
|
seedTrack: Track | null,
|
||||||
|
existingIds: Set<string>,
|
||||||
|
count = 5,
|
||||||
|
): Promise<Track[]> {
|
||||||
|
const artistId = seedTrack?.artistId?.trim() || null;
|
||||||
|
const artistName = seedTrack?.artist?.trim() || null;
|
||||||
|
|
||||||
|
const [similar, top] = await Promise.all([
|
||||||
|
artistId ? getSimilarSongs2(artistId).catch(() => []) : Promise.resolve([]),
|
||||||
|
artistName ? getTopSongs(artistName).catch(() => []) : Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const seedId = seedTrack?.id ?? null;
|
||||||
|
const mixCandidates = shuffleArray(
|
||||||
|
[...top, ...similar]
|
||||||
|
.map(songToTrack)
|
||||||
|
.filter(t => t.id !== seedId && !existingIds.has(t.id)),
|
||||||
|
)
|
||||||
|
.slice(0, count)
|
||||||
|
.map(t => ({ ...t, autoAdded: true as const }));
|
||||||
|
|
||||||
|
if (mixCandidates.length > 0) return mixCandidates;
|
||||||
|
|
||||||
|
const random = await getRandomSongs(count, seedTrack?.genre).catch(() => []);
|
||||||
|
return random
|
||||||
|
.map(songToTrack)
|
||||||
|
.filter(t => t.id !== seedId && !existingIds.has(t.id))
|
||||||
|
.slice(0, count)
|
||||||
|
.map(t => ({ ...t, autoAdded: true as const }));
|
||||||
|
}
|
||||||
|
|
||||||
interface PlayerState {
|
interface PlayerState {
|
||||||
currentTrack: Track | null;
|
currentTrack: Track | null;
|
||||||
currentRadio: InternetRadioStation | null;
|
currentRadio: InternetRadioStation | null;
|
||||||
@@ -1017,9 +1063,9 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
const remainingAuto = queue.slice(nextIdx + 1).filter(t => t.autoAdded).length;
|
const remainingAuto = queue.slice(nextIdx + 1).filter(t => t.autoAdded).length;
|
||||||
if (remainingAuto <= 2) {
|
if (remainingAuto <= 2) {
|
||||||
infiniteQueueFetching = true;
|
infiniteQueueFetching = true;
|
||||||
getRandomSongs(5, currentTrack?.genre).then(songs => {
|
const existingIds = new Set(get().queue.map(t => t.id));
|
||||||
if (songs.length > 0) {
|
buildInfiniteQueueCandidates(currentTrack, existingIds, 5).then(newTracks => {
|
||||||
const newTracks: Track[] = songs.map(s => ({ ...songToTrack(s), autoAdded: true }));
|
if (newTracks.length > 0) {
|
||||||
set(state => ({ queue: [...state.queue, ...newTracks] }));
|
set(state => ({ queue: [...state.queue, ...newTracks] }));
|
||||||
}
|
}
|
||||||
}).catch(() => {}).finally(() => { infiniteQueueFetching = false; });
|
}).catch(() => {}).finally(() => { infiniteQueueFetching = false; });
|
||||||
@@ -1105,15 +1151,15 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
if (infiniteQueueEnabled && repeatMode === 'off') {
|
if (infiniteQueueEnabled && repeatMode === 'off') {
|
||||||
if (infiniteQueueFetching) return;
|
if (infiniteQueueFetching) return;
|
||||||
infiniteQueueFetching = true;
|
infiniteQueueFetching = true;
|
||||||
getRandomSongs(5, currentTrack?.genre).then(songs => {
|
const existingIds = new Set(get().queue.map(t => t.id));
|
||||||
|
buildInfiniteQueueCandidates(currentTrack, existingIds, 5).then(newTracks => {
|
||||||
infiniteQueueFetching = false;
|
infiniteQueueFetching = false;
|
||||||
if (songs.length === 0) {
|
if (newTracks.length === 0) {
|
||||||
invoke('audio_stop').catch(console.error);
|
invoke('audio_stop').catch(console.error);
|
||||||
isAudioPaused = false;
|
isAudioPaused = false;
|
||||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newTracks: Track[] = songs.map(s => ({ ...songToTrack(s), autoAdded: true }));
|
|
||||||
const currentQueue = get().queue;
|
const currentQueue = get().queue;
|
||||||
const newQueue = [...currentQueue, ...newTracks];
|
const newQueue = [...currentQueue, ...newTracks];
|
||||||
get().playTrack(newTracks[0], newQueue, false);
|
get().playTrack(newTracks[0], newQueue, false);
|
||||||
|
|||||||
Reference in New Issue
Block a user