From 11c6a3f51b491150f8bb3a9ee67f2662f0e41aac Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Wed, 6 May 2026 16:34:32 +0800 Subject: [PATCH] feat(queue): addNext inserts a song to play right after current MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splices into currentIndex+1 and shifts both playedIndices and the history back-stack to keep references valid. Falls through to plain push when nothing is playing so the idle-bot "add → start playing" flow is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/audio/queue.test.ts | 74 +++++++++++++++++++++++++++++++++++++++++ src/audio/queue.ts | 28 ++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/audio/queue.test.ts b/src/audio/queue.test.ts index 2416527..d454ae9 100644 --- a/src/audio/queue.test.ts +++ b/src/audio/queue.test.ts @@ -386,4 +386,78 @@ describe("PlayQueue", () => { expect(count).toBe(50); }); }); + + describe("addNext", () => { + it("appends when queue is empty (no current)", () => { + queue.addNext(makeSong("a")); + expect(queue.size()).toBe(1); + expect(queue.list()[0].id).toBe("a"); + }); + + it("appends when nothing is currently playing (currentIndex < 0)", () => { + queue.add(makeSong("a")); + queue.add(makeSong("b")); + // No play() yet → currentIndex still -1 + queue.addNext(makeSong("c")); + expect(queue.list().map((s) => s.id)).toEqual(["a", "b", "c"]); + }); + + it("inserts at currentIndex+1 mid-queue", () => { + queue.add(makeSong("a")); + queue.add(makeSong("b")); + queue.add(makeSong("c")); + queue.add(makeSong("d")); + queue.play(); // current = 0 (a) + queue.next(); // current = 1 (b) + queue.addNext(makeSong("x")); + expect(queue.list().map((s) => s.id)).toEqual(["a", "b", "x", "c", "d"]); + expect(queue.current()?.id).toBe("b"); // current unchanged + }); + + it("makes the inserted song play next when next() is called", () => { + queue.setMode(PlayMode.Sequential); + queue.add(makeSong("a")); + queue.add(makeSong("b")); + queue.play(); // current = 0 (a) + queue.addNext(makeSong("x")); + expect(queue.next()?.id).toBe("x"); + }); + + it("shifts playedIndices entries > currentIndex by +1", () => { + queue.setMode(PlayMode.Random); + queue.add(makeSong("a")); + queue.add(makeSong("b")); + queue.add(makeSong("c")); + queue.add(makeSong("d")); + queue.playAt(2); // current = 2 (c), played = {2} + queue.playAt(3); // current = 3 (d), played = {2, 3} + queue.playAt(2); // current = 2 (c), played = {2, 3} + // Now insert after c — d's index 3 should become 4 + queue.addNext(makeSong("x")); + expect(queue.list().map((s) => s.id)).toEqual(["a", "b", "c", "x", "d"]); + // After addNext: currentIndex still 2; played should be {2, 4} + // (the previously-played 'd' is now at index 4) + // Verify by removing 'x' (index 3) — d should remain played at index 3 + queue.remove(3); + expect(queue.list().map((s) => s.id)).toEqual(["a", "b", "c", "d"]); + }); + + it("shifts history entries > currentIndex by +1", () => { + queue.setMode(PlayMode.Random); + queue.add(makeSong("a")); + queue.add(makeSong("b")); + queue.add(makeSong("c")); + queue.add(makeSong("d")); + queue.playAt(0); // current = 0 + queue.playAt(3); // current = 3 (d), history = [0] + queue.playAt(1); // current = 1 (b), history = [0, 3] + queue.addNext(makeSong("x")); + // Insert at index 2 → entries > 1 shift +1 → history becomes [0, 4] + // queue: [a, b, x, c, d]; d is now at index 4 + // prev → pop 4 → song at index 4 = d + expect(queue.prev()?.id).toBe("d"); + // prev again → pop 0 → song at index 0 = a + expect(queue.prev()?.id).toBe("a"); + }); + }); }); diff --git a/src/audio/queue.ts b/src/audio/queue.ts index 578dee1..8a57e13 100644 --- a/src/audio/queue.ts +++ b/src/audio/queue.ts @@ -40,6 +40,34 @@ export class PlayQueue { this.songs.push(...songs); } + /** + * Insert a song to play immediately after the current one. Falls + * through to plain push when nothing is playing yet (currentIndex < 0 + * or queue empty), so the existing "add → idle bot starts playing" + * flow continues to work. + * + * Shifts playedIndices and history entries > currentIndex by +1 so + * their references stay valid after the splice. + */ + addNext(song: QueuedSong): void { + if (this.currentIndex < 0 || this.songs.length === 0) { + this.songs.push(song); + return; + } + const insertAt = this.currentIndex + 1; + this.songs.splice(insertAt, 0, song); + + const shifted = new Set(); + for (const i of this.playedIndices) { + shifted.add(i > this.currentIndex ? i + 1 : i); + } + this.playedIndices = shifted; + + this.history = this.history.map((i) => + i > this.currentIndex ? i + 1 : i, + ); + } + remove(index: number): QueuedSong | null { if (index < 0 || index >= this.songs.length) return null; const [removed] = this.songs.splice(index, 1);