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