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