ziplayer 0.2.6 → 0.2.7-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/AI-Guide.md +607 -0
  2. package/README.md +513 -196
  3. package/dist/extensions/BaseExtension.d.ts +1 -0
  4. package/dist/extensions/BaseExtension.d.ts.map +1 -1
  5. package/dist/extensions/BaseExtension.js.map +1 -1
  6. package/dist/extensions/index.d.ts +38 -3
  7. package/dist/extensions/index.d.ts.map +1 -1
  8. package/dist/extensions/index.js +259 -41
  9. package/dist/extensions/index.js.map +1 -1
  10. package/dist/persistence/PersistenceManager.d.ts +61 -0
  11. package/dist/persistence/PersistenceManager.d.ts.map +1 -0
  12. package/dist/persistence/PersistenceManager.js +551 -0
  13. package/dist/persistence/PersistenceManager.js.map +1 -0
  14. package/dist/plugins/BasePlugin.js +1 -1
  15. package/dist/plugins/BasePlugin.js.map +1 -1
  16. package/dist/plugins/index.d.ts +19 -4
  17. package/dist/plugins/index.d.ts.map +1 -1
  18. package/dist/plugins/index.js +273 -146
  19. package/dist/plugins/index.js.map +1 -1
  20. package/dist/structures/FilterManager.js +3 -3
  21. package/dist/structures/FilterManager.js.map +1 -1
  22. package/dist/structures/Player.d.ts +64 -14
  23. package/dist/structures/Player.d.ts.map +1 -1
  24. package/dist/structures/Player.js +344 -91
  25. package/dist/structures/Player.js.map +1 -1
  26. package/dist/structures/PlayerManager.d.ts +125 -91
  27. package/dist/structures/PlayerManager.d.ts.map +1 -1
  28. package/dist/structures/PlayerManager.js +406 -111
  29. package/dist/structures/PlayerManager.js.map +1 -1
  30. package/dist/structures/Queue.d.ts +136 -31
  31. package/dist/structures/Queue.d.ts.map +1 -1
  32. package/dist/structures/Queue.js +265 -46
  33. package/dist/structures/Queue.js.map +1 -1
  34. package/dist/types/index.d.ts +39 -6
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/types/index.js +1 -0
  37. package/dist/types/index.js.map +1 -1
  38. package/dist/types/persistence.d.ts +55 -0
  39. package/dist/types/persistence.d.ts.map +1 -0
  40. package/dist/types/persistence.js +3 -0
  41. package/dist/types/persistence.js.map +1 -0
  42. package/package.json +47 -46
  43. package/src/extensions/BaseExtension.ts +36 -35
  44. package/src/extensions/index.ts +473 -190
  45. package/src/index.ts +16 -16
  46. package/src/persistence/PersistenceManager.ts +572 -0
  47. package/src/plugins/BasePlugin.ts +27 -27
  48. package/src/plugins/index.ts +403 -236
  49. package/src/structures/FilterManager.ts +303 -303
  50. package/src/structures/Player.ts +1962 -1689
  51. package/src/structures/PlayerManager.ts +788 -416
  52. package/src/structures/Queue.ts +599 -354
  53. package/src/types/index.ts +406 -373
  54. package/src/types/persistence.ts +65 -0
  55. package/src/types/plugin.ts +1 -1
  56. package/src/utils/timeout.ts +10 -10
  57. package/tsconfig.json +22 -23
@@ -1,416 +1,788 @@
1
- import { EventEmitter } from "events";
2
- import { Player } from "./Player";
3
- import { PlayerManagerOptions, PlayerOptions, Track, SourcePlugin, SearchResult, ManagerEvents } from "../types";
4
- import type { BaseExtension } from "../extensions";
5
- import { withTimeout } from "../utils/timeout";
6
-
7
- const GLOBAL_MANAGER_KEY: symbol = Symbol.for("ziplayer.PlayerManager.instance");
8
- export const getGlobalManager = (): PlayerManager | null => {
9
- try {
10
- const instance = (globalThis as any)[GLOBAL_MANAGER_KEY];
11
- if (!instance) {
12
- return null;
13
- }
14
- return instance as PlayerManager;
15
- } catch (error) {
16
- console.error("[PlayerManager] Error getting global instance:", error);
17
- return null;
18
- }
19
- };
20
- const setGlobalManager = (instance: PlayerManager): void => {
21
- try {
22
- (globalThis as any)[GLOBAL_MANAGER_KEY] = instance;
23
- } catch (error) {
24
- console.error("[PlayerManager] Error setting global instance:", error);
25
- }
26
- };
27
-
28
- export declare interface PlayerManager {
29
- on<K extends keyof ManagerEvents>(event: K, listener: (...args: ManagerEvents[K]) => void): this;
30
- emit<K extends keyof ManagerEvents>(event: K, ...args: ManagerEvents[K]): boolean;
31
- }
32
-
33
- /**
34
- * The main class for managing players across multiple Discord guilds.
35
- *
36
- * @example
37
- * // Basic setup with plugins and extensions
38
- * const manager = new PlayerManager({
39
- * plugins: [
40
- * new YouTubePlugin(),
41
- * new SoundCloudPlugin(),
42
- * new SpotifyPlugin(),
43
- * new TTSPlugin({ defaultLang: "en" })
44
- * ],
45
- * extensions: [
46
- * new voiceExt(null, { lang: "en-US" }),
47
- * new lavalinkExt(null, {
48
- * nodes: [{ host: "localhost", port: 2333, password: "youshallnotpass" }]
49
- * })
50
- * ],
51
- * extractorTimeout: 10000
52
- * });
53
- *
54
- * // Create a player for a guild
55
- * const player = await manager.create(guildId, {
56
- * tts: { interrupt: true, volume: 1 },
57
- * leaveOnEnd: true,
58
- * leaveTimeout: 30000
59
- * });
60
- *
61
- * // Get existing player
62
- * const existingPlayer = manager.get(guildId);
63
- * if (existingPlayer) {
64
- * await existingPlayer.play("Never Gonna Give You Up", userId);
65
- * }
66
- */
67
- export class PlayerManager extends EventEmitter {
68
- private static instance: PlayerManager | null = null;
69
- private players: Map<string, Player> = new Map();
70
- static async default(opt?: PlayerOptions): Promise<Player> {
71
- let globaldef = getGlobalManager();
72
- if (!globaldef) {
73
- globaldef = new PlayerManager({});
74
- }
75
- return await globaldef.create("default", opt);
76
- }
77
- private plugins: SourcePlugin[];
78
- private extensions: any[];
79
- private B_debug: boolean = false;
80
- private extractorTimeout: number = 10000;
81
-
82
- private debug(message?: any, ...optionalParams: any[]): void {
83
- if (this.listenerCount("debug") > 0) {
84
- this.emit("debug", message, ...optionalParams);
85
- if (!this.B_debug) {
86
- this.B_debug = true;
87
- }
88
- }
89
- }
90
-
91
- constructor(options: PlayerManagerOptions = {}) {
92
- super();
93
- this.plugins = [];
94
- const provided = options.plugins || [];
95
- for (const p of provided as any[]) {
96
- try {
97
- if (p && typeof p === "object") {
98
- this.plugins.push(p as SourcePlugin);
99
- } else if (typeof p === "function") {
100
- const instance = new (p as any)();
101
- this.plugins.push(instance as SourcePlugin);
102
- }
103
- } catch (e) {
104
- this.debug(`[PlayerManager] Failed to init plugin:`, e);
105
- }
106
- }
107
- this.extensions = options.extensions || [];
108
-
109
- setGlobalManager(this);
110
- }
111
-
112
- private withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
113
- const timeout = this.extractorTimeout;
114
- return Promise.race([promise, new Promise<never>((_, reject) => setTimeout(() => reject(new Error(message)), timeout))]);
115
- }
116
-
117
- private resolveGuildId(guildOrId: string | { id: string }): string {
118
- if (typeof guildOrId === "string") return guildOrId;
119
- if (guildOrId && typeof guildOrId === "object" && "id" in guildOrId) return guildOrId.id;
120
- throw new Error("Invalid guild or guildId provided.");
121
- }
122
-
123
- /**
124
- * Create a new player for a guild
125
- *
126
- * @param {string | {id: string}} guildOrId - Guild ID or guild object
127
- * @param {PlayerOptions} options - Player configuration options
128
- * @returns {Promise<Player>} The created player instance
129
- *
130
- * @example
131
- * // Create player with basic options
132
- * const player = await manager.create(guildId, {
133
- * tts: { interrupt: true, volume: 1 },
134
- * leaveOnEnd: true,
135
- * leaveTimeout: 30000
136
- * });
137
- *
138
- * // Create player with advanced options
139
- * const advancedPlayer = await manager.create(guild, {
140
- * volume: 0.8,
141
- * quality: "high",
142
- * selfDeaf: false,
143
- * selfMute: false,
144
- * tts: {
145
- * createPlayer: true,
146
- * interrupt: true,
147
- * volume: 1.0,
148
- * Max_Time_TTS: 30000
149
- * },
150
- * userdata: { customData: "example" }
151
- * });
152
- *
153
- * // Connect and play immediately
154
- * await player.connect(voiceChannel);
155
- * await player.play("Never Gonna Give You Up", userId);
156
- */
157
-
158
- async create(guildOrId: string | { id: string }, options?: PlayerOptions): Promise<Player> {
159
- const guildId = this.resolveGuildId(guildOrId);
160
- if (this.players.has(guildId)) {
161
- return this.players.get(guildId)!;
162
- }
163
-
164
- this.debug(`[PlayerManager] Creating player for guildId: ${guildId}`);
165
- const player = new Player(guildId, options, this);
166
- this.plugins.forEach((plugin) => player.addPlugin(plugin));
167
-
168
- let extsToActivate: any[] = [];
169
- const optExts = (options as any)?.extensions as any[] | string[] | undefined;
170
- if (Array.isArray(optExts)) {
171
- if (optExts.length === 0) {
172
- extsToActivate = [];
173
- } else if (typeof optExts[0] === "string") {
174
- const wanted = new Set(optExts as string[]);
175
- extsToActivate = this.extensions.filter((ext) => {
176
- const name = typeof ext === "function" ? ext.name : ext?.name;
177
- return !!name && wanted.has(name);
178
- });
179
- } else {
180
- extsToActivate = optExts;
181
- }
182
- }
183
-
184
- for (const ext of extsToActivate) {
185
- let instance = ext;
186
- if (typeof ext === "function") {
187
- try {
188
- instance = new ext(player);
189
- } catch (e) {
190
- this.debug(`[PlayerManager] Extension constructor error:`, e);
191
- continue;
192
- }
193
- }
194
- if (instance && typeof instance === "object") {
195
- const extInstance = instance as BaseExtension;
196
- if ("player" in extInstance && !extInstance.player) extInstance.player = player;
197
- player.attachExtension(extInstance);
198
- if (typeof extInstance.active === "function") {
199
- let activated: boolean | void = true;
200
- try {
201
- activated = await withTimeout(
202
- Promise.resolve(extInstance.active({ manager: this, player })),
203
- player.options.extractorTimeout ?? 15000,
204
- `Extension ${extInstance?.name} activation timed out`,
205
- );
206
- this.debug(`[PlayerManager] Extension ${extInstance?.name} active`);
207
- } catch (e) {
208
- activated = false;
209
- this.debug(`[PlayerManager] Extension activation error:`, e);
210
- }
211
- if (activated === false) {
212
- player.detachExtension(extInstance);
213
- continue;
214
- }
215
- }
216
- }
217
- }
218
-
219
- // Forward all player events
220
- player.on("willPlay", (track, tracks) => this.emit("willPlay", player, track as Track, tracks as Track[]));
221
- player.on("trackStart", (track) => this.emit("trackStart", player, track as Track));
222
- player.on("trackEnd", (track) => this.emit("trackEnd", player, track as Track));
223
- player.on("queueEnd", () => this.emit("queueEnd", player));
224
- player.on("playerError", (error, track) => this.emit("playerError", player, error, track as Track));
225
- player.on("connectionError", (error) => this.emit("connectionError", player, error));
226
- player.on("volumeChange", (old, volume) => this.emit("volumeChange", player, old as number, volume as number));
227
- player.on("queueAdd", (track) => this.emit("queueAdd", player, track as Track));
228
- player.on("queueAddList", (tracks) => this.emit("queueAddList", player, tracks as Track[]));
229
- player.on("queueRemove", (track, index) => this.emit("queueRemove", player, track as Track, index));
230
- player.on("playerPause", (track) => this.emit("playerPause", player, track as Track));
231
- player.on("playerResume", (track) => this.emit("playerResume", player, track as Track));
232
- player.on("playerStop", () => this.emit("playerStop", player));
233
- player.on("playerDestroy", () => {
234
- this.emit("playerDestroy", player);
235
- this.players.delete(guildId);
236
- });
237
- player.on("ttsStart", (payload) => this.emit("ttsStart", player, payload));
238
- player.on("ttsEnd", () => this.emit("ttsEnd", player));
239
- player.on("debug", (...args) => {
240
- if (this.listenerCount("debug") > 0) {
241
- this.emit("debug", ...args);
242
- }
243
- });
244
-
245
- this.players.set(guildId, player);
246
- return player;
247
- }
248
-
249
- /**
250
- * Get an existing player for a guild
251
- *
252
- * @param {string | {id: string}} guildOrId - Guild ID or guild object
253
- * @returns {Player | undefined} The player instance or undefined if not found
254
- * @example
255
- * // Get player by guild ID
256
- * const player = manager.get(guildId);
257
- * if (player) {
258
- * await player.play("Never Gonna Give You Up", userId);
259
- * } else {
260
- * console.log("No player found for this guild");
261
- * }
262
- *
263
- * // Get player by guild object
264
- * const playerFromGuild = manager.get(guild);
265
- * if (playerFromGuild) {
266
- * playerFromGuild.setVolume(0.5);
267
- * }
268
- *
269
- * // Check if player exists before using
270
- * const existingPlayer = manager.get(guildId);
271
- * if (existingPlayer && existingPlayer.playing) {
272
- * existingPlayer.pause();
273
- * }
274
- */
275
-
276
- get(guildOrId: string | { id: string }): Player | undefined {
277
- const guildId = this.resolveGuildId(guildOrId);
278
- return this.players.get(guildId);
279
- }
280
-
281
- /**
282
- * Get an existing player for a guild
283
- *
284
- * @param {string | {id: string}} guildOrId - Guild ID or guild object
285
- * @returns {Player | undefined} The player instance or undefined
286
- * @example
287
- * const player = manager.get(guildId);
288
- * if (player) {
289
- * await player.play("song name", userId);
290
- * }
291
- */
292
- getPlayer(guildOrId: string | { id: string }): Player | undefined {
293
- const guildId = this.resolveGuildId(guildOrId);
294
- return this.players.get(guildId);
295
- }
296
-
297
- /**
298
- * Get all players
299
- *
300
- * @returns {Player[]} All player instances
301
- * @example
302
- * const players = manager.getall();
303
- * console.log(`Players: ${players.length}`);
304
- */
305
- getall(): Player[] | [] {
306
- return Array.from(this.players.values());
307
- }
308
-
309
- /**
310
- * Destroy a player and clean up resources
311
- *
312
- * @param {string | {id: string}} guildOrId - Guild ID or guild object
313
- * @returns {boolean} True if player was destroyed, false if not found
314
- * @example
315
- * // Destroy player by guild ID
316
- * const destroyed = manager.delete(guildId);
317
- * if (destroyed) {
318
- * console.log("Player destroyed successfully");
319
- * } else {
320
- * console.log("No player found to destroy");
321
- * }
322
- *
323
- * // Destroy player by guild object
324
- * const destroyedFromGuild = manager.delete(guild);
325
- * console.log(`Player destroyed: ${destroyedFromGuild}`);
326
- *
327
- * // Clean up all players
328
- * for (const [guildId, player] of manager.players) {
329
- * const destroyed = manager.delete(guildId);
330
- * console.log(`Destroyed player for ${guildId}: ${destroyed}`);
331
- * }
332
- */
333
- delete(guildOrId: string | { id: string }): boolean {
334
- const guildId = this.resolveGuildId(guildOrId);
335
- const player = this.players.get(guildId);
336
- if (player) {
337
- this.debug(`[PlayerManager] Deleting player for guildId: ${guildId}`);
338
- player.destroy();
339
- return this.players.delete(guildId);
340
- }
341
- return false;
342
- }
343
-
344
- /**
345
- * Check if a player exists for a guild
346
- *
347
- * @param {string | {id: string}} guildOrId - Guild ID or guild object
348
- * @returns {boolean} True if player exists, false if not
349
- * @example
350
- * const exists = manager.has(guildId);
351
- * console.log(`Player exists: ${exists}`);
352
- */
353
- has(guildOrId: string | { id: string }): boolean {
354
- const guildId = this.resolveGuildId(guildOrId);
355
- return this.players.has(guildId);
356
- }
357
-
358
- get size(): number {
359
- return this.players.size;
360
- }
361
-
362
- get debugEnabled(): boolean {
363
- return this.B_debug;
364
- }
365
- /**
366
- * Destroy all players
367
- *
368
- * @returns {void}
369
- * @example
370
- * manager.destroy();
371
- * console.log(`All players destroyed`);
372
- */
373
- destroy(): void {
374
- this.debug(`[PlayerManager] Destroying all players`);
375
- for (const player of this.players.values()) {
376
- player.destroy();
377
- }
378
- this.players.clear();
379
- this.removeAllListeners();
380
- }
381
-
382
- /**
383
- * Search using registered plugins without creating a Player.
384
- *
385
- * @param {string} query - The query to search for
386
- * @param {string} requestedBy - The user ID who requested the search
387
- * @returns {Promise<SearchResult>} The search result
388
- * @example
389
- * const result = await manager.search("Never Gonna Give You Up", userId);
390
- * console.log(`Search result: ${result.tracks.length} tracks`);
391
- */
392
- async search(query: string, requestedBy: string): Promise<SearchResult> {
393
- this.debug(`[PlayerManager] Search called with query: ${query}, requestedBy: ${requestedBy}`);
394
- const plugin = this.plugins.find((p) => p.canHandle(query));
395
- if (!plugin) {
396
- this.debug(`[PlayerManager] No plugin found to handle: ${query}`);
397
- throw new Error(`No plugin found to handle: ${query}`);
398
- }
399
-
400
- try {
401
- return await this.withTimeout(plugin.search(query, requestedBy), "Search operation timed out");
402
- } catch (error) {
403
- this.debug(`[PlayerManager] Search error:`, error);
404
- throw error as Error;
405
- }
406
- }
407
- }
408
-
409
- export function getInstance(): PlayerManager | null {
410
- const globalInst = getGlobalManager();
411
- if (!globalInst) {
412
- console.error("[PlayerManager] Global instance not found, make sure to initialize with new PlayerManager(options)");
413
- return null;
414
- }
415
- return globalInst;
416
- }
1
+ import { EventEmitter } from "events";
2
+ import { Player } from "./Player";
3
+ import {
4
+ PlayerManagerOptions,
5
+ PlayerOptions,
6
+ Track,
7
+ SourcePlugin,
8
+ SearchResult,
9
+ ManagerEvents,
10
+ PlayerStats,
11
+ PersistenceOptions,
12
+ } from "../types";
13
+ import type { BaseExtension } from "../extensions";
14
+ import { withTimeout } from "../utils/timeout";
15
+ import { PersistenceManager } from "../persistence/PersistenceManager";
16
+
17
+ const GLOBAL_MANAGER_KEY: symbol = Symbol.for("ziplayer.PlayerManager.instance");
18
+
19
+ export const getGlobalManager = (): PlayerManager | null => {
20
+ try {
21
+ const instance = (globalThis as any)[GLOBAL_MANAGER_KEY];
22
+ if (!instance) {
23
+ return null;
24
+ }
25
+ return instance as PlayerManager;
26
+ } catch (error) {
27
+ console.error("[PlayerManager] Error getting global instance:", error);
28
+ return null;
29
+ }
30
+ };
31
+
32
+ const setGlobalManager = (instance: PlayerManager): void => {
33
+ try {
34
+ (globalThis as any)[GLOBAL_MANAGER_KEY] = instance;
35
+ } catch (error) {
36
+ console.error("[PlayerManager] Error setting global instance:", error);
37
+ }
38
+ };
39
+
40
+ export declare interface PlayerManager {
41
+ on<K extends keyof ManagerEvents>(event: K, listener: (...args: ManagerEvents[K]) => void): this;
42
+ emit<K extends keyof ManagerEvents>(event: K, ...args: ManagerEvents[K]): boolean;
43
+ }
44
+
45
+ interface ManagerCacheEntry<T> {
46
+ data: T;
47
+ timestamp: number;
48
+ expiresAt: number;
49
+ }
50
+
51
+ /**
52
+ * The main class for managing players across multiple Discord guilds.
53
+ *
54
+ * @example
55
+ * // Basic setup with plugins and extensions
56
+ * const manager = new PlayerManager({
57
+ * plugins: [
58
+ * new YouTubePlugin(),
59
+ * new SoundCloudPlugin(),
60
+ * new SpotifyPlugin(),
61
+ * new TTSPlugin({ defaultLang: "en" })
62
+ * ],
63
+ * extensions: [
64
+ * new voiceExt(null, { lang: "en-US" }),
65
+ * new lavalinkExt(null, {
66
+ * nodes: [{ host: "localhost", port: 2333, password: "youshallnotpass" }]
67
+ * })
68
+ * ],
69
+ * extractorTimeout: 10000,
70
+ * autoCleanup: true,
71
+ * cleanupInterval: 60000
72
+ * });
73
+ *
74
+ * // Create a player for a guild
75
+ * const player = await manager.create(guildId, {
76
+ * tts: { interrupt: true, volume: 1 },
77
+ * leaveOnEnd: true,
78
+ * leaveTimeout: 30000
79
+ * });
80
+ *
81
+ * // Get existing player
82
+ * const existingPlayer = manager.get(guildId);
83
+ * if (existingPlayer) {
84
+ * await existingPlayer.play("Never Gonna Give You Up", userId);
85
+ * }
86
+ */
87
+ export class PlayerManager extends EventEmitter {
88
+ private static instance: PlayerManager | null = null;
89
+ private players: Map<string, Player> = new Map();
90
+ private searchCache: Map<string, ManagerCacheEntry<SearchResult>>;
91
+ private readonly SEARCH_CACHE_TTL = 60 * 1000; // 1 minute
92
+ private readonly MAX_CACHE_SIZE = 100;
93
+ private cleanupInterval: NodeJS.Timeout | null = null;
94
+ private statsInterval: NodeJS.Timeout | null = null;
95
+
96
+ private persistenceManager?: PersistenceManager;
97
+ static async default(opt?: PlayerOptions): Promise<Player> {
98
+ let globaldef = getGlobalManager();
99
+ if (!globaldef) {
100
+ globaldef = new PlayerManager({});
101
+ }
102
+ return await globaldef.create("default", opt);
103
+ }
104
+
105
+ private plugins: SourcePlugin[];
106
+ private extensions: any[];
107
+ private B_debug: boolean = false;
108
+ private extractorTimeout: number = 10000;
109
+ private autoCleanup: boolean = true;
110
+ private cleanupTimeout: number = 60000; // 1 minute
111
+ private enableSearchCache: boolean = true;
112
+
113
+ private debug(message?: any, ...optionalParams: any[]): void {
114
+ if (this.listenerCount("debug") > 0) {
115
+ this.emit("debug", `[PlayerManager] ${message}`, ...optionalParams);
116
+ if (!this.B_debug) {
117
+ this.B_debug = true;
118
+ }
119
+ }
120
+ }
121
+
122
+ constructor(options: PlayerManagerOptions = {}) {
123
+ super();
124
+ this.plugins = [];
125
+ this.searchCache = new Map();
126
+
127
+ // Initialize plugins
128
+ const provided = options.plugins || [];
129
+ for (const p of provided as any[]) {
130
+ try {
131
+ if (p && typeof p === "object") {
132
+ this.plugins.push(p as SourcePlugin);
133
+ } else if (typeof p === "function") {
134
+ const instance = new (p as any)();
135
+ this.plugins.push(instance as SourcePlugin);
136
+ }
137
+ this.debug(`Registered plugin: ${p.name || "unnamed"}`);
138
+ } catch (e) {
139
+ this.debug(`Failed to init plugin:`, e);
140
+ }
141
+ }
142
+
143
+ this.extensions = options.extensions || [];
144
+ this.extractorTimeout = options.extractorTimeout ?? 10000;
145
+ this.autoCleanup = options.autoCleanup ?? true;
146
+ this.cleanupTimeout = options.cleanupInterval ?? 60000;
147
+ this.enableSearchCache = options.enableSearchCache ?? true;
148
+
149
+ if (options.persistence) {
150
+ this.initPersistence(options.persistence);
151
+ }
152
+ // Setup auto cleanup
153
+ if (this.autoCleanup) {
154
+ this.startAutoCleanup();
155
+ }
156
+
157
+ // Setup stats collection (optional)
158
+ if (options.enableStatsCollection) {
159
+ this.startStatsCollection();
160
+ }
161
+
162
+ setGlobalManager(this);
163
+ this.debug(`Initialized with ${this.plugins.length} plugins, ${this.extensions.length} extensions`);
164
+ }
165
+
166
+ private withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
167
+ const timeout = this.extractorTimeout;
168
+ return Promise.race([promise, new Promise<never>((_, reject) => setTimeout(() => reject(new Error(message)), timeout))]);
169
+ }
170
+
171
+ private resolveGuildId(guildOrId: string | { id: string }): string {
172
+ if (typeof guildOrId === "string") return guildOrId;
173
+ if (guildOrId && typeof guildOrId === "object" && "id" in guildOrId) return guildOrId.id;
174
+ throw new Error("Invalid guild or guildId provided.");
175
+ }
176
+
177
+ private getSearchCacheKey(query: string): string {
178
+ return query.toLowerCase().trim();
179
+ }
180
+
181
+ private getCachedSearch(query: string): SearchResult | null {
182
+ if (!this.enableSearchCache) return null;
183
+
184
+ const key = this.getSearchCacheKey(query);
185
+ const cached = this.searchCache.get(key);
186
+
187
+ if (cached && Date.now() < cached.expiresAt) {
188
+ this.debug(`[Cache] Search hit for: ${query}`);
189
+ return cached.data;
190
+ }
191
+
192
+ if (cached) {
193
+ this.searchCache.delete(key);
194
+ }
195
+
196
+ return null;
197
+ }
198
+
199
+ private setCachedSearch(query: string, result: SearchResult): void {
200
+ if (!this.enableSearchCache) return;
201
+
202
+ // Clean up old entries if cache is too large
203
+ if (this.searchCache.size >= this.MAX_CACHE_SIZE) {
204
+ const oldest = Array.from(this.searchCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
205
+ if (oldest) this.searchCache.delete(oldest[0]);
206
+ }
207
+
208
+ const key = this.getSearchCacheKey(query);
209
+ this.searchCache.set(key, {
210
+ data: result,
211
+ timestamp: Date.now(),
212
+ expiresAt: Date.now() + this.SEARCH_CACHE_TTL,
213
+ });
214
+ this.debug(`[Cache] Search stored for: ${query}`);
215
+ }
216
+
217
+ private clearExpiredCache(): void {
218
+ const now = Date.now();
219
+ let expiredCount = 0;
220
+
221
+ for (const [key, entry] of this.searchCache) {
222
+ if (now >= entry.expiresAt) {
223
+ this.searchCache.delete(key);
224
+ expiredCount++;
225
+ }
226
+ }
227
+
228
+ if (expiredCount > 0) {
229
+ this.debug(`[Cache] Cleared ${expiredCount} expired search entries`);
230
+ }
231
+ }
232
+
233
+ private startAutoCleanup(): void {
234
+ if (this.cleanupInterval) {
235
+ clearInterval(this.cleanupInterval);
236
+ }
237
+
238
+ this.cleanupInterval = setInterval(() => {
239
+ this.cleanupInactivePlayers();
240
+ this.clearExpiredCache();
241
+ }, this.cleanupTimeout);
242
+
243
+ this.debug(`Auto-cleanup started with interval: ${this.cleanupTimeout}ms`);
244
+ }
245
+
246
+ private startStatsCollection(): void {
247
+ if (this.statsInterval) {
248
+ clearInterval(this.statsInterval);
249
+ }
250
+
251
+ this.statsInterval = setInterval(() => {
252
+ const stats = this.getStats();
253
+ this.emit("stats", stats);
254
+ }, 30000); // Every 30 seconds
255
+ }
256
+
257
+ private cleanupInactivePlayers(): void {
258
+ let cleanedCount = 0;
259
+
260
+ for (const [guildId, player] of this.players) {
261
+ // Clean up players that are not playing and not connected
262
+ if (!player.isPlaying && !player.connection && player.queue.isEmpty) {
263
+ const idleTime = Date.now() - (player as any)._lastActivity || Date.now();
264
+ if (idleTime > this.cleanupTimeout) {
265
+ this.debug(`Cleaning up inactive player for guild: ${guildId}`);
266
+ player.destroy();
267
+ this.players.delete(guildId);
268
+ cleanedCount++;
269
+ }
270
+ }
271
+ }
272
+
273
+ if (cleanedCount > 0) {
274
+ this.debug(`Cleaned up ${cleanedCount} inactive players`);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Create a new player for a guild
280
+ *
281
+ * @param {string | {id: string}} guildOrId - Guild ID or guild object
282
+ * @param {PlayerOptions} options - Player configuration options
283
+ * @returns {Promise<Player>} The created player instance
284
+ */
285
+ async create(guildOrId: string | { id: string }, options?: PlayerOptions): Promise<Player> {
286
+ const guildId = this.resolveGuildId(guildOrId);
287
+
288
+ if (this.players.has(guildId)) {
289
+ this.debug(`Player already exists for guildId: ${guildId}, returning existing`);
290
+ return this.players.get(guildId)!;
291
+ }
292
+
293
+ this.debug(`Creating player for guildId: ${guildId}`);
294
+ const player = new Player(guildId, options, this);
295
+
296
+ // Add all registered plugins
297
+ this.plugins.forEach((plugin) => player.addPlugin(plugin));
298
+
299
+ // Activate extensions
300
+ let extsToActivate: any[] = [];
301
+ const optExts = (options as any)?.extensions as any[] | string[] | undefined;
302
+
303
+ if (Array.isArray(optExts)) {
304
+ if (optExts.length === 0) {
305
+ extsToActivate = [];
306
+ } else if (typeof optExts[0] === "string") {
307
+ const wanted = new Set(optExts as string[]);
308
+ extsToActivate = this.extensions.filter((ext) => {
309
+ const name = typeof ext === "function" ? ext.name : ext?.name;
310
+ return !!name && wanted.has(name);
311
+ });
312
+ } else {
313
+ extsToActivate = optExts;
314
+ }
315
+ } else {
316
+ // Use all extensions by default
317
+ extsToActivate = this.extensions;
318
+ }
319
+
320
+ for (const ext of extsToActivate) {
321
+ let instance = ext;
322
+ if (typeof ext === "function") {
323
+ try {
324
+ instance = new ext(player);
325
+ } catch (e) {
326
+ this.debug(`Extension constructor error for ${ext.name}:`, e);
327
+ continue;
328
+ }
329
+ }
330
+
331
+ if (instance && typeof instance === "object") {
332
+ const extInstance = instance as BaseExtension;
333
+ if ("player" in extInstance && !extInstance.player) extInstance.player = player;
334
+ player.attachExtension(extInstance);
335
+
336
+ if (typeof extInstance.active === "function") {
337
+ let activated: boolean | void = true;
338
+ try {
339
+ activated = await withTimeout(
340
+ Promise.resolve(extInstance.active({ manager: this, player })),
341
+ player.options.extractorTimeout ?? 15000,
342
+ `Extension ${extInstance?.name} activation timed out`,
343
+ );
344
+ this.debug(`Extension ${extInstance?.name} active check returned: ${activated}`);
345
+ } catch (e) {
346
+ activated = false;
347
+ this.debug(`Extension activation error for ${extInstance?.name}:`, e);
348
+ }
349
+
350
+ if (activated === false) {
351
+ player.detachExtension(extInstance);
352
+ continue;
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ // Forward all player events to manager
359
+ this.setupEventForwarding(player, guildId);
360
+
361
+ // Mark last activity
362
+ (player as any)._lastActivity = Date.now();
363
+
364
+ this.players.set(guildId, player);
365
+ this.debug(`Player created for guildId: ${guildId}`);
366
+ return player;
367
+ }
368
+
369
+ private setupEventForwarding(player: Player, guildId: string): void {
370
+ player.on("willPlay", (track, tracks) => this.emit("willPlay", player, track as Track, tracks as Track[]));
371
+ player.on("trackStart", (track) => {
372
+ (player as any)._lastActivity = Date.now();
373
+ this.emit("trackStart", player, track as Track);
374
+ });
375
+ player.on("trackEnd", (track) => this.emit("trackEnd", player, track as Track));
376
+ player.on("queueEnd", () => this.emit("queueEnd", player));
377
+ player.on("playerError", (error, track) => this.emit("playerError", player, error, track as Track));
378
+ player.on("connectionError", (error) => this.emit("connectionError", player, error));
379
+ player.on("volumeChange", (oldVol, newVol) => this.emit("volumeChange", player, oldVol as number, newVol as number));
380
+ player.on("queueAdd", (track) => this.emit("queueAdd", player, track as Track));
381
+ player.on("queueAddList", (tracks) => this.emit("queueAddList", player, tracks as Track[]));
382
+ player.on("queueRemove", (track, index) => this.emit("queueRemove", player, track as Track, index as number));
383
+ player.on("playerPause", (track) => this.emit("playerPause", player, track as Track));
384
+ player.on("playerResume", (track) => this.emit("playerResume", player, track as Track));
385
+ player.on("playerStop", () => this.emit("playerStop", player));
386
+ player.on("playerDestroy", () => {
387
+ this.emit("playerDestroy", player);
388
+ this.players.delete(guildId);
389
+ this.debug(`Player destroyed for guildId: ${guildId}`);
390
+ });
391
+ player.on("ttsStart", (payload) => this.emit("ttsStart", player, payload));
392
+ player.on("ttsEnd", () => this.emit("ttsEnd", player));
393
+ player.on("debug", (...args) => {
394
+ if (this.listenerCount("debug") > 0) {
395
+ this.emit("debug", ...args);
396
+ }
397
+ });
398
+ }
399
+
400
+ /**
401
+ * Get an existing player for a guild
402
+ *
403
+ * @param {string | {id: string}} guildOrId - Guild ID or guild object
404
+ * @returns {Player | undefined} The player instance or undefined if not found
405
+ */
406
+ get(guildOrId: string | { id: string }): Player | undefined {
407
+ const guildId = this.resolveGuildId(guildOrId);
408
+ const player = this.players.get(guildId);
409
+ if (player) {
410
+ (player as any)._lastActivity = Date.now();
411
+ }
412
+ return player;
413
+ }
414
+
415
+ /**
416
+ * Get an existing player for a guild (alias for get)
417
+ */
418
+ getPlayer(guildOrId: string | { id: string }): Player | undefined {
419
+ return this.get(guildOrId);
420
+ }
421
+
422
+ /**
423
+ * Get all players
424
+ *
425
+ * @returns {Player[]} All player instances
426
+ */
427
+ getAll(): Player[] {
428
+ return Array.from(this.players.values());
429
+ }
430
+
431
+ /**
432
+ * Alias for getAll
433
+ */
434
+ getall(): Player[] {
435
+ return this.getAll();
436
+ }
437
+
438
+ /**
439
+ * Get players by filter
440
+ *
441
+ * @param {(player: Player) => boolean} filter - Filter function
442
+ * @returns {Player[]} Filtered player instances
443
+ */
444
+ getPlayersByFilter(filter: (player: Player) => boolean): Player[] {
445
+ return this.getAll().filter(filter);
446
+ }
447
+
448
+ /**
449
+ * Get players in a voice channel
450
+ *
451
+ * @param {string} channelId - Voice channel ID
452
+ * @returns {Player[]} Players in the channel
453
+ */
454
+ getPlayersInChannel(channelId: string): Player[] {
455
+ return this.getAll().filter((p) => p.connection?.joinConfig.channelId === channelId);
456
+ }
457
+
458
+ /**
459
+ * Destroy a player and clean up resources
460
+ *
461
+ * @param {string | {id: string}} guildOrId - Guild ID or guild object
462
+ * @returns {boolean} True if player was destroyed, false if not found
463
+ */
464
+ delete(guildOrId: string | { id: string }): boolean {
465
+ const guildId = this.resolveGuildId(guildOrId);
466
+ const player = this.players.get(guildId);
467
+
468
+ if (player) {
469
+ this.debug(`Deleting player for guildId: ${guildId}`);
470
+ player.destroy();
471
+ return true;
472
+ }
473
+ return false;
474
+ }
475
+
476
+ /**
477
+ * Destroy multiple players by filter
478
+ *
479
+ * @param {(player: Player) => boolean} filter - Filter function
480
+ * @returns {number} Number of players destroyed
481
+ */
482
+ deleteWhere(filter: (player: Player) => boolean): number {
483
+ const toDelete = this.getPlayersByFilter(filter);
484
+ let count = 0;
485
+
486
+ for (const player of toDelete) {
487
+ const guildId = player.guildId;
488
+ player.destroy();
489
+ this.players.delete(guildId);
490
+ count++;
491
+ }
492
+
493
+ if (count > 0) {
494
+ this.debug(`Deleted ${count} players by filter`);
495
+ }
496
+ return count;
497
+ }
498
+
499
+ /**
500
+ * Check if a player exists for a guild
501
+ *
502
+ * @param {string | {id: string}} guildOrId - Guild ID or guild object
503
+ * @returns {boolean} True if player exists
504
+ */
505
+ has(guildOrId: string | { id: string }): boolean {
506
+ const guildId = this.resolveGuildId(guildOrId);
507
+ return this.players.has(guildId);
508
+ }
509
+
510
+ /**
511
+ * Get number of players
512
+ */
513
+ get size(): number {
514
+ return this.players.size;
515
+ }
516
+
517
+ /**
518
+ * Check if debug is enabled
519
+ */
520
+ get debugEnabled(): boolean {
521
+ return this.B_debug;
522
+ }
523
+
524
+ /**
525
+ * Get manager statistics
526
+ *
527
+ * @returns {PlayerStats} Statistics about players
528
+ */
529
+ getStats(): PlayerStats {
530
+ let activePlayers = 0;
531
+ let pausedPlayers = 0;
532
+ let connectedPlayers = 0;
533
+ let totalTracksInQueue = 0;
534
+
535
+ for (const player of this.players.values()) {
536
+ if (player.isPlaying) activePlayers++;
537
+ if (player.isPaused) pausedPlayers++;
538
+ if (player.connection) connectedPlayers++;
539
+ totalTracksInQueue += player.queueSize;
540
+ }
541
+
542
+ return {
543
+ totalPlayers: this.players.size,
544
+ activePlayers,
545
+ pausedPlayers,
546
+ connectedPlayers,
547
+ totalTracksInQueue,
548
+ };
549
+ }
550
+
551
+ /**
552
+ * Broadcast an action to all players
553
+ *
554
+ * @param {string} action - Action to perform
555
+ * @param {...any[]} args - Arguments for the action
556
+ * @example
557
+ * manager.broadcast("setVolume", 50);
558
+ * manager.broadcast("pause");
559
+ */
560
+ broadcast(action: string, ...args: any[]): void {
561
+ for (const player of this.players.values()) {
562
+ if (typeof (player as any)[action] === "function") {
563
+ try {
564
+ (player as any)[action](...args);
565
+ } catch (error) {
566
+ this.debug(`Error broadcasting ${action} to ${player.guildId}:`, error);
567
+ }
568
+ }
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Destroy all players and clean up
574
+ */
575
+ destroy(): void {
576
+ this.debug(`Destroying all players`);
577
+
578
+ if (this.persistenceManager) {
579
+ this.persistenceManager.saveAll().catch((err) => {
580
+ this.debug("Failed to save players before destroy:", err);
581
+ });
582
+ this.persistenceManager.shutdown().catch(console.error);
583
+ }
584
+ // Stop cleanup intervals
585
+ if (this.cleanupInterval) {
586
+ clearInterval(this.cleanupInterval);
587
+ this.cleanupInterval = null;
588
+ }
589
+
590
+ if (this.statsInterval) {
591
+ clearInterval(this.statsInterval);
592
+ this.statsInterval = null;
593
+ }
594
+
595
+ // Destroy all players
596
+ for (const player of this.players.values()) {
597
+ player.destroy();
598
+ }
599
+
600
+ this.players.clear();
601
+ this.searchCache.clear();
602
+ this.removeAllListeners();
603
+ this.debug(`PlayerManager destroyed`);
604
+ }
605
+
606
+ /**
607
+ * Search using registered plugins without creating a Player.
608
+ *
609
+ * @param {string} query - The query to search for
610
+ * @param {string} requestedBy - The user ID who requested the search
611
+ * @returns {Promise<SearchResult>} The search result
612
+ */
613
+ async search(query: string, requestedBy: string): Promise<SearchResult> {
614
+ this.debug(`Search called with query: ${query}, requestedBy: ${requestedBy}`);
615
+
616
+ // Check cache first
617
+ const cached = this.getCachedSearch(query);
618
+ if (cached) {
619
+ return cached;
620
+ }
621
+
622
+ const plugin = this.plugins.find((p) => p.canHandle(query));
623
+ if (!plugin) {
624
+ this.debug(`No plugin found to handle: ${query}`);
625
+ throw new Error(`No plugin found to handle: ${query}`);
626
+ }
627
+
628
+ try {
629
+ const result = await this.withTimeout(plugin.search(query, requestedBy), "Search operation timed out");
630
+ this.setCachedSearch(query, result);
631
+ return result;
632
+ } catch (error) {
633
+ this.debug(`Search error:`, error);
634
+ throw error as Error;
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Clear search cache
640
+ */
641
+ clearSearchCache(): void {
642
+ const size = this.searchCache.size;
643
+ this.searchCache.clear();
644
+ this.debug(`Cleared ${size} search cache entries`);
645
+ }
646
+
647
+ /**
648
+ * Register a plugin after initialization
649
+ *
650
+ * @param {SourcePlugin} plugin - Plugin to register
651
+ */
652
+ registerPlugin(plugin: SourcePlugin): void {
653
+ this.plugins.push(plugin);
654
+ this.debug(`Registered plugin: ${plugin.name}`);
655
+
656
+ // Register plugin with all existing players
657
+ for (const player of this.players.values()) {
658
+ player.addPlugin(plugin);
659
+ }
660
+ }
661
+
662
+ /**
663
+ * Unregister a plugin
664
+ *
665
+ * @param {string} name - Plugin name to unregister
666
+ * @returns {boolean} True if plugin was unregistered
667
+ */
668
+ unregisterPlugin(name: string): boolean {
669
+ const index = this.plugins.findIndex((p) => p.name === name);
670
+ if (index === -1) return false;
671
+
672
+ this.plugins.splice(index, 1);
673
+ this.debug(`Unregistered plugin: ${name}`);
674
+
675
+ // Note: Cannot easily remove plugins from existing players
676
+ return true;
677
+ }
678
+
679
+ /**
680
+ * Get all registered plugins
681
+ */
682
+ getPlugins(): SourcePlugin[] {
683
+ return [...this.plugins];
684
+ }
685
+
686
+ /**
687
+ * Register an extension after initialization
688
+ *
689
+ * @param {BaseExtension} extension - Extension to register
690
+ */
691
+ registerExtension(extension: BaseExtension): void {
692
+ this.extensions.push(extension);
693
+ this.debug(`Registered extension: ${extension.name}`);
694
+
695
+ // Register extension with all existing players
696
+ for (const player of this.players.values()) {
697
+ player.attachExtension(extension);
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Get manager configuration
703
+ */
704
+ getConfig(): object {
705
+ return {
706
+ extractorTimeout: this.extractorTimeout,
707
+ autoCleanup: this.autoCleanup,
708
+ cleanupTimeout: this.cleanupTimeout,
709
+ enableSearchCache: this.enableSearchCache,
710
+ pluginsCount: this.plugins.length,
711
+ extensionsCount: this.extensions.length,
712
+ playersCount: this.players.size,
713
+ };
714
+ }
715
+ private initPersistence(persistenceOptions: PersistenceOptions): void {
716
+ this.persistenceManager = new PersistenceManager(this, persistenceOptions);
717
+
718
+ // Forward persistence events
719
+ this.persistenceManager.on("playerSaved", (guildId) => {
720
+ this.emit("playerSaved", guildId);
721
+ });
722
+
723
+ this.persistenceManager.on("playerLoaded", (guildId, data) => {
724
+ this.emit("playerLoaded", guildId, data);
725
+ });
726
+
727
+ this.persistenceManager.on("savedAll", (results) => {
728
+ this.emit("savedAll", results);
729
+ });
730
+
731
+ this.persistenceManager.on("loadedAll", (results) => {
732
+ this.emit("loadedAll", results);
733
+ });
734
+
735
+ this.debug("Persistence manager initialized");
736
+ }
737
+
738
+ /**
739
+ * Get persistence manager
740
+ */
741
+ getPersistence(): PersistenceManager | undefined {
742
+ return this.persistenceManager;
743
+ }
744
+
745
+ /**
746
+ * Save all players
747
+ */
748
+ async saveAllPlayers(): Promise<Map<string, boolean>> {
749
+ if (!this.persistenceManager) {
750
+ throw new Error("Persistence not enabled");
751
+ }
752
+ return await this.persistenceManager.saveAll();
753
+ }
754
+
755
+ /**
756
+ * Load all players
757
+ */
758
+ async loadAllPlayers(restorePosition: boolean = true): Promise<Map<string, boolean>> {
759
+ if (!this.persistenceManager) {
760
+ throw new Error("Persistence not enabled");
761
+ }
762
+ return await this.persistenceManager.loadAll(restorePosition);
763
+ }
764
+
765
+ /**
766
+ * Save a specific player
767
+ */
768
+ async savePlayer(guildId: string): Promise<boolean> {
769
+ if (!this.persistenceManager) return false;
770
+ const player = this.get(guildId);
771
+ if (!player) return false;
772
+ return await this.persistenceManager.savePlayer(player);
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Get the global PlayerManager instance
778
+ *
779
+ * @returns {PlayerManager | null} Global instance or null
780
+ */
781
+ export function getInstance(): PlayerManager | null {
782
+ const globalInst = getGlobalManager();
783
+ if (!globalInst) {
784
+ console.error("[PlayerManager] Global instance not found, make sure to initialize with new PlayerManager(options)");
785
+ return null;
786
+ }
787
+ return globalInst;
788
+ }