ziplayer 0.2.7-dev.0 → 0.2.7-dev.2

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 (54) hide show
  1. package/AI-Guide.md +407 -756
  2. package/README.md +275 -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 +968 -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 +204 -113
  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 +65 -14
  23. package/dist/structures/Player.d.ts.map +1 -1
  24. package/dist/structures/Player.js +330 -88
  25. package/dist/structures/Player.js.map +1 -1
  26. package/dist/structures/PlayerManager.d.ts +127 -91
  27. package/dist/structures/PlayerManager.d.ts.map +1 -1
  28. package/dist/structures/PlayerManager.js +437 -124
  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 +46 -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 +74 -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 +3 -2
  43. package/src/extensions/BaseExtension.ts +1 -0
  44. package/src/extensions/index.ts +320 -37
  45. package/src/persistence/PersistenceManager.ts +1073 -0
  46. package/src/plugins/BasePlugin.ts +1 -1
  47. package/src/plugins/index.ts +248 -133
  48. package/src/structures/FilterManager.ts +3 -3
  49. package/src/structures/Player.ts +358 -94
  50. package/src/structures/PlayerManager.ts +535 -129
  51. package/src/structures/Queue.ts +300 -55
  52. package/src/types/index.ts +52 -10
  53. package/src/types/persistence.ts +83 -0
  54. package/src/types/plugin.ts +1 -1
@@ -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,158 +298,237 @@ class PlayerManager extends events_1.EventEmitter {
205
298
  }
206
299
  }
207
300
  }
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));
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) {
310
+ const forwardEvents = {
311
+ willPlay: "willPlay",
312
+ trackStart: "trackStart",
313
+ trackEnd: "trackEnd",
314
+ queueEnd: "queueEnd",
315
+ playerError: "playerError",
316
+ connectionError: "connectionError",
317
+ volumeChange: "volumeChange",
318
+ queueAdd: "queueAdd",
319
+ queueAddList: "queueAddList",
320
+ queueRemove: "queueRemove",
321
+ playerPause: "playerPause",
322
+ playerResume: "playerResume",
323
+ playerStop: "playerStop",
324
+ ttsStart: "ttsStart",
325
+ ttsEnd: "ttsEnd",
326
+ };
327
+ for (const [sourceEvent, targetEvent] of Object.entries(forwardEvents)) {
328
+ player.on(sourceEvent, (...args) => {
329
+ if (sourceEvent === "trackStart") {
330
+ player._lastActivity = Date.now();
331
+ }
332
+ this.emit(targetEvent, player, ...args);
333
+ });
334
+ }
222
335
  player.on("playerDestroy", () => {
223
336
  this.emit("playerDestroy", player);
224
337
  this.players.delete(guildId);
338
+ this.debug(`Player destroyed for guildId: ${guildId}`);
225
339
  });
226
- player.on("ttsStart", (payload) => this.emit("ttsStart", player, payload));
227
- player.on("ttsEnd", () => this.emit("ttsEnd", player));
228
340
  player.on("debug", (...args) => {
229
341
  if (this.listenerCount("debug") > 0) {
230
342
  this.emit("debug", ...args);
231
343
  }
232
344
  });
233
- this.players.set(guildId, player);
234
- return player;
235
345
  }
236
346
  /**
237
347
  * Get an existing player for a guild
238
348
  *
239
349
  * @param {string | {id: string}} guildOrId - Guild ID or guild object
240
350
  * @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
351
  */
262
352
  get(guildOrId) {
263
353
  const guildId = this.resolveGuildId(guildOrId);
264
- return this.players.get(guildId);
354
+ const player = this.players.get(guildId);
355
+ if (player) {
356
+ player._lastActivity = Date.now();
357
+ }
358
+ return player;
265
359
  }
266
360
  /**
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
- * }
361
+ * Get an existing player for a guild (alias for get)
276
362
  */
277
363
  getPlayer(guildOrId) {
278
- const guildId = this.resolveGuildId(guildOrId);
279
- return this.players.get(guildId);
364
+ return this.get(guildOrId);
280
365
  }
281
366
  /**
282
367
  * Get all players
283
368
  *
284
369
  * @returns {Player[]} All player instances
285
- * @example
286
- * const players = manager.getall();
287
- * console.log(`Players: ${players.length}`);
288
370
  */
289
- getall() {
371
+ getAll() {
290
372
  return Array.from(this.players.values());
291
373
  }
374
+ /**
375
+ * Alias for getAll
376
+ */
377
+ getall() {
378
+ return this.getAll();
379
+ }
380
+ /**
381
+ * Get players by filter
382
+ *
383
+ * @param {(player: Player) => boolean} filter - Filter function
384
+ * @returns {Player[]} Filtered player instances
385
+ */
386
+ getPlayersByFilter(filter) {
387
+ return this.getAll().filter(filter);
388
+ }
389
+ /**
390
+ * Get players in a voice channel
391
+ *
392
+ * @param {string} channelId - Voice channel ID
393
+ * @returns {Player[]} Players in the channel
394
+ */
395
+ getPlayersInChannel(channelId) {
396
+ return this.getAll().filter((p) => p.connection?.joinConfig.channelId === channelId);
397
+ }
292
398
  /**
293
399
  * Destroy a player and clean up resources
294
400
  *
295
401
  * @param {string | {id: string}} guildOrId - Guild ID or guild object
296
402
  * @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
403
  */
316
404
  delete(guildOrId) {
317
405
  const guildId = this.resolveGuildId(guildOrId);
318
406
  const player = this.players.get(guildId);
319
407
  if (player) {
320
- this.debug(`[PlayerManager] Deleting player for guildId: ${guildId}`);
408
+ this.debug(`Deleting player for guildId: ${guildId}`);
321
409
  player.destroy();
322
- return this.players.delete(guildId);
410
+ return true;
323
411
  }
324
412
  return false;
325
413
  }
414
+ /**
415
+ * Destroy multiple players by filter
416
+ *
417
+ * @param {(player: Player) => boolean} filter - Filter function
418
+ * @returns {number} Number of players destroyed
419
+ */
420
+ deleteWhere(filter) {
421
+ const toDelete = this.getPlayersByFilter(filter);
422
+ let count = 0;
423
+ for (const player of toDelete) {
424
+ const guildId = player.guildId;
425
+ player.destroy();
426
+ this.players.delete(guildId);
427
+ count++;
428
+ }
429
+ if (count > 0) {
430
+ this.debug(`Deleted ${count} players by filter`);
431
+ }
432
+ return count;
433
+ }
326
434
  /**
327
435
  * Check if a player exists for a guild
328
436
  *
329
437
  * @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}`);
438
+ * @returns {boolean} True if player exists
334
439
  */
335
440
  has(guildOrId) {
336
441
  const guildId = this.resolveGuildId(guildOrId);
337
442
  return this.players.has(guildId);
338
443
  }
444
+ /**
445
+ * Get number of players
446
+ */
339
447
  get size() {
340
448
  return this.players.size;
341
449
  }
450
+ /**
451
+ * Check if debug is enabled
452
+ */
342
453
  get debugEnabled() {
343
454
  return this.B_debug;
344
455
  }
345
456
  /**
346
- * Destroy all players
457
+ * Get manager statistics
458
+ *
459
+ * @returns {PlayerStats} Statistics about players
460
+ */
461
+ getStats() {
462
+ let activePlayers = 0;
463
+ let pausedPlayers = 0;
464
+ let connectedPlayers = 0;
465
+ let totalTracksInQueue = 0;
466
+ for (const player of this.players.values()) {
467
+ if (player.isPlaying)
468
+ activePlayers++;
469
+ if (player.isPaused)
470
+ pausedPlayers++;
471
+ if (player.connection)
472
+ connectedPlayers++;
473
+ totalTracksInQueue += player.queueSize;
474
+ }
475
+ return {
476
+ totalPlayers: this.players.size,
477
+ activePlayers,
478
+ pausedPlayers,
479
+ connectedPlayers,
480
+ totalTracksInQueue,
481
+ };
482
+ }
483
+ /**
484
+ * Broadcast an action to all players
347
485
  *
348
- * @returns {void}
486
+ * @param {string} action - Action to perform
487
+ * @param {...any[]} args - Arguments for the action
349
488
  * @example
350
- * manager.destroy();
351
- * console.log(`All players destroyed`);
489
+ * manager.broadcast("setVolume", 50);
490
+ * manager.broadcast("pause");
491
+ */
492
+ broadcast(action, ...args) {
493
+ for (const player of this.players.values()) {
494
+ if (typeof player[action] === "function") {
495
+ try {
496
+ player[action](...args);
497
+ }
498
+ catch (error) {
499
+ this.debug(`Error broadcasting ${action} to ${player.guildId}:`, error);
500
+ }
501
+ }
502
+ }
503
+ }
504
+ /**
505
+ * Destroy all players and clean up
352
506
  */
353
507
  destroy() {
354
- this.debug(`[PlayerManager] Destroying all players`);
508
+ this.debug(`Destroying all players`);
509
+ if (this.persistenceManager) {
510
+ this.persistenceManager.saveAll().catch((err) => {
511
+ this.debug("Failed to save players before destroy:", err);
512
+ });
513
+ this.persistenceManager.shutdown().catch(console.error);
514
+ }
515
+ // Stop cleanup intervals
516
+ if (this.cleanupInterval) {
517
+ clearInterval(this.cleanupInterval);
518
+ this.cleanupInterval = null;
519
+ }
520
+ if (this.statsInterval) {
521
+ clearInterval(this.statsInterval);
522
+ this.statsInterval = null;
523
+ }
524
+ // Destroy all players
355
525
  for (const player of this.players.values()) {
356
526
  player.destroy();
357
527
  }
358
528
  this.players.clear();
529
+ this.searchCache.clear();
359
530
  this.removeAllListeners();
531
+ this.debug(`PlayerManager destroyed`);
360
532
  }
361
533
  /**
362
534
  * Search using registered plugins without creating a Player.
@@ -364,28 +536,169 @@ class PlayerManager extends events_1.EventEmitter {
364
536
  * @param {string} query - The query to search for
365
537
  * @param {string} requestedBy - The user ID who requested the search
366
538
  * @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
539
  */
371
540
  async search(query, requestedBy) {
372
- this.debug(`[PlayerManager] Search called with query: ${query}, requestedBy: ${requestedBy}`);
541
+ this.debug(`Search called with query: ${query}, requestedBy: ${requestedBy}`);
542
+ // Check cache first
543
+ const cached = this.getCachedSearch(query);
544
+ if (cached) {
545
+ return cached;
546
+ }
373
547
  const plugin = this.plugins.find((p) => p.canHandle(query));
374
548
  if (!plugin) {
375
- this.debug(`[PlayerManager] No plugin found to handle: ${query}`);
549
+ this.debug(`No plugin found to handle: ${query}`);
376
550
  throw new Error(`No plugin found to handle: ${query}`);
377
551
  }
378
552
  try {
379
- return await this.withTimeout(plugin.search(query, requestedBy), "Search operation timed out");
553
+ const result = await this.withTimeout(plugin.search(query, requestedBy), "Search operation timed out");
554
+ this.setCachedSearch(query, result);
555
+ return result;
380
556
  }
381
557
  catch (error) {
382
- this.debug(`[PlayerManager] Search error:`, error);
558
+ this.debug(`Search error:`, error);
383
559
  throw error;
384
560
  }
385
561
  }
562
+ /**
563
+ * Clear search cache
564
+ */
565
+ clearSearchCache() {
566
+ const size = this.searchCache.size;
567
+ this.searchCache.clear();
568
+ this.debug(`Cleared ${size} search cache entries`);
569
+ }
570
+ /**
571
+ * Register a plugin after initialization
572
+ *
573
+ * @param {SourcePlugin} plugin - Plugin to register
574
+ */
575
+ registerPlugin(plugin) {
576
+ this.plugins.push(plugin);
577
+ this.debug(`Registered plugin: ${plugin.name}`);
578
+ // Register plugin with all existing players
579
+ for (const player of this.players.values()) {
580
+ player.addPlugin(plugin);
581
+ }
582
+ }
583
+ /**
584
+ * Unregister a plugin
585
+ *
586
+ * @param {string} name - Plugin name to unregister
587
+ * @returns {boolean} True if plugin was unregistered
588
+ */
589
+ unregisterPlugin(name) {
590
+ const index = this.plugins.findIndex((p) => p.name === name);
591
+ if (index === -1)
592
+ return false;
593
+ this.plugins.splice(index, 1);
594
+ this.debug(`Unregistered plugin: ${name}`);
595
+ // Note: Cannot easily remove plugins from existing players
596
+ return true;
597
+ }
598
+ /**
599
+ * Get all registered plugins
600
+ */
601
+ getPlugins() {
602
+ return [...this.plugins];
603
+ }
604
+ /**
605
+ * Register an extension after initialization
606
+ *
607
+ * @param {BaseExtension} extension - Extension to register
608
+ */
609
+ registerExtension(extension) {
610
+ this.extensions.push(extension);
611
+ this.debug(`Registered extension: ${extension.name}`);
612
+ // Register extension with all existing players
613
+ for (const player of this.players.values()) {
614
+ player.attachExtension(extension);
615
+ }
616
+ }
617
+ /**
618
+ * Get manager configuration
619
+ */
620
+ getConfig() {
621
+ return {
622
+ extractorTimeout: this.extractorTimeout,
623
+ autoCleanup: this.autoCleanup,
624
+ cleanupTimeout: this.cleanupTimeout,
625
+ enableSearchCache: this.enableSearchCache,
626
+ pluginsCount: this.plugins.length,
627
+ extensionsCount: this.extensions.length,
628
+ playersCount: this.players.size,
629
+ };
630
+ }
631
+ initPersistence(persistenceOptions) {
632
+ this.persistenceManager = new PersistenceManager_1.PersistenceManager(this, persistenceOptions);
633
+ const forwardEvents = {
634
+ playerSaved: "playerSaved",
635
+ playerLoaded: "playerLoaded",
636
+ playerSkipped: "RTSkipped",
637
+ playerMarkedDestroyed: "RTMarkedDestroyed",
638
+ playerDestroyedCleared: "RTDestroyedCleared",
639
+ savedAll: "savedAll",
640
+ loadedAll: "loadedAll",
641
+ backupsCleaned: "backupsCleaned",
642
+ allBackupsCleaned: "allBackupsCleaned",
643
+ backupStats: "backupStats",
644
+ backupCleanupDone: "backupCleanupDone",
645
+ };
646
+ for (const [sourceEvent, targetEvent] of Object.entries(forwardEvents)) {
647
+ this.persistenceManager.on(sourceEvent, (...args) => {
648
+ this.emit(targetEvent, ...args);
649
+ });
650
+ }
651
+ this.debug("Persistence manager initialized");
652
+ }
653
+ isAutoRestoreEnabled() {
654
+ return this.persistenceManager?.isAutoRestoreEnabled() ?? false;
655
+ }
656
+ getDestroyedPlayers() {
657
+ return this.persistenceManager?.getDestroyedPlayers() ?? [];
658
+ }
659
+ /**
660
+ * Get persistence manager
661
+ */
662
+ getPersistence() {
663
+ return this.persistenceManager;
664
+ }
665
+ /**
666
+ * Save all players
667
+ */
668
+ async saveAllPlayers() {
669
+ if (!this.persistenceManager) {
670
+ throw new Error("Persistence not enabled");
671
+ }
672
+ return await this.persistenceManager.saveAll();
673
+ }
674
+ /**
675
+ * Load all players
676
+ */
677
+ async loadAllPlayers(restorePosition = true) {
678
+ if (!this.persistenceManager) {
679
+ throw new Error("Persistence not enabled");
680
+ }
681
+ return await this.persistenceManager.loadAll(restorePosition);
682
+ }
683
+ /**
684
+ * Save a specific player
685
+ */
686
+ async savePlayer(guildId) {
687
+ if (!this.persistenceManager)
688
+ return false;
689
+ const player = this.get(guildId);
690
+ if (!player)
691
+ return false;
692
+ return await this.persistenceManager.savePlayer(player);
693
+ }
386
694
  }
387
695
  exports.PlayerManager = PlayerManager;
388
696
  PlayerManager.instance = null;
697
+ /**
698
+ * Get the global PlayerManager instance
699
+ *
700
+ * @returns {PlayerManager | null} Global instance or null
701
+ */
389
702
  function getInstance() {
390
703
  const globalInst = (0, exports.getGlobalManager)();
391
704
  if (!globalInst) {