From 4eac2e4dde1f0fd1aa8469d3e3e30836dbb3fff1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 14:53:15 +0000 Subject: [PATCH] fix: Random mode now stops after all songs played instead of looping forever MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Random (shuffle) mode, the queue's next() method would return the same song indefinitely when only one song was in the playlist, and never terminate even with multiple songs. This happened because played songs were not tracked. Added a playedIndices Set to track which songs have already been played in Random mode. Once all songs have been played once, next() returns null to stop playback — matching the expected behavior where Random plays each song once in random order, while RandomLoop is the mode for infinite shuffling. https://claude.ai/code/session_01W3ZncxL5VfdZeB4qqYWDmY --- src/audio/queue.test.ts | 26 ++++++++++++++++++++++++++ src/audio/queue.ts | 35 +++++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/audio/queue.test.ts b/src/audio/queue.test.ts index e801014..e956d6e 100644 --- a/src/audio/queue.test.ts +++ b/src/audio/queue.test.ts @@ -150,6 +150,32 @@ describe("PlayQueue", () => { expect(next).not.toBeNull(); }); + it("random mode with single song returns null on next", () => { + queue.setMode(PlayMode.Random); + queue.add(makeSong("1")); + queue.play(); + expect(queue.next()).toBeNull(); + }); + + it("random mode plays each song exactly once then stops", () => { + queue.setMode(PlayMode.Random); + queue.add(makeSong("A")); + queue.add(makeSong("B")); + queue.add(makeSong("C")); + queue.play(); + const played = new Set(); + played.add(queue.current()!.id); + for (let i = 0; i < 3; i++) { + const song = queue.next(); + if (!song) break; + played.add(song.id); + } + // All 3 songs should have been played + expect(played).toEqual(new Set(["A", "B", "C"])); + // next() after all played should return null + expect(queue.next()).toBeNull(); + }); + it("random-loop mode never returns null", () => { queue.setMode(PlayMode.RandomLoop); queue.add(makeSong("1")); diff --git a/src/audio/queue.ts b/src/audio/queue.ts index 9c26ab0..7a6f36f 100644 --- a/src/audio/queue.ts +++ b/src/audio/queue.ts @@ -20,6 +20,7 @@ export class PlayQueue { private songs: QueuedSong[] = []; private currentIndex = -1; private mode: PlayMode = PlayMode.Sequential; + private playedIndices = new Set(); add(song: QueuedSong): void { this.songs.push(song); @@ -36,32 +37,36 @@ export class PlayQueue { if (index < this.currentIndex) { this.currentIndex--; } else if (index === this.currentIndex) { - // Move the pointer back by one so next() in sequential mode advances - // to the song that shifted into the removed slot. Without this, the - // shifted song is silently skipped because current() incorrectly - // returns it (even though the player is still on the removed song) - // and next() then increments past it. currentIndex may become -1, - // which is fine — it represents "no current song" and next() will - // pick index 0. this.currentIndex--; } + // Rebuild playedIndices to account for shifted indices + const newPlayed = new Set(); + for (const idx of this.playedIndices) { + if (idx === index) continue; + newPlayed.add(idx > index ? idx - 1 : idx); + } + this.playedIndices = newPlayed; + return removed; } clear(): void { this.songs = []; this.currentIndex = -1; + this.playedIndices.clear(); } play(): QueuedSong | null { if (this.songs.length === 0) return null; + this.playedIndices.clear(); this.currentIndex = 0; return this.songs[0]; } playAt(index: number): QueuedSong | null { if (index < 0 || index >= this.songs.length) return null; + this.playedIndices.clear(); this.currentIndex = index; return this.songs[index]; } @@ -81,11 +86,16 @@ export class PlayQueue { return this.songs[this.currentIndex]; } case PlayMode.Random: { - if (this.songs.length === 1) return this.songs[0]; - let nextIndex: number; - do { - nextIndex = Math.floor(Math.random() * this.songs.length); - } while (nextIndex === this.currentIndex && this.songs.length > 1); + if (this.currentIndex >= 0) { + this.playedIndices.add(this.currentIndex); + } + const unplayed: number[] = []; + for (let i = 0; i < this.songs.length; i++) { + if (!this.playedIndices.has(i)) unplayed.push(i); + } + if (unplayed.length === 0) return null; + const nextIndex = + unplayed[Math.floor(Math.random() * unplayed.length)]; this.currentIndex = nextIndex; return this.songs[nextIndex]; } @@ -141,6 +151,7 @@ export class PlayQueue { setMode(mode: PlayMode): void { this.mode = mode; + this.playedIndices.clear(); } getCurrentIndex(): number {