optimize player state for large queues

This commit is contained in:
jeffvli
2025-11-20 12:34:31 -08:00
parent ab3e7caad1
commit b8dd1a1548
2 changed files with 134 additions and 50 deletions
@@ -340,12 +340,14 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
} }
// Get total count // Get total count
const countResult = (await queryClient.fetchQuery( const countResult = (await queryClient.fetchQuery({
listCountQueryFn({ ...listCountQueryFn({
query: { ...query }, query: { ...query },
serverId, serverId,
}), }),
)) as number; gcTime: 0,
staleTime: 0,
})) as number;
totalCount = countResult || 0; totalCount = countResult || 0;
// Check if we need confirmation // Check if we need confirmation
@@ -392,12 +394,14 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
startIndex, startIndex,
}; };
const pageResult = (await queryClient.fetchQuery( const pageResult = (await queryClient.fetchQuery({
listQueryFn({ ...listQueryFn({
query: pageQuery, query: pageQuery,
serverId, serverId,
}), }),
)) as { items: any[] }; gcTime: 0,
staleTime: 0,
})) as { items: any[] };
if (pageResult?.items) { if (pageResult?.items) {
const pageIds = pageResult.items.map((item: any) => item.id); const pageIds = pageResult.items.map((item: any) => item.id);
@@ -729,8 +733,8 @@ export async function fetchSongsByItemType(
for (const id of args.id) { for (const id of args.id) {
promises.push( promises.push(
queryClient.fetchQuery( queryClient.fetchQuery({
songsQueries.list({ ...songsQueries.list({
query: { query: {
albumIds: [id], albumIds: [id],
sortBy: SongListSort.ID, sortBy: SongListSort.ID,
@@ -740,7 +744,9 @@ export async function fetchSongsByItemType(
}, },
serverId: serverId, serverId: serverId,
}), }),
), gcTime: 0,
staleTime: 0,
}),
); );
} }
@@ -756,8 +762,8 @@ export async function fetchSongsByItemType(
for (const id of args.id) { for (const id of args.id) {
promises.push( promises.push(
queryClient.fetchQuery( queryClient.fetchQuery({
songsQueries.list({ ...songsQueries.list({
query: { query: {
albumArtistIds: [id], albumArtistIds: [id],
limit: -1, limit: -1,
@@ -768,7 +774,9 @@ export async function fetchSongsByItemType(
}, },
serverId: serverId, serverId: serverId,
}), }),
), gcTime: 0,
staleTime: 0,
}),
); );
} }
@@ -783,8 +791,8 @@ export async function fetchSongsByItemType(
for (const id of args.id) { for (const id of args.id) {
promises.push( promises.push(
queryClient.fetchQuery( queryClient.fetchQuery({
songsQueries.list({ ...songsQueries.list({
query: { query: {
genreIds: [id], genreIds: [id],
limit: -1, limit: -1,
@@ -795,7 +803,9 @@ export async function fetchSongsByItemType(
}, },
serverId: serverId, serverId: serverId,
}), }),
), gcTime: 0,
staleTime: 0,
}),
); );
} }
@@ -809,15 +819,17 @@ export async function fetchSongsByItemType(
for (const id of args.id) { for (const id of args.id) {
promises.push( promises.push(
queryClient.fetchQuery( queryClient.fetchQuery({
playlistsQueries.songList({ ...playlistsQueries.songList({
query: { query: {
id: id, id: id,
...args.params, ...args.params,
}, },
serverId: serverId, serverId: serverId,
}), }),
), gcTime: 0,
staleTime: 0,
}),
); );
} }
+104 -32
View File
@@ -135,7 +135,6 @@ export const usePlayerStoreBase = create<PlayerState>()(
case Play.LAST: { case Play.LAST: {
set((state) => { set((state) => {
const currentIndex = state.player.index; const currentIndex = state.player.index;
// Add new songs to songs object
newItems.forEach((item) => { newItems.forEach((item) => {
state.queue.songs[item._uniqueId] = item; state.queue.songs[item._uniqueId] = item;
}); });
@@ -161,7 +160,6 @@ export const usePlayerStoreBase = create<PlayerState>()(
case Play.NEXT: { case Play.NEXT: {
set((state) => { set((state) => {
const currentIndex = state.player.index; const currentIndex = state.player.index;
// Add new songs to songs object
newItems.forEach((item) => { newItems.forEach((item) => {
state.queue.songs[item._uniqueId] = item; state.queue.songs[item._uniqueId] = item;
}); });
@@ -187,7 +185,6 @@ export const usePlayerStoreBase = create<PlayerState>()(
} }
case Play.NOW: { case Play.NOW: {
set((state) => { set((state) => {
// Add new songs to songs object
newItems.forEach((item) => { newItems.forEach((item) => {
state.queue.songs[item._uniqueId] = item; state.queue.songs[item._uniqueId] = item;
}); });
@@ -208,7 +205,6 @@ export const usePlayerStoreBase = create<PlayerState>()(
} }
case Play.SHUFFLE: { case Play.SHUFFLE: {
set((state) => { set((state) => {
// Add new songs to songs object
newItems.forEach((item) => { newItems.forEach((item) => {
state.queue.songs[item._uniqueId] = item; state.queue.songs[item._uniqueId] = item;
}); });
@@ -485,33 +481,27 @@ export const usePlayerStoreBase = create<PlayerState>()(
state.player.index = -1; state.player.index = -1;
state.queue.default = []; state.queue.default = [];
state.queue.priority = []; state.queue.priority = [];
state.queue.shuffled = [];
state.queue.songs = {}; state.queue.songs = {};
}); });
}, },
clearSelected: (items: QueueSong[]) => { clearSelected: (items: QueueSong[]) => {
set((state) => { set((state) => {
const uniqueIds = items.map((item) => item._uniqueId); const uniqueIds = new Set(items.map((item) => item._uniqueId));
state.queue.default = state.queue.default.filter( state.queue.default = state.queue.default.filter(
(id) => !uniqueIds.includes(id), (id) => !uniqueIds.has(id),
); );
state.queue.priority = state.queue.priority.filter( state.queue.priority = state.queue.priority.filter(
(id) => !uniqueIds.includes(id), (id) => !uniqueIds.has(id),
); );
// Remove songs from songs object if they're not in default or priority state.queue.shuffled = state.queue.shuffled.filter(
const remainingIds = new Set([ (id) => !uniqueIds.has(id),
...state.queue.default, );
...state.queue.priority,
]); cleanupOrphanedSongs(state);
const filteredSongs: Record<string, QueueSong> = {};
Object.values(state.queue.songs).forEach((song) => {
if (remainingIds.has(song._uniqueId)) {
filteredSongs[song._uniqueId] = song;
}
});
state.queue.songs = filteredSongs;
const newQueue = [...state.queue.priority, ...state.queue.default]; const newQueue = [...state.queue.priority, ...state.queue.default];
@@ -569,18 +559,24 @@ export const usePlayerStoreBase = create<PlayerState>()(
getQueueOrder: () => { getQueueOrder: () => {
const queueType = getQueueType(); const queueType = getQueueType();
const state = get(); const state = get();
const songsMap = new Map(Object.entries(state.queue.songs)); const songs = state.queue.songs;
if (queueType === PlayerQueueType.PRIORITY) { if (queueType === PlayerQueueType.PRIORITY) {
const defaultIds = state.queue.default; const defaultIds = state.queue.default;
const priorityIds = state.queue.priority; const priorityIds = state.queue.priority;
const defaultQueue = defaultIds const defaultQueue: QueueSong[] = [];
.map((id) => songsMap.get(id)) const priorityQueue: QueueSong[] = [];
.filter((song): song is QueueSong => song !== undefined);
const priorityQueue = priorityIds for (const id of priorityIds) {
.map((id) => songsMap.get(id)) const song = songs[id];
.filter((song): song is QueueSong => song !== undefined); if (song) priorityQueue.push(song);
}
for (const id of defaultIds) {
const song = songs[id];
if (song) defaultQueue.push(song);
}
return { return {
groups: [ groups: [
@@ -592,9 +588,12 @@ export const usePlayerStoreBase = create<PlayerState>()(
} }
const defaultIds = state.queue.default; const defaultIds = state.queue.default;
const defaultQueue = defaultIds const defaultQueue: QueueSong[] = [];
.map((id) => songsMap.get(id))
.filter((song): song is QueueSong => song !== undefined); for (const id of defaultIds) {
const song = songs[id];
if (song) defaultQueue.push(song);
}
return { return {
groups: [{ count: defaultQueue.length, name: 'All' }], groups: [{ count: defaultQueue.length, name: 'All' }],
@@ -736,6 +735,7 @@ export const usePlayerStoreBase = create<PlayerState>()(
state.player.index = -1; state.player.index = -1;
state.queue.default = []; state.queue.default = [];
state.queue.priority = []; state.queue.priority = [];
state.queue.shuffled = [];
state.queue.songs = {}; state.queue.songs = {};
}); });
}, },
@@ -1073,6 +1073,7 @@ export const usePlayerStoreBase = create<PlayerState>()(
} }
state.player.queueType = queueType; state.player.queueType = queueType;
cleanupOrphanedSongs(state);
}); });
}, },
setRepeat: (repeat: PlayerRepeat) => { setRepeat: (repeat: PlayerRepeat) => {
@@ -1085,6 +1086,7 @@ export const usePlayerStoreBase = create<PlayerState>()(
state.player.shuffle = shuffle; state.player.shuffle = shuffle;
const queue = state.queue.default; const queue = state.queue.default;
state.queue.shuffled = shuffleInPlace([...queue]); state.queue.shuffled = shuffleInPlace([...queue]);
cleanupOrphanedSongs(state);
}); });
}, },
setSpeed: (speed: number) => { setSpeed: (speed: number) => {
@@ -1218,6 +1220,28 @@ export const usePlayerStoreBase = create<PlayerState>()(
) as typeof filteredState.player; ) as typeof filteredState.player;
} }
if (filteredState.queue) {
const allQueueIds = new Set([
...(filteredState.queue.default || []),
...(filteredState.queue.priority || []),
...(filteredState.queue.shuffled || []),
]);
const songs = filteredState.queue.songs || {};
const cleanedSongs: Record<string, QueueSong> = {};
for (const [id, song] of Object.entries(songs)) {
if (allQueueIds.has(id)) {
cleanedSongs[id] = song;
}
}
filteredState.queue = {
...filteredState.queue,
songs: cleanedSongs,
};
}
return filteredState; return filteredState;
}, },
storage: createJSONStorage(() => idbStateStorage), storage: createJSONStorage(() => idbStateStorage),
@@ -1531,25 +1555,73 @@ export const usePlayerQueue = () => {
return usePlayerStoreBase( return usePlayerStoreBase(
useShallow((state) => { useShallow((state) => {
const queueType = state.player.queueType; const queueType = state.player.queueType;
const songs = state.queue.songs;
switch (queueType) { switch (queueType) {
case PlayerQueueType.DEFAULT: { case PlayerQueueType.DEFAULT: {
const queue = state.queue.default; const queue = state.queue.default;
return queue.map((id) => state.queue.songs[id]); const result: QueueSong[] = [];
for (const id of queue) {
const song = songs[id];
if (song) result.push(song);
}
return result;
} }
case PlayerQueueType.PRIORITY: { case PlayerQueueType.PRIORITY: {
const priorityQueue = state.queue.priority; const priorityQueue = state.queue.priority;
return priorityQueue.map((id) => state.queue.songs[id]); const result: QueueSong[] = [];
for (const id of priorityQueue) {
const song = songs[id];
if (song) result.push(song);
}
return result;
} }
default: { default: {
const defaultQueue = state.queue.default; const defaultQueue = state.queue.default;
return defaultQueue.map((id) => state.queue.songs[id]); const result: QueueSong[] = [];
for (const id of defaultQueue) {
const song = songs[id];
if (song) result.push(song);
}
return result;
} }
} }
}), }),
); );
}; };
function cleanupOrphanedSongs(state: any): boolean {
const allQueueIds = new Set([
...state.queue.default,
...state.queue.priority,
...state.queue.shuffled,
]);
const songs = state.queue.songs;
const songIds = Object.keys(songs);
let hasOrphans = false;
const orphanedIds: string[] = [];
for (const songId of songIds) {
if (!allQueueIds.has(songId)) {
orphanedIds.push(songId);
hasOrphans = true;
}
}
if (hasOrphans) {
const cleanedSongs: Record<string, QueueSong> = {};
for (const songId of songIds) {
if (!orphanedIds.includes(songId)) {
cleanedSongs[songId] = songs[songId];
}
}
state.queue.songs = cleanedSongs;
}
return hasOrphans;
}
function getQueueType() { function getQueueType() {
const queueType: PlayerQueueType = usePlayerStore.getState().player.queueType; const queueType: PlayerQueueType = usePlayerStore.getState().player.queueType;
return queueType; return queueType;