mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Inert in this commit — no callers yet. Sets up the back-stack used by the upcoming history-aware prev rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
180 lines
4.6 KiB
TypeScript
180 lines
4.6 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>();
|
|
private history: number[] = [];
|
|
private static readonly HISTORY_LIMIT = 50;
|
|
|
|
private pushHistory(idx: number): void {
|
|
if (idx < 0 || idx >= this.songs.length) return;
|
|
this.history.push(idx);
|
|
if (this.history.length > PlayQueue.HISTORY_LIMIT) {
|
|
this.history.shift();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|