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 {