mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
RandomLoop randomly selects from queue, causing the same song to play multiple times when the queue is small (~3 songs from personal_fm API). Random mode ensures each song plays once before repeating, relying on refillFm() to fetch fresh songs before exhaustion. Also fix the proactive refill condition to use unplayedCount() instead of size() - getCurrentIndex(), which was meaningless for non-sequential play modes.
170 lines
4.3 KiB
TypeScript
170 lines
4.3 KiB
TypeScript
export enum PlayMode {
|
|
Sequential = "seq",
|
|
Loop = "loop",
|
|
Random = "random",
|
|
RandomLoop = "rloop",
|
|
}
|
|
|
|
export interface QueuedSong {
|
|
id: string;
|
|
name: string;
|
|
artist: string;
|
|
album: string;
|
|
platform: "netease" | "qq" | "bilibili" | "youtube";
|
|
url?: string; // resolved lazily at play time
|
|
coverUrl: string;
|
|
duration: number; // seconds
|
|
}
|
|
|
|
export class PlayQueue {
|
|
private songs: QueuedSong[] = [];
|
|
private currentIndex = -1;
|
|
private mode: PlayMode = PlayMode.Sequential;
|
|
private playedIndices = new Set<number>();
|
|
|
|
add(song: QueuedSong): void {
|
|
this.songs.push(song);
|
|
}
|
|
|
|
addMany(songs: QueuedSong[]): void {
|
|
this.songs.push(...songs);
|
|
}
|
|
|
|
remove(index: number): QueuedSong | null {
|
|
if (index < 0 || index >= this.songs.length) return null;
|
|
const [removed] = this.songs.splice(index, 1);
|
|
|
|
if (index < this.currentIndex) {
|
|
this.currentIndex--;
|
|
} else if (index === 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;
|
|
}
|
|
|
|
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;
|
|
this.playedIndices.add(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;
|
|
this.playedIndices.add(index);
|
|
return this.songs[index];
|
|
}
|
|
|
|
next(): QueuedSong | null {
|
|
if (this.songs.length === 0) return null;
|
|
|
|
switch (this.mode) {
|
|
case PlayMode.Sequential: {
|
|
const nextIndex = this.currentIndex + 1;
|
|
if (nextIndex >= this.songs.length) return null;
|
|
this.currentIndex = nextIndex;
|
|
return this.songs[nextIndex];
|
|
}
|
|
case PlayMode.Loop: {
|
|
this.currentIndex = (this.currentIndex + 1) % this.songs.length;
|
|
return this.songs[this.currentIndex];
|
|
}
|
|
case PlayMode.Random: {
|
|
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;
|
|
this.playedIndices.add(nextIndex);
|
|
return this.songs[nextIndex];
|
|
}
|
|
case PlayMode.RandomLoop: {
|
|
if (this.songs.length === 1) {
|
|
this.currentIndex = 0;
|
|
return this.songs[0];
|
|
}
|
|
let idx: number;
|
|
do {
|
|
idx = Math.floor(Math.random() * this.songs.length);
|
|
} while (idx === this.currentIndex);
|
|
this.currentIndex = idx;
|
|
return this.songs[idx];
|
|
}
|
|
}
|
|
}
|
|
|
|
prev(): QueuedSong | null {
|
|
if (this.songs.length === 0) return null;
|
|
const prevIndex = this.currentIndex - 1;
|
|
if (prevIndex < 0) {
|
|
// In Sequential mode, don't wrap around
|
|
if (this.mode === PlayMode.Sequential) return null;
|
|
this.currentIndex = this.songs.length - 1;
|
|
} else {
|
|
this.currentIndex = prevIndex;
|
|
}
|
|
this.playedIndices.add(this.currentIndex);
|
|
return this.songs[this.currentIndex];
|
|
}
|
|
|
|
current(): QueuedSong | null {
|
|
if (this.currentIndex < 0 || this.currentIndex >= this.songs.length)
|
|
return null;
|
|
return this.songs[this.currentIndex];
|
|
}
|
|
|
|
list(): QueuedSong[] {
|
|
return [...this.songs];
|
|
}
|
|
|
|
size(): number {
|
|
return this.songs.length;
|
|
}
|
|
|
|
isEmpty(): boolean {
|
|
return this.songs.length === 0;
|
|
}
|
|
|
|
getMode(): PlayMode {
|
|
return this.mode;
|
|
}
|
|
|
|
setMode(mode: PlayMode): void {
|
|
this.mode = mode;
|
|
this.playedIndices.clear();
|
|
if (this.currentIndex >= 0) {
|
|
this.playedIndices.add(this.currentIndex);
|
|
}
|
|
}
|
|
|
|
getCurrentIndex(): number {
|
|
return this.currentIndex;
|
|
}
|
|
|
|
/** Number of songs not yet played in Random mode. */
|
|
unplayedCount(): number {
|
|
return this.songs.length - this.playedIndices.size;
|
|
}
|
|
}
|