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