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