diff --git a/src/components/player/RadioController.tsx b/src/components/player/RadioController.tsx index fe48182..b7f24ca 100644 --- a/src/components/player/RadioController.tsx +++ b/src/components/player/RadioController.tsx @@ -8,6 +8,11 @@ import { pickToEntry } from '../../lib/radio'; // the end of the queue. const LOOKAHEAD = 3; +// Only the most recent tracks are sent as "don't repeat these" — bounding the +// request so a long radio session doesn't ship an ever-growing exclude list +// (and a huge NOT IN on the server). Recent-repeat avoidance is what matters. +const EXCLUDE_WINDOW = 150; + /** * Headless controller: while a radio drives the queue, top it up as it drains * (§6.5 / B6 infinite feed). Mounted once in AppShell. A failed prefetch just @@ -34,7 +39,7 @@ export function RadioController() { fromLikes: radio.fromLikes, exploration: radio.exploration, count: 10, - excludeIds: entries.map((e) => e.trackId), + excludeIds: entries.slice(-EXCLUDE_WINDOW).map((e) => e.trackId), }).unwrap(); if (res.picks.length > 0) { dispatch(appendToQueue(res.picks.map(pickToEntry))); diff --git a/src/store/slices/queue.ts b/src/store/slices/queue.ts index 1f20134..a5843e8 100644 --- a/src/store/slices/queue.ts +++ b/src/store/slices/queue.ts @@ -38,6 +38,12 @@ export interface QueueState { radio: RadioContext | null; } +// An infinite radio keeps appending, so cap the backlog: once the queue grows +// past QUEUE_CAP, drop the oldest *already-played* entries (never the current +// track or the look-ahead), keeping at least KEEP_BEHIND for prev-navigation. +const QUEUE_CAP = 300; +const KEEP_BEHIND = 50; + export const queueInitialState: QueueState = { entries: [], currentIndex: -1, @@ -83,6 +89,17 @@ export const queueSlice = createSlice({ }, appendToQueue(state, action: PayloadAction) { state.entries.push(...action.payload); + if (state.entries.length > QUEUE_CAP) { + // Only trim entries safely behind the current one. + const drop = Math.min( + state.entries.length - QUEUE_CAP, + Math.max(0, state.currentIndex - KEEP_BEHIND), + ); + if (drop > 0) { + state.entries.splice(0, drop); + state.currentIndex -= drop; + } + } }, setExploration(state, action: PayloadAction) { if (state.radio) state.radio.exploration = action.payload;