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