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
@@ -46,7 +46,9 @@ const setGlobalManager = (instance) => {
46
46
  * nodes: [{ host: "localhost", port: 2333, password: "youshallnotpass" }]
47
47
  * })
48
48
  * ],
49
- * extractorTimeout: 10000
49
+ * extractorTimeout: 10000,
50
+ * autoCleanup: true,
51
+ * cleanupInterval: 60000
50
52
  * });
51
53
  *
52
54
  * // Create a player for a guild
@@ -72,7 +74,7 @@ class PlayerManager extends events_1.EventEmitter {
72
74
  }
73
75
  debug(message, ...optionalParams) {
74
76
  if (this.listenerCount("debug") > 0) {
75
- this.emit("debug", message, ...optionalParams);
77
+ this.emit("debug", `[PlayerManager] ${message}`, ...optionalParams);
76
78
  if (!this.B_debug) {
77
79
  this.B_debug = true;
78
80
  }
@@ -81,9 +83,18 @@ class PlayerManager extends events_1.EventEmitter {
81
83
  constructor(options = {}) {
82
84
  super();
83
85
  this.players = new Map();
86
+ this.SEARCH_CACHE_TTL = 60 * 1000; // 1 minute
87
+ this.MAX_CACHE_SIZE = 100;
88
+ this.cleanupInterval = null;
89
+ this.statsInterval = null;
84
90
  this.B_debug = false;
85
91
  this.extractorTimeout = 10000;
92
+ this.autoCleanup = true;
93
+ this.cleanupTimeout = 60000; // 1 minute
94
+ this.enableSearchCache = true;
86
95
  this.plugins = [];
96
+ this.searchCache = new Map();
97
+ // Initialize plugins
87
98
  const provided = options.plugins || [];
88
99
  for (const p of provided) {
89
100
  try {
@@ -94,13 +105,27 @@ class PlayerManager extends events_1.EventEmitter {
94
105
  const instance = new p();
95
106
  this.plugins.push(instance);
96
107
  }
108
+ this.debug(`Registered plugin: ${p.name || "unnamed"}`);
97
109
  }
98
110
  catch (e) {
99
- this.debug(`[PlayerManager] Failed to init plugin:`, e);
111
+ this.debug(`Failed to init plugin:`, e);
100
112
  }
101
113
  }
102
114
  this.extensions = options.extensions || [];
115
+ this.extractorTimeout = options.extractorTimeout ?? 10000;
116
+ this.autoCleanup = options.autoCleanup ?? true;
117
+ this.cleanupTimeout = options.cleanupInterval ?? 60000;
118
+ this.enableSearchCache = options.enableSearchCache ?? true;
119
+ // Setup auto cleanup
120
+ if (this.autoCleanup) {
121
+ this.startAutoCleanup();
122
+ }
123
+ // Setup stats collection (optional)
124
+ if (options.enableStatsCollection) {
125
+ this.startStatsCollection();
126
+ }
103
127
  setGlobalManager(this);
128
+ this.debug(`Initialized with ${this.plugins.length} plugins, ${this.extensions.length} extensions`);
104
129
  }
105
130
  withTimeout(promise, message) {
106
131
  const timeout = this.extractorTimeout;
@@ -113,48 +138,108 @@ class PlayerManager extends events_1.EventEmitter {
113
138
  return guildOrId.id;
114
139
  throw new Error("Invalid guild or guildId provided.");
115
140
  }
141
+ getSearchCacheKey(query) {
142
+ return query.toLowerCase().trim();
143
+ }
144
+ getCachedSearch(query) {
145
+ if (!this.enableSearchCache)
146
+ return null;
147
+ const key = this.getSearchCacheKey(query);
148
+ const cached = this.searchCache.get(key);
149
+ if (cached && Date.now() < cached.expiresAt) {
150
+ this.debug(`[Cache] Search hit for: ${query}`);
151
+ return cached.data;
152
+ }
153
+ if (cached) {
154
+ this.searchCache.delete(key);
155
+ }
156
+ return null;
157
+ }
158
+ setCachedSearch(query, result) {
159
+ if (!this.enableSearchCache)
160
+ return;
161
+ // Clean up old entries if cache is too large
162
+ if (this.searchCache.size >= this.MAX_CACHE_SIZE) {
163
+ const oldest = Array.from(this.searchCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
164
+ if (oldest)
165
+ this.searchCache.delete(oldest[0]);
166
+ }
167
+ const key = this.getSearchCacheKey(query);
168
+ this.searchCache.set(key, {
169
+ data: result,
170
+ timestamp: Date.now(),
171
+ expiresAt: Date.now() + this.SEARCH_CACHE_TTL,
172
+ });
173
+ this.debug(`[Cache] Search stored for: ${query}`);
174
+ }
175
+ clearExpiredCache() {
176
+ const now = Date.now();
177
+ let expiredCount = 0;
178
+ for (const [key, entry] of this.searchCache) {
179
+ if (now >= entry.expiresAt) {
180
+ this.searchCache.delete(key);
181
+ expiredCount++;
182
+ }
183
+ }
184
+ if (expiredCount > 0) {
185
+ this.debug(`[Cache] Cleared ${expiredCount} expired search entries`);
186
+ }
187
+ }
188
+ startAutoCleanup() {
189
+ if (this.cleanupInterval) {
190
+ clearInterval(this.cleanupInterval);
191
+ }
192
+ this.cleanupInterval = setInterval(() => {
193
+ this.cleanupInactivePlayers();
194
+ this.clearExpiredCache();
195
+ }, this.cleanupTimeout);
196
+ this.debug(`Auto-cleanup started with interval: ${this.cleanupTimeout}ms`);
197
+ }
198
+ startStatsCollection() {
199
+ if (this.statsInterval) {
200
+ clearInterval(this.statsInterval);
201
+ }
202
+ this.statsInterval = setInterval(() => {
203
+ const stats = this.getStats();
204
+ this.emit("stats", stats);
205
+ }, 30000); // Every 30 seconds
206
+ }
207
+ cleanupInactivePlayers() {
208
+ let cleanedCount = 0;
209
+ for (const [guildId, player] of this.players) {
210
+ // Clean up players that are not playing and not connected
211
+ if (!player.isPlaying && !player.connection && player.queue.isEmpty) {
212
+ const idleTime = Date.now() - player._lastActivity || Date.now();
213
+ if (idleTime > this.cleanupTimeout) {
214
+ this.debug(`Cleaning up inactive player for guild: ${guildId}`);
215
+ player.destroy();
216
+ this.players.delete(guildId);
217
+ cleanedCount++;
218
+ }
219
+ }
220
+ }
221
+ if (cleanedCount > 0) {
222
+ this.debug(`Cleaned up ${cleanedCount} inactive players`);
223
+ }
224
+ }
116
225
  /**
117
226
  * Create a new player for a guild
118
227
  *
119
228
  * @param {string | {id: string}} guildOrId - Guild ID or guild object
120
229
  * @param {PlayerOptions} options - Player configuration options
121
230
  * @returns {Promise<Player>} The created player instance
122
- *
123
- * @example
124
- * // Create player with basic options
125
- * const player = await manager.create(guildId, {
126
- * tts: { interrupt: true, volume: 1 },
127
- * leaveOnEnd: true,
128
- * leaveTimeout: 30000
129
- * });
130
- *
131
- * // Create player with advanced options
132
- * const advancedPlayer = await manager.create(guild, {
133
- * volume: 0.8,
134
- * quality: "high",
135
- * selfDeaf: false,
136
- * selfMute: false,
137
- * tts: {
138
- * createPlayer: true,
139
- * interrupt: true,
140
- * volume: 1.0,
141
- * Max_Time_TTS: 30000
142
- * },
143
- * userdata: { customData: "example" }
144
- * });
145
- *
146
- * // Connect and play immediately
147
- * await player.connect(voiceChannel);
148
- * await player.play("Never Gonna Give You Up", userId);
149
231
  */
150
232
  async create(guildOrId, options) {
151
233
  const guildId = this.resolveGuildId(guildOrId);
152
234
  if (this.players.has(guildId)) {
235
+ this.debug(`Player already exists for guildId: ${guildId}, returning existing`);
153
236
  return this.players.get(guildId);
154
237
  }
155
- this.debug(`[PlayerManager] Creating player for guildId: ${guildId}`);
238
+ this.debug(`Creating player for guildId: ${guildId}`);
156
239
  const player = new Player_1.Player(guildId, options, this);
240
+ // Add all registered plugins
157
241
  this.plugins.forEach((plugin) => player.addPlugin(plugin));
242
+ // Activate extensions
158
243
  let extsToActivate = [];
159
244
  const optExts = options?.extensions;
160
245
  if (Array.isArray(optExts)) {
@@ -172,6 +257,10 @@ class PlayerManager extends events_1.EventEmitter {
172
257
  extsToActivate = optExts;
173
258
  }
174
259
  }
260
+ else {
261
+ // Use all extensions by default
262
+ extsToActivate = this.extensions;
263
+ }
175
264
  for (const ext of extsToActivate) {
176
265
  let instance = ext;
177
266
  if (typeof ext === "function") {
@@ -179,7 +268,7 @@ class PlayerManager extends events_1.EventEmitter {
179
268
  instance = new ext(player);
180
269
  }
181
270
  catch (e) {
182
- this.debug(`[PlayerManager] Extension constructor error:`, e);
271
+ this.debug(`Extension constructor error for ${ext.name}:`, e);
183
272
  continue;
184
273
  }
185
274
  }
@@ -192,11 +281,11 @@ class PlayerManager extends events_1.EventEmitter {
192
281
  let activated = true;
193
282
  try {
194
283
  activated = await (0, timeout_1.withTimeout)(Promise.resolve(extInstance.active({ manager: this, player })), player.options.extractorTimeout ?? 15000, `Extension ${extInstance?.name} activation timed out`);
195
- this.debug(`[PlayerManager] Extension ${extInstance?.name} active`);
284
+ this.debug(`Extension ${extInstance?.name} active check returned: ${activated}`);
196
285
  }
197
286
  catch (e) {
198
287
  activated = false;
199
- this.debug(`[PlayerManager] Extension activation error:`, e);
288
+ this.debug(`Extension activation error for ${extInstance?.name}:`, e);
200
289
  }
201
290
  if (activated === false) {
202
291
  player.detachExtension(extInstance);
@@ -205,158 +294,232 @@ class PlayerManager extends events_1.EventEmitter {
205
294
  }
206
295
  }
207
296
  }
208
- // Forward all player events
209
- player.on("willPlay", (track, tracks) => this.emit("willPlay", player, track, tracks));
210
- player.on("trackStart", (track) => this.emit("trackStart", player, track));
211
- player.on("trackEnd", (track) => this.emit("trackEnd", player, track));
212
- player.on("queueEnd", () => this.emit("queueEnd", player));
213
- player.on("playerError", (error, track) => this.emit("playerError", player, error, track));
214
- player.on("connectionError", (error) => this.emit("connectionError", player, error));
215
- player.on("volumeChange", (old, volume) => this.emit("volumeChange", player, old, volume));
216
- player.on("queueAdd", (track) => this.emit("queueAdd", player, track));
217
- player.on("queueAddList", (tracks) => this.emit("queueAddList", player, tracks));
218
- player.on("queueRemove", (track, index) => this.emit("queueRemove", player, track, index));
219
- player.on("playerPause", (track) => this.emit("playerPause", player, track));
220
- player.on("playerResume", (track) => this.emit("playerResume", player, track));
221
- player.on("playerStop", () => this.emit("playerStop", player));
297
+ // Forward all player events to manager
298
+ this.setupEventForwarding(player, guildId);
299
+ // Mark last activity
300
+ player._lastActivity = Date.now();
301
+ this.players.set(guildId, player);
302
+ this.debug(`Player created for guildId: ${guildId}`);
303
+ return player;
304
+ }
305
+ setupEventForwarding(player, guildId) {
306
+ const forwardEvents = {
307
+ willPlay: "willPlay",
308
+ trackStart: "trackStart",
309
+ trackEnd: "trackEnd",
310
+ queueEnd: "queueEnd",
311
+ playerError: "playerError",
312
+ connectionError: "connectionError",
313
+ volumeChange: "volumeChange",
314
+ queueAdd: "queueAdd",
315
+ queueAddList: "queueAddList",
316
+ queueRemove: "queueRemove",
317
+ playerPause: "playerPause",
318
+ playerResume: "playerResume",
319
+ playerStop: "playerStop",
320
+ ttsStart: "ttsStart",
321
+ ttsEnd: "ttsEnd",
322
+ streamError: "streamError",
323
+ };
324
+ for (const [sourceEvent, targetEvent] of Object.entries(forwardEvents)) {
325
+ player.on(sourceEvent, (...args) => {
326
+ if (sourceEvent === "trackStart") {
327
+ player._lastActivity = Date.now();
328
+ }
329
+ this.emit(targetEvent, player, ...args);
330
+ });
331
+ }
222
332
  player.on("playerDestroy", () => {
223
333
  this.emit("playerDestroy", player);
224
334
  this.players.delete(guildId);
335
+ this.debug(`Player destroyed for guildId: ${guildId}`);
225
336
  });
226
- player.on("ttsStart", (payload) => this.emit("ttsStart", player, payload));
227
- player.on("ttsEnd", () => this.emit("ttsEnd", player));
228
337
  player.on("debug", (...args) => {
229
338
  if (this.listenerCount("debug") > 0) {
230
339
  this.emit("debug", ...args);
231
340
  }
232
341
  });
233
- this.players.set(guildId, player);
234
- return player;
235
342
  }
236
343
  /**
237
344
  * Get an existing player for a guild
238
345
  *
239
346
  * @param {string | {id: string}} guildOrId - Guild ID or guild object
240
347
  * @returns {Player | undefined} The player instance or undefined if not found
241
- * @example
242
- * // Get player by guild ID
243
- * const player = manager.get(guildId);
244
- * if (player) {
245
- * await player.play("Never Gonna Give You Up", userId);
246
- * } else {
247
- * console.log("No player found for this guild");
248
- * }
249
- *
250
- * // Get player by guild object
251
- * const playerFromGuild = manager.get(guild);
252
- * if (playerFromGuild) {
253
- * playerFromGuild.setVolume(0.5);
254
- * }
255
- *
256
- * // Check if player exists before using
257
- * const existingPlayer = manager.get(guildId);
258
- * if (existingPlayer && existingPlayer.playing) {
259
- * existingPlayer.pause();
260
- * }
261
348
  */
262
349
  get(guildOrId) {
263
350
  const guildId = this.resolveGuildId(guildOrId);
264
- return this.players.get(guildId);
351
+ const player = this.players.get(guildId);
352
+ if (player) {
353
+ player._lastActivity = Date.now();
354
+ }
355
+ return player;
265
356
  }
266
357
  /**
267
- * Get an existing player for a guild
268
- *
269
- * @param {string | {id: string}} guildOrId - Guild ID or guild object
270
- * @returns {Player | undefined} The player instance or undefined
271
- * @example
272
- * const player = manager.get(guildId);
273
- * if (player) {
274
- * await player.play("song name", userId);
275
- * }
358
+ * Get an existing player for a guild (alias for get)
276
359
  */
277
360
  getPlayer(guildOrId) {
278
- const guildId = this.resolveGuildId(guildOrId);
279
- return this.players.get(guildId);
361
+ return this.get(guildOrId);
280
362
  }
281
363
  /**
282
364
  * Get all players
283
365
  *
284
366
  * @returns {Player[]} All player instances
285
- * @example
286
- * const players = manager.getall();
287
- * console.log(`Players: ${players.length}`);
288
367
  */
289
- getall() {
368
+ getAll() {
290
369
  return Array.from(this.players.values());
291
370
  }
371
+ /**
372
+ * Alias for getAll
373
+ */
374
+ getall() {
375
+ return this.getAll();
376
+ }
377
+ /**
378
+ * Get players by filter
379
+ *
380
+ * @param {(player: Player) => boolean} filter - Filter function
381
+ * @returns {Player[]} Filtered player instances
382
+ */
383
+ getPlayersByFilter(filter) {
384
+ return this.getAll().filter(filter);
385
+ }
386
+ /**
387
+ * Get players in a voice channel
388
+ *
389
+ * @param {string} channelId - Voice channel ID
390
+ * @returns {Player[]} Players in the channel
391
+ */
392
+ getPlayersInChannel(channelId) {
393
+ return this.getAll().filter((p) => p.connection?.joinConfig.channelId === channelId);
394
+ }
292
395
  /**
293
396
  * Destroy a player and clean up resources
294
397
  *
295
398
  * @param {string | {id: string}} guildOrId - Guild ID or guild object
296
399
  * @returns {boolean} True if player was destroyed, false if not found
297
- * @example
298
- * // Destroy player by guild ID
299
- * const destroyed = manager.delete(guildId);
300
- * if (destroyed) {
301
- * console.log("Player destroyed successfully");
302
- * } else {
303
- * console.log("No player found to destroy");
304
- * }
305
- *
306
- * // Destroy player by guild object
307
- * const destroyedFromGuild = manager.delete(guild);
308
- * console.log(`Player destroyed: ${destroyedFromGuild}`);
309
- *
310
- * // Clean up all players
311
- * for (const [guildId, player] of manager.players) {
312
- * const destroyed = manager.delete(guildId);
313
- * console.log(`Destroyed player for ${guildId}: ${destroyed}`);
314
- * }
315
400
  */
316
401
  delete(guildOrId) {
317
402
  const guildId = this.resolveGuildId(guildOrId);
318
403
  const player = this.players.get(guildId);
319
404
  if (player) {
320
- this.debug(`[PlayerManager] Deleting player for guildId: ${guildId}`);
405
+ this.debug(`Deleting player for guildId: ${guildId}`);
321
406
  player.destroy();
322
- return this.players.delete(guildId);
407
+ return true;
323
408
  }
324
409
  return false;
325
410
  }
411
+ /**
412
+ * Destroy multiple players by filter
413
+ *
414
+ * @param {(player: Player) => boolean} filter - Filter function
415
+ * @returns {number} Number of players destroyed
416
+ */
417
+ deleteWhere(filter) {
418
+ const toDelete = this.getPlayersByFilter(filter);
419
+ let count = 0;
420
+ for (const player of toDelete) {
421
+ const guildId = player.guildId;
422
+ player.destroy();
423
+ this.players.delete(guildId);
424
+ count++;
425
+ }
426
+ if (count > 0) {
427
+ this.debug(`Deleted ${count} players by filter`);
428
+ }
429
+ return count;
430
+ }
326
431
  /**
327
432
  * Check if a player exists for a guild
328
433
  *
329
434
  * @param {string | {id: string}} guildOrId - Guild ID or guild object
330
- * @returns {boolean} True if player exists, false if not
331
- * @example
332
- * const exists = manager.has(guildId);
333
- * console.log(`Player exists: ${exists}`);
435
+ * @returns {boolean} True if player exists
334
436
  */
335
437
  has(guildOrId) {
336
438
  const guildId = this.resolveGuildId(guildOrId);
337
439
  return this.players.has(guildId);
338
440
  }
441
+ /**
442
+ * Get number of players
443
+ */
339
444
  get size() {
340
445
  return this.players.size;
341
446
  }
447
+ /**
448
+ * Check if debug is enabled
449
+ */
342
450
  get debugEnabled() {
343
451
  return this.B_debug;
344
452
  }
345
453
  /**
346
- * Destroy all players
454
+ * Get manager statistics
347
455
  *
348
- * @returns {void}
456
+ * @returns {PlayerStats} Statistics about players
457
+ */
458
+ getStats() {
459
+ let activePlayers = 0;
460
+ let pausedPlayers = 0;
461
+ let connectedPlayers = 0;
462
+ let totalTracksInQueue = 0;
463
+ for (const player of this.players.values()) {
464
+ if (player.isPlaying)
465
+ activePlayers++;
466
+ if (player.isPaused)
467
+ pausedPlayers++;
468
+ if (player.connection)
469
+ connectedPlayers++;
470
+ totalTracksInQueue += player.queueSize;
471
+ }
472
+ return {
473
+ totalPlayers: this.players.size,
474
+ activePlayers,
475
+ pausedPlayers,
476
+ connectedPlayers,
477
+ totalTracksInQueue,
478
+ };
479
+ }
480
+ /**
481
+ * Broadcast an action to all players
482
+ *
483
+ * @param {string} action - Action to perform
484
+ * @param {...any[]} args - Arguments for the action
349
485
  * @example
350
- * manager.destroy();
351
- * console.log(`All players destroyed`);
486
+ * manager.broadcast("setVolume", 50);
487
+ * manager.broadcast("pause");
488
+ */
489
+ broadcast(action, ...args) {
490
+ for (const player of this.players.values()) {
491
+ if (typeof player[action] === "function") {
492
+ try {
493
+ player[action](...args);
494
+ }
495
+ catch (error) {
496
+ this.debug(`Error broadcasting ${action} to ${player.guildId}:`, error);
497
+ }
498
+ }
499
+ }
500
+ }
501
+ /**
502
+ * Destroy all players and clean up
352
503
  */
353
504
  destroy() {
354
- this.debug(`[PlayerManager] Destroying all players`);
505
+ this.debug(`Destroying all players`);
506
+ // Stop cleanup intervals
507
+ if (this.cleanupInterval) {
508
+ clearInterval(this.cleanupInterval);
509
+ this.cleanupInterval = null;
510
+ }
511
+ if (this.statsInterval) {
512
+ clearInterval(this.statsInterval);
513
+ this.statsInterval = null;
514
+ }
515
+ // Destroy all players
355
516
  for (const player of this.players.values()) {
356
517
  player.destroy();
357
518
  }
358
519
  this.players.clear();
520
+ this.searchCache.clear();
359
521
  this.removeAllListeners();
522
+ this.debug(`PlayerManager destroyed`);
360
523
  }
361
524
  /**
362
525
  * Search using registered plugins without creating a Player.
@@ -364,28 +527,106 @@ class PlayerManager extends events_1.EventEmitter {
364
527
  * @param {string} query - The query to search for
365
528
  * @param {string} requestedBy - The user ID who requested the search
366
529
  * @returns {Promise<SearchResult>} The search result
367
- * @example
368
- * const result = await manager.search("Never Gonna Give You Up", userId);
369
- * console.log(`Search result: ${result.tracks.length} tracks`);
370
530
  */
371
531
  async search(query, requestedBy) {
372
- this.debug(`[PlayerManager] Search called with query: ${query}, requestedBy: ${requestedBy}`);
532
+ this.debug(`Search called with query: ${query}, requestedBy: ${requestedBy}`);
533
+ // Check cache first
534
+ const cached = this.getCachedSearch(query);
535
+ if (cached) {
536
+ return cached;
537
+ }
373
538
  const plugin = this.plugins.find((p) => p.canHandle(query));
374
539
  if (!plugin) {
375
- this.debug(`[PlayerManager] No plugin found to handle: ${query}`);
540
+ this.debug(`No plugin found to handle: ${query}`);
376
541
  throw new Error(`No plugin found to handle: ${query}`);
377
542
  }
378
543
  try {
379
- return await this.withTimeout(plugin.search(query, requestedBy), "Search operation timed out");
544
+ const result = await this.withTimeout(plugin.search(query, requestedBy), "Search operation timed out");
545
+ this.setCachedSearch(query, result);
546
+ return result;
380
547
  }
381
548
  catch (error) {
382
- this.debug(`[PlayerManager] Search error:`, error);
549
+ this.debug(`Search error:`, error);
383
550
  throw error;
384
551
  }
385
552
  }
553
+ /**
554
+ * Clear search cache
555
+ */
556
+ clearSearchCache() {
557
+ const size = this.searchCache.size;
558
+ this.searchCache.clear();
559
+ this.debug(`Cleared ${size} search cache entries`);
560
+ }
561
+ /**
562
+ * Register a plugin after initialization
563
+ *
564
+ * @param {SourcePlugin} plugin - Plugin to register
565
+ */
566
+ registerPlugin(plugin) {
567
+ this.plugins.push(plugin);
568
+ this.debug(`Registered plugin: ${plugin.name}`);
569
+ // Register plugin with all existing players
570
+ for (const player of this.players.values()) {
571
+ player.addPlugin(plugin);
572
+ }
573
+ }
574
+ /**
575
+ * Unregister a plugin
576
+ *
577
+ * @param {string} name - Plugin name to unregister
578
+ * @returns {boolean} True if plugin was unregistered
579
+ */
580
+ unregisterPlugin(name) {
581
+ const index = this.plugins.findIndex((p) => p.name === name);
582
+ if (index === -1)
583
+ return false;
584
+ this.plugins.splice(index, 1);
585
+ this.debug(`Unregistered plugin: ${name}`);
586
+ // Note: Cannot easily remove plugins from existing players
587
+ return true;
588
+ }
589
+ /**
590
+ * Get all registered plugins
591
+ */
592
+ getPlugins() {
593
+ return [...this.plugins];
594
+ }
595
+ /**
596
+ * Register an extension after initialization
597
+ *
598
+ * @param {BaseExtension} extension - Extension to register
599
+ */
600
+ registerExtension(extension) {
601
+ this.extensions.push(extension);
602
+ this.debug(`Registered extension: ${extension.name}`);
603
+ // Register extension with all existing players
604
+ for (const player of this.players.values()) {
605
+ player.attachExtension(extension);
606
+ }
607
+ }
608
+ /**
609
+ * Get manager configuration
610
+ */
611
+ getConfig() {
612
+ return {
613
+ extractorTimeout: this.extractorTimeout,
614
+ autoCleanup: this.autoCleanup,
615
+ cleanupTimeout: this.cleanupTimeout,
616
+ enableSearchCache: this.enableSearchCache,
617
+ pluginsCount: this.plugins.length,
618
+ extensionsCount: this.extensions.length,
619
+ playersCount: this.players.size,
620
+ };
621
+ }
386
622
  }
387
623
  exports.PlayerManager = PlayerManager;
388
624
  PlayerManager.instance = null;
625
+ /**
626
+ * Get the global PlayerManager instance
627
+ *
628
+ * @returns {PlayerManager | null} Global instance or null
629
+ */
389
630
  function getInstance() {
390
631
  const globalInst = (0, exports.getGlobalManager)();
391
632
  if (!globalInst) {