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