mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Merge pull request #27 from ZHANGTIANYAO1/claude/fix-shuffle-loop-bug-FrIbI
Fix random mode to track played songs and prevent duplicates
This commit is contained in:
2 files changed
+122
-12
No files matched your search
@@ -150,6 +150,101 @@ describe("PlayQueue", () => {
|
|||||||
expect(next).not.toBeNull();
|
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<string>();
|
||||||
|
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 mode: removing currently-playing song does not skip others", () => {
|
||||||
|
queue.setMode(PlayMode.Random);
|
||||||
|
queue.add(makeSong("A"));
|
||||||
|
queue.add(makeSong("B"));
|
||||||
|
queue.add(makeSong("C"));
|
||||||
|
queue.add(makeSong("D"));
|
||||||
|
queue.play(); // plays A (index 0)
|
||||||
|
const second = queue.next()!; // plays some song
|
||||||
|
// Remove the currently-playing song
|
||||||
|
const curIdx = queue.getCurrentIndex();
|
||||||
|
queue.remove(curIdx);
|
||||||
|
// Remaining songs (excluding A and the removed song) should all be reachable
|
||||||
|
const played = new Set<string>();
|
||||||
|
played.add("A"); // already played via play()
|
||||||
|
played.add(second.id); // played and then removed
|
||||||
|
let song = queue.next();
|
||||||
|
while (song) {
|
||||||
|
played.add(song.id);
|
||||||
|
song = queue.next();
|
||||||
|
}
|
||||||
|
// All 4 original songs should have been played or accounted for
|
||||||
|
expect(played).toEqual(new Set(["A", "B", "C", "D"]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("random mode: prev does not cause duplicate plays", () => {
|
||||||
|
queue.setMode(PlayMode.Random);
|
||||||
|
queue.add(makeSong("A"));
|
||||||
|
queue.add(makeSong("B"));
|
||||||
|
queue.add(makeSong("C"));
|
||||||
|
queue.play(); // plays A
|
||||||
|
queue.next(); // plays B or C
|
||||||
|
queue.prev(); // go back — this song is now marked as played
|
||||||
|
// Exhaust remaining songs
|
||||||
|
const ids: string[] = [];
|
||||||
|
let song = queue.next();
|
||||||
|
while (song) {
|
||||||
|
ids.push(song.id);
|
||||||
|
song = queue.next();
|
||||||
|
}
|
||||||
|
// No song ID should appear more than once across the entire session
|
||||||
|
expect(new Set(ids).size).toBe(ids.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("random mode: adding song mid-playback includes the new song", () => {
|
||||||
|
queue.setMode(PlayMode.Random);
|
||||||
|
queue.add(makeSong("A"));
|
||||||
|
queue.add(makeSong("B"));
|
||||||
|
queue.play(); // plays A
|
||||||
|
queue.next(); // plays B
|
||||||
|
// Add a new song while all existing songs have been played
|
||||||
|
queue.add(makeSong("C"));
|
||||||
|
const song = queue.next();
|
||||||
|
expect(song).not.toBeNull();
|
||||||
|
expect(song!.id).toBe("C");
|
||||||
|
// After C, should stop
|
||||||
|
expect(queue.next()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("random mode: setMode preserves current song as played", () => {
|
||||||
|
queue.add(makeSong("A"));
|
||||||
|
queue.add(makeSong("B"));
|
||||||
|
queue.play(); // plays A in sequential mode
|
||||||
|
queue.setMode(PlayMode.Random); // switch to random — A should be marked played
|
||||||
|
// next() should only return B, never A again
|
||||||
|
const song = queue.next();
|
||||||
|
expect(song?.id).toBe("B");
|
||||||
|
expect(queue.next()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("random-loop mode never returns null", () => {
|
it("random-loop mode never returns null", () => {
|
||||||
queue.setMode(PlayMode.RandomLoop);
|
queue.setMode(PlayMode.RandomLoop);
|
||||||
queue.add(makeSong("1"));
|
queue.add(makeSong("1"));
|
||||||
|
|||||||
+27
-12
@@ -20,6 +20,7 @@ export class PlayQueue {
|
|||||||
private songs: QueuedSong[] = [];
|
private songs: QueuedSong[] = [];
|
||||||
private currentIndex = -1;
|
private currentIndex = -1;
|
||||||
private mode: PlayMode = PlayMode.Sequential;
|
private mode: PlayMode = PlayMode.Sequential;
|
||||||
|
private playedIndices = new Set<number>();
|
||||||
|
|
||||||
add(song: QueuedSong): void {
|
add(song: QueuedSong): void {
|
||||||
this.songs.push(song);
|
this.songs.push(song);
|
||||||
@@ -36,33 +37,39 @@ export class PlayQueue {
|
|||||||
if (index < this.currentIndex) {
|
if (index < this.currentIndex) {
|
||||||
this.currentIndex--;
|
this.currentIndex--;
|
||||||
} else if (index === 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--;
|
this.currentIndex--;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rebuild playedIndices to account for shifted indices
|
||||||
|
const newPlayed = new Set<number>();
|
||||||
|
for (const idx of this.playedIndices) {
|
||||||
|
if (idx === index) continue;
|
||||||
|
newPlayed.add(idx > index ? idx - 1 : idx);
|
||||||
|
}
|
||||||
|
this.playedIndices = newPlayed;
|
||||||
|
|
||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.songs = [];
|
this.songs = [];
|
||||||
this.currentIndex = -1;
|
this.currentIndex = -1;
|
||||||
|
this.playedIndices.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
play(): QueuedSong | null {
|
play(): QueuedSong | null {
|
||||||
if (this.songs.length === 0) return null;
|
if (this.songs.length === 0) return null;
|
||||||
|
this.playedIndices.clear();
|
||||||
this.currentIndex = 0;
|
this.currentIndex = 0;
|
||||||
|
this.playedIndices.add(0);
|
||||||
return this.songs[0];
|
return this.songs[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
playAt(index: number): QueuedSong | null {
|
playAt(index: number): QueuedSong | null {
|
||||||
if (index < 0 || index >= this.songs.length) return null;
|
if (index < 0 || index >= this.songs.length) return null;
|
||||||
|
this.playedIndices.clear();
|
||||||
this.currentIndex = index;
|
this.currentIndex = index;
|
||||||
|
this.playedIndices.add(index);
|
||||||
return this.songs[index];
|
return this.songs[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,12 +88,15 @@ export class PlayQueue {
|
|||||||
return this.songs[this.currentIndex];
|
return this.songs[this.currentIndex];
|
||||||
}
|
}
|
||||||
case PlayMode.Random: {
|
case PlayMode.Random: {
|
||||||
if (this.songs.length === 1) return this.songs[0];
|
const unplayed: number[] = [];
|
||||||
let nextIndex: number;
|
for (let i = 0; i < this.songs.length; i++) {
|
||||||
do {
|
if (!this.playedIndices.has(i)) unplayed.push(i);
|
||||||
nextIndex = Math.floor(Math.random() * this.songs.length);
|
}
|
||||||
} while (nextIndex === this.currentIndex && this.songs.length > 1);
|
if (unplayed.length === 0) return null;
|
||||||
|
const nextIndex =
|
||||||
|
unplayed[Math.floor(Math.random() * unplayed.length)];
|
||||||
this.currentIndex = nextIndex;
|
this.currentIndex = nextIndex;
|
||||||
|
this.playedIndices.add(nextIndex);
|
||||||
return this.songs[nextIndex];
|
return this.songs[nextIndex];
|
||||||
}
|
}
|
||||||
case PlayMode.RandomLoop: {
|
case PlayMode.RandomLoop: {
|
||||||
@@ -114,6 +124,7 @@ export class PlayQueue {
|
|||||||
} else {
|
} else {
|
||||||
this.currentIndex = prevIndex;
|
this.currentIndex = prevIndex;
|
||||||
}
|
}
|
||||||
|
this.playedIndices.add(this.currentIndex);
|
||||||
return this.songs[this.currentIndex];
|
return this.songs[this.currentIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +152,10 @@ export class PlayQueue {
|
|||||||
|
|
||||||
setMode(mode: PlayMode): void {
|
setMode(mode: PlayMode): void {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
|
this.playedIndices.clear();
|
||||||
|
if (this.currentIndex >= 0) {
|
||||||
|
this.playedIndices.add(this.currentIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentIndex(): number {
|
getCurrentIndex(): number {
|
||||||
|
|||||||
Reference in new issue
Block a user