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.
- package/AI-Guide.md +407 -756
- package/README.md +275 -10
- 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 +95 -0
- package/dist/persistence/PersistenceManager.d.ts.map +1 -0
- package/dist/persistence/PersistenceManager.js +968 -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 +204 -113
- 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 +65 -14
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +330 -88
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +127 -91
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +437 -124
- 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 +46 -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 +74 -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 +3 -2
- package/src/extensions/BaseExtension.ts +1 -0
- package/src/extensions/index.ts +320 -37
- package/src/persistence/PersistenceManager.ts +1073 -0
- package/src/plugins/BasePlugin.ts +1 -1
- package/src/plugins/index.ts +248 -133
- package/src/structures/FilterManager.ts +3 -3
- package/src/structures/Player.ts +358 -94
- package/src/structures/PlayerManager.ts +535 -129
- package/src/structures/Queue.ts +300 -55
- package/src/types/index.ts +52 -10
- package/src/types/persistence.ts +83 -0
- 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 {
|
|
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
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
345
|
+
this.debug(`Extension ${extInstance?.name} active check returned: ${activated}`);
|
|
207
346
|
} catch (e) {
|
|
208
347
|
activated = false;
|
|
209
|
-
this.debug(`
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
player
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(`
|
|
485
|
+
this.debug(`Deleting player for guildId: ${guildId}`);
|
|
338
486
|
player.destroy();
|
|
339
|
-
return
|
|
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
|
|
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
|
-
*
|
|
568
|
+
* Broadcast an action to all players
|
|
367
569
|
*
|
|
368
|
-
* @
|
|
570
|
+
* @param {string} action - Action to perform
|
|
571
|
+
* @param {...any[]} args - Arguments for the action
|
|
369
572
|
* @example
|
|
370
|
-
* manager.
|
|
371
|
-
*
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
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(`
|
|
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) {
|