feat(spotify): orchestrate go-librespot backend from BotInstance (Stage 2 Task 7)

Construct one SpotifyController per bot (config.spotify + per-bot work/config
dirs under DATA_DIR, threaded via BotManager + index). resolveAndPlay now
routes spotify: sentinels through controller.ensureStarted/playTrack +
player.playPcmStream (falling back to the Stage-1 message when unavailable),
fences/pauses the sidecar on source transitions, advances via controller
"trackEnded", and delegates pause/resume/stop transport. Correction C4:
no re-attach on a spotify->spotify handoff (playPcmStream once across tracks,
no player.stop() — playPcmStream fences the prior ffmpeg internally); occupancy
auto-pause/resume + updateAutoPause + a new BotInstance.seek() (web seek route)
also delegate to the sidecar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.8 committed 2026-07-02 21:44:39 +08:00
1 parent 179e7c248a
commit b5b3585e77
5 files changed
+480 -13

No files matched your search

+8 -1
View File
@@ -1,5 +1,6 @@
import crypto from "node:crypto";
import { EventEmitter } from "node:events";
import path from "node:path";
import {
BotInstance,
type BotInstanceOptions,
@@ -77,6 +78,7 @@ export class BotManager extends EventEmitter {
private localProvider: MusicProvider;
private kugouProvider: MusicProvider;
private spotifyProvider: MusicProvider;
private spotifyDataDir: string;
private database: BotDatabase;
private config: BotConfig;
private logger: Logger;
@@ -96,7 +98,8 @@ export class BotManager extends EventEmitter {
configPath: string,
localProvider?: MusicProvider,
kugouProvider?: MusicProvider,
spotifyProvider?: MusicProvider
spotifyProvider?: MusicProvider,
spotifyDataDir?: string
) {
super();
this.neteaseProvider = neteaseProvider;
@@ -106,6 +109,7 @@ export class BotManager extends EventEmitter {
this.localProvider = localProvider ?? neteaseProvider;
this.kugouProvider = kugouProvider ?? neteaseProvider;
this.spotifyProvider = spotifyProvider ?? neteaseProvider;
this.spotifyDataDir = spotifyDataDir ?? path.join(process.cwd(), "data", "spotify");
// Let the local provider see which uploads are still referenced by any
// bot's queue, so it never deletes a file another queue/bot still needs.
const referenceable = this.localProvider as Partial<{
@@ -149,6 +153,7 @@ export class BotManager extends EventEmitter {
config: this.config,
logger: this.logger,
avatarStore: this.avatarStore,
spotifyDataDir: this.spotifyDataDir,
});
this.bots.set(id, bot);
@@ -290,6 +295,7 @@ export class BotManager extends EventEmitter {
config: this.config,
logger: this.logger,
avatarStore: this.avatarStore,
spotifyDataDir: this.spotifyDataDir,
});
this.bots.set(id, bot);
this.emit("botInstance", bot);
@@ -345,6 +351,7 @@ export class BotManager extends EventEmitter {
config: this.config,
logger: this.logger,
avatarStore: this.avatarStore,
spotifyDataDir: this.spotifyDataDir,
});
this.bots.set(saved.id, bot);