lumisjs 1.0.0
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/LICENSE +21 -0
- package/README.md +274 -0
- package/SECURITY.md +160 -0
- package/index.js +90 -0
- package/package.json +68 -0
- package/src/cache/CacheManager.js +393 -0
- package/src/cache/MemoryAdapter.js +259 -0
- package/src/cache/MultiLevelCache.js +362 -0
- package/src/cache/RedisAdapter.js +329 -0
- package/src/cache/SQLiteAdapter.js +280 -0
- package/src/cache/index.js +15 -0
- package/src/client/Client.js +554 -0
- package/src/client/ShardingManager.js +274 -0
- package/src/config/ConfigValidator.js +172 -0
- package/src/config/index.js +7 -0
- package/src/dashboard/DashboardManager.js +362 -0
- package/src/datagen/DataGenerator.js +47 -0
- package/src/datagen/apis/GraphqlMocker.js +16 -0
- package/src/datagen/apis/OpenApiMocker.js +18 -0
- package/src/datagen/cli.js +130 -0
- package/src/datagen/formatters/csv.js +45 -0
- package/src/datagen/formatters/index.js +15 -0
- package/src/datagen/formatters/json.js +33 -0
- package/src/datagen/formatters/mongo.js +22 -0
- package/src/datagen/formatters/sql.js +32 -0
- package/src/datagen/index.js +17 -0
- package/src/datagen/plugin/PluginManager.js +43 -0
- package/src/datagen/plugins/example-plugin.js +21 -0
- package/src/datagen/schema/SchemaGenerator.js +172 -0
- package/src/datagen/schema/loadSchema.js +14 -0
- package/src/datagen/utils/seed.js +26 -0
- package/src/di/ServiceContainer.js +229 -0
- package/src/errors/ErrorCodes.js +110 -0
- package/src/errors/LumisError.js +85 -0
- package/src/game/EconomyManager.js +161 -0
- package/src/game/GameSessionManager.js +76 -0
- package/src/game/GuildManager.js +197 -0
- package/src/game/InventoryManager.js +140 -0
- package/src/game/LevelingSystem.js +194 -0
- package/src/game/MusicManager.js +587 -0
- package/src/health/HealthChecker.js +170 -0
- package/src/health/index.js +7 -0
- package/src/performance/PerformanceMonitor.js +244 -0
- package/src/performance/index.js +7 -0
- package/src/security/InputSanitizer.js +185 -0
- package/src/security/SecurityMiddleware.js +231 -0
- package/src/security/index.js +9 -0
- package/src/shutdown/GracefulShutdown.js +159 -0
- package/src/shutdown/index.js +7 -0
- package/src/utils/StructuredLogger.js +118 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events');
|
|
4
|
+
const { request } = require('undici');
|
|
5
|
+
const Collection = require('../utils/Collection');
|
|
6
|
+
const { Events } = require('../utils/Constants');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_OPTIONS = {
|
|
9
|
+
defaultSearchPlatform: 'ytsearch',
|
|
10
|
+
nodes: []
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
class LavalinkNode {
|
|
14
|
+
constructor({ host, port, password, secure = false, name } = {}) {
|
|
15
|
+
if (!host) throw new TypeError('lavalink node host is required');
|
|
16
|
+
if (!port) throw new TypeError('lavalink node port is required');
|
|
17
|
+
if (!password) throw new TypeError('lavalink node password is required');
|
|
18
|
+
|
|
19
|
+
this.host = host;
|
|
20
|
+
this.port = port;
|
|
21
|
+
this.password = password;
|
|
22
|
+
this.secure = Boolean(secure);
|
|
23
|
+
this.name = name || `${host}:${port}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get baseURL() {
|
|
27
|
+
return `${this.secure ? 'https' : 'http'}://${this.host}:${this.port}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async loadTracks(identifier) {
|
|
31
|
+
const url = `${this.baseURL}/loadtracks?identifier=${encodeURIComponent(identifier)}`;
|
|
32
|
+
const res = await request(url, {
|
|
33
|
+
headers: { Authorization: this.password }
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (res.statusCode !== 200) {
|
|
37
|
+
const body = await res.body.text();
|
|
38
|
+
throw new Error(`Lavalink loadtracks failed (${res.statusCode}): ${body}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return res.body.json();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async createSession(guildId, body) {
|
|
45
|
+
const url = `${this.baseURL}/v4/session/${guildId}`;
|
|
46
|
+
const res = await request(url, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: this.password,
|
|
50
|
+
'Content-Type': 'application/json'
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(body)
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (res.statusCode !== 204) {
|
|
56
|
+
const text = await res.body.text();
|
|
57
|
+
throw new Error(`Lavalink session create failed (${res.statusCode}): ${text}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async play(guildId, track, options = {}) {
|
|
64
|
+
const url = `${this.baseURL}/v4/player/${guildId}/play`;
|
|
65
|
+
|
|
66
|
+
const body = {
|
|
67
|
+
track,
|
|
68
|
+
noReplace: options.noReplace ?? false,
|
|
69
|
+
pause: options.pause ?? false,
|
|
70
|
+
startTime: options.startTime ?? 0,
|
|
71
|
+
endTime: options.endTime ?? 0,
|
|
72
|
+
volume: options.volume ?? 100
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const res = await request(url, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
Authorization: this.password,
|
|
79
|
+
'Content-Type': 'application/json'
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify(body)
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (res.statusCode !== 204) {
|
|
85
|
+
const text = await res.body.text();
|
|
86
|
+
throw new Error(`Lavalink play failed (${res.statusCode}): ${text}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async stop(guildId) {
|
|
93
|
+
const url = `${this.baseURL}/v4/player/${guildId}/stop`;
|
|
94
|
+
const res = await request(url, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { Authorization: this.password }
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (res.statusCode !== 204) {
|
|
100
|
+
const text = await res.body.text();
|
|
101
|
+
throw new Error(`Lavalink stop failed (${res.statusCode}): ${text}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class MusicManager extends EventEmitter {
|
|
109
|
+
constructor(client, options = {}) {
|
|
110
|
+
super();
|
|
111
|
+
|
|
112
|
+
this.client = client;
|
|
113
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Lavalink nodes (optional). Example:
|
|
117
|
+
* [{ host: '127.0.0.1', port: 2333, password: 'yours', secure: false }]
|
|
118
|
+
*/
|
|
119
|
+
this.nodes = (this.options.nodes || []).map((node) => new LavalinkNode(node));
|
|
120
|
+
this._nodeIndex = 0;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Map of guild id -> player
|
|
124
|
+
* @type {Collection<string, MusicPlayer>}
|
|
125
|
+
*/
|
|
126
|
+
this.players = new Collection();
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Backward-compatible queue storage
|
|
130
|
+
* @type {Collection<string, Object>}
|
|
131
|
+
*/
|
|
132
|
+
this.queues = new Collection();
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Cache voice session data needed for Lavalink voice session updates.
|
|
136
|
+
* @type {Map<string, any>}
|
|
137
|
+
*/
|
|
138
|
+
this.voiceStates = new Map();
|
|
139
|
+
this.voiceServers = new Map();
|
|
140
|
+
|
|
141
|
+
this.client.on(Events.VOICE_STATE_UPDATE, (data) => this.updateVoiceState(data));
|
|
142
|
+
this.client.on(Events.VOICE_SERVER_UPDATE, (data) => this.updateVoiceServer(data));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_getNextNode() {
|
|
146
|
+
if (!this.nodes.length) return null;
|
|
147
|
+
const node = this.nodes[this._nodeIndex % this.nodes.length];
|
|
148
|
+
this._nodeIndex += 1;
|
|
149
|
+
return node;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_formatSearchQuery(query) {
|
|
153
|
+
if (typeof query !== 'string') return query;
|
|
154
|
+
if (query.startsWith('http://') || query.startsWith('https://')) return query;
|
|
155
|
+
return `${this.options.defaultSearchPlatform}:${query}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create or fetch an existing player for a guild.
|
|
160
|
+
* @param {Object} options
|
|
161
|
+
* @param {string} options.guildId
|
|
162
|
+
* @param {string} options.voiceChannel
|
|
163
|
+
* @param {string} [options.textChannel]
|
|
164
|
+
* @param {boolean} [options.deaf=true]
|
|
165
|
+
* @param {boolean} [options.mute=false]
|
|
166
|
+
*/
|
|
167
|
+
createConnection({ guildId, voiceChannel, textChannel = null, deaf = true, mute = false } = {}) {
|
|
168
|
+
if (!guildId) throw new TypeError('guildId is required');
|
|
169
|
+
if (!voiceChannel) throw new TypeError('voiceChannel is required');
|
|
170
|
+
|
|
171
|
+
let player = this.players.get(guildId);
|
|
172
|
+
if (player) return player;
|
|
173
|
+
|
|
174
|
+
const node = this._getNextNode();
|
|
175
|
+
|
|
176
|
+
this.joinVoiceChannel(guildId, voiceChannel, { deaf, mute });
|
|
177
|
+
|
|
178
|
+
player = new MusicPlayer(this, {
|
|
179
|
+
guildId,
|
|
180
|
+
voiceChannel,
|
|
181
|
+
textChannel,
|
|
182
|
+
deaf,
|
|
183
|
+
mute,
|
|
184
|
+
node
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
this.players.set(guildId, player);
|
|
188
|
+
return player;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolve a search query into a track/playlist result.
|
|
193
|
+
* This is a minimal stub implementation. For real usage, replace with a proper search provider.
|
|
194
|
+
* @param {Object} options
|
|
195
|
+
* @param {string} options.query
|
|
196
|
+
* @param {any} [options.requester]
|
|
197
|
+
*/
|
|
198
|
+
async resolve({ query, requester = null } = {}) {
|
|
199
|
+
if (!query) throw new TypeError('query is required');
|
|
200
|
+
|
|
201
|
+
// If nodes are configured, use Lavalink to resolve the query.
|
|
202
|
+
// Otherwise, fallback to a simple stub implementation.
|
|
203
|
+
const node = this._getNextNode();
|
|
204
|
+
if (node) {
|
|
205
|
+
const identifier = this._formatSearchQuery(query);
|
|
206
|
+
const data = await node.loadTracks(identifier);
|
|
207
|
+
|
|
208
|
+
const tracks = (data.tracks || []).map((t) => ({
|
|
209
|
+
title: t.info.title,
|
|
210
|
+
uri: t.info.uri,
|
|
211
|
+
identifier: t.info.identifier,
|
|
212
|
+
author: t.info.author,
|
|
213
|
+
duration: t.info.length,
|
|
214
|
+
isStream: t.info.isStream,
|
|
215
|
+
requester,
|
|
216
|
+
track: t.track,
|
|
217
|
+
raw: t
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
loadType: data.loadType,
|
|
222
|
+
tracks,
|
|
223
|
+
playlistInfo: data.playlistInfo || null
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const track = this._buildTrack(query, requester);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
loadType: 'search',
|
|
231
|
+
tracks: [track],
|
|
232
|
+
playlistInfo: null
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Build a track object similar to Aqualink's track shape.
|
|
238
|
+
* @param {string|Object} query
|
|
239
|
+
* @param {any} requester
|
|
240
|
+
*/
|
|
241
|
+
_buildTrack(query, requester) {
|
|
242
|
+
const identifier = typeof query === 'string' ? query : query.uri || query.identifier;
|
|
243
|
+
const title = typeof query === 'string' ? query : query.title || identifier;
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
title,
|
|
247
|
+
uri: identifier || '',
|
|
248
|
+
identifier: identifier || '',
|
|
249
|
+
author: (requester && requester.username) || 'Lumis',
|
|
250
|
+
duration: 0,
|
|
251
|
+
isStream: false,
|
|
252
|
+
requester
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async joinVoiceChannel(guildId, channelId, options = {}) {
|
|
257
|
+
if (!this.client.ws || !this.client.ws.ws) {
|
|
258
|
+
this.client.logger.error('No WebSocket connection, cannot join voice channel.');
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const { deaf = true, mute = false } = options;
|
|
263
|
+
|
|
264
|
+
const payload = {
|
|
265
|
+
op: 4,
|
|
266
|
+
d: {
|
|
267
|
+
guild_id: String(guildId),
|
|
268
|
+
channel_id: String(channelId),
|
|
269
|
+
self_mute: Boolean(mute),
|
|
270
|
+
self_deaf: Boolean(deaf)
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
this.client.ws.ws.send(JSON.stringify(payload));
|
|
275
|
+
|
|
276
|
+
if (!this.queues.has(guildId)) {
|
|
277
|
+
this.queues.set(guildId, {
|
|
278
|
+
textChannel: null,
|
|
279
|
+
voiceChannel: channelId,
|
|
280
|
+
connection: null, // İleride WebRTC bağlantısı eklenebilir
|
|
281
|
+
songs: [],
|
|
282
|
+
volume: 100,
|
|
283
|
+
playing: false
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
const queue = this.queues.get(guildId);
|
|
287
|
+
queue.voiceChannel = channelId;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.client.logger.info(`Voice channel join request sent: ${channelId}`);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async leaveVoiceChannel(guildId) {
|
|
295
|
+
if (!this.client.ws || !this.client.ws.ws) return;
|
|
296
|
+
|
|
297
|
+
const payload = {
|
|
298
|
+
op: 4,
|
|
299
|
+
d: {
|
|
300
|
+
guild_id: String(guildId),
|
|
301
|
+
channel_id: null,
|
|
302
|
+
self_mute: false,
|
|
303
|
+
self_deaf: false
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
this.client.ws.ws.send(JSON.stringify(payload));
|
|
308
|
+
|
|
309
|
+
this.queues.delete(guildId);
|
|
310
|
+
this.voiceStates.delete(guildId);
|
|
311
|
+
this.voiceServers.delete(guildId);
|
|
312
|
+
|
|
313
|
+
const player = this.players.get(guildId);
|
|
314
|
+
if (player) player.destroy();
|
|
315
|
+
this.players.delete(guildId);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async updateVoiceState(data) {
|
|
319
|
+
const { guild_id: guildId } = data;
|
|
320
|
+
this.voiceStates.set(guildId, data);
|
|
321
|
+
|
|
322
|
+
const player = this.players.get(guildId);
|
|
323
|
+
if (!player) return;
|
|
324
|
+
|
|
325
|
+
player.voiceState = data;
|
|
326
|
+
await this._sendLavalinkVoiceUpdate(player);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async updateVoiceServer(data) {
|
|
330
|
+
const { guild_id: guildId } = data;
|
|
331
|
+
this.voiceServers.set(guildId, data);
|
|
332
|
+
|
|
333
|
+
const player = this.players.get(guildId);
|
|
334
|
+
if (!player) return;
|
|
335
|
+
|
|
336
|
+
player.voiceServer = data;
|
|
337
|
+
await this._sendLavalinkVoiceUpdate(player);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async _sendLavalinkVoiceUpdate(player) {
|
|
341
|
+
if (!player.voiceServer || !player.voiceState || !player.node) return;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
await player.node.createSession(player.guildId, {
|
|
345
|
+
voiceServer: player.voiceServer,
|
|
346
|
+
voiceState: player.voiceState
|
|
347
|
+
});
|
|
348
|
+
} catch (error) {
|
|
349
|
+
this.client.logger.error(`Lavalink voice update failed for guild ${player.guildId}: ${error.message}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Backwards compatible helper.
|
|
355
|
+
* @param {string} guildId
|
|
356
|
+
*/
|
|
357
|
+
getQueue(guildId) {
|
|
358
|
+
const player = this.players.get(guildId);
|
|
359
|
+
if (player) return player.queue;
|
|
360
|
+
return this.queues.get(guildId) || null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Backwards-compatible helper to add a track to a guild queue.
|
|
365
|
+
* @param {string} guildId
|
|
366
|
+
* @param {Object} song
|
|
367
|
+
*/
|
|
368
|
+
addTrack(guildId, song) {
|
|
369
|
+
const player = this.players.get(guildId);
|
|
370
|
+
if (player) {
|
|
371
|
+
player.queue.add(song);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const queue = this.queues.get(guildId);
|
|
376
|
+
if (!queue) {
|
|
377
|
+
this.client.logger.warn(`Queue not found. Please join a voice channel first (${guildId}).`);
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
queue.songs.push(song);
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Backwards-compatible play helper.
|
|
387
|
+
* @param {string} guildId
|
|
388
|
+
*/
|
|
389
|
+
play(guildId) {
|
|
390
|
+
const player = this.players.get(guildId);
|
|
391
|
+
if (player) return player.play();
|
|
392
|
+
|
|
393
|
+
const queue = this.queues.get(guildId);
|
|
394
|
+
if (!queue || queue.songs.length === 0) return false;
|
|
395
|
+
|
|
396
|
+
queue.playing = true;
|
|
397
|
+
const currentSong = queue.songs[0];
|
|
398
|
+
|
|
399
|
+
this.client.logger.info(`(SIMULATED) Now playing: ${currentSong.title} (Duration: ${currentSong.duration})`);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Backwards-compatible skip helper.
|
|
405
|
+
* @param {string} guildId
|
|
406
|
+
*/
|
|
407
|
+
skip(guildId) {
|
|
408
|
+
const player = this.players.get(guildId);
|
|
409
|
+
if (player) return player.skip();
|
|
410
|
+
|
|
411
|
+
const queue = this.queues.get(guildId);
|
|
412
|
+
if (!queue || queue.songs.length === 0) return false;
|
|
413
|
+
|
|
414
|
+
const skipped = queue.songs.shift();
|
|
415
|
+
|
|
416
|
+
if (queue.songs.length > 0) {
|
|
417
|
+
this.play(guildId);
|
|
418
|
+
} else {
|
|
419
|
+
queue.playing = false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return skipped;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
class PlayerQueue {
|
|
427
|
+
constructor(player) {
|
|
428
|
+
this.player = player;
|
|
429
|
+
this.tracks = [];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
add(...tracks) {
|
|
433
|
+
this.tracks.push(...tracks);
|
|
434
|
+
return this.tracks.length;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
shift() {
|
|
438
|
+
return this.tracks.shift();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
remove(index) {
|
|
442
|
+
if (index < 0 || index >= this.tracks.length) return null;
|
|
443
|
+
return this.tracks.splice(index, 1)[0];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
clear() {
|
|
447
|
+
this.tracks = [];
|
|
448
|
+
return this.tracks;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
shuffle() {
|
|
452
|
+
for (let i = this.tracks.length - 1; i > 0; i -= 1) {
|
|
453
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
454
|
+
[this.tracks[i], this.tracks[j]] = [this.tracks[j], this.tracks[i]];
|
|
455
|
+
}
|
|
456
|
+
return this.tracks;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
get size() {
|
|
460
|
+
return this.tracks.length;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
get first() {
|
|
464
|
+
return this.tracks[0] || null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
toArray() {
|
|
468
|
+
return [...this.tracks];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
class MusicPlayer {
|
|
473
|
+
constructor(manager, options = {}) {
|
|
474
|
+
this.manager = manager;
|
|
475
|
+
this.guildId = options.guildId;
|
|
476
|
+
this.voiceChannel = options.voiceChannel;
|
|
477
|
+
this.textChannel = options.textChannel || null;
|
|
478
|
+
|
|
479
|
+
this.deaf = Boolean(options.deaf);
|
|
480
|
+
this.mute = Boolean(options.mute);
|
|
481
|
+
|
|
482
|
+
this.queue = new PlayerQueue(this);
|
|
483
|
+
this.playing = false;
|
|
484
|
+
this.paused = false;
|
|
485
|
+
this.volume = 100;
|
|
486
|
+
this.current = null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async play() {
|
|
490
|
+
if (!this.queue.size) return false;
|
|
491
|
+
|
|
492
|
+
const next = this.queue.first;
|
|
493
|
+
if (!next) return false;
|
|
494
|
+
|
|
495
|
+
this.current = next;
|
|
496
|
+
this.playing = true;
|
|
497
|
+
this.paused = false;
|
|
498
|
+
|
|
499
|
+
if (this.node && next.track) {
|
|
500
|
+
try {
|
|
501
|
+
await this.node.play(this.guildId, next.track, { volume: this.volume });
|
|
502
|
+
} catch (error) {
|
|
503
|
+
this.manager.client.logger.error(`Lavalink play failed: ${error.message}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
this.manager.client.logger.info(`Now playing: ${next.title}`);
|
|
508
|
+
this.manager.emit('trackStart', this, next);
|
|
509
|
+
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async pause() {
|
|
514
|
+
if (!this.playing || this.paused) return false;
|
|
515
|
+
this.paused = true;
|
|
516
|
+
this.manager.emit('trackPause', this, this.current);
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async resume() {
|
|
521
|
+
if (!this.playing || !this.paused) return false;
|
|
522
|
+
this.paused = false;
|
|
523
|
+
this.manager.emit('trackResume', this, this.current);
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async skip() {
|
|
528
|
+
if (!this.playing) return false;
|
|
529
|
+
|
|
530
|
+
const skipped = this.queue.shift();
|
|
531
|
+
this.manager.emit('trackSkip', this, skipped);
|
|
532
|
+
|
|
533
|
+
if (this.node) {
|
|
534
|
+
try {
|
|
535
|
+
await this.node.stop(this.guildId);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
this.manager.client.logger.error(`Lavalink stop failed: ${error.message}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (this.queue.size > 0) {
|
|
542
|
+
await this.play();
|
|
543
|
+
return skipped;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
this.playing = false;
|
|
547
|
+
this.current = null;
|
|
548
|
+
this.manager.emit('queueEnd', this);
|
|
549
|
+
return skipped;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async stop() {
|
|
553
|
+
this.playing = false;
|
|
554
|
+
this.paused = false;
|
|
555
|
+
this.current = null;
|
|
556
|
+
this.queue.clear();
|
|
557
|
+
this.manager.emit('stop', this);
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
setVolume(volume) {
|
|
562
|
+
this.volume = Number(volume);
|
|
563
|
+
this.manager.emit('volumeChange', this, this.volume);
|
|
564
|
+
return this.volume;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async destroy() {
|
|
568
|
+
await this.stop();
|
|
569
|
+
this.manager.players.delete(this.guildId);
|
|
570
|
+
this.manager.queues.delete(this.guildId);
|
|
571
|
+
this.manager.emit('destroy', this);
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
MusicManager.Events = {
|
|
577
|
+
TrackStart: 'trackStart',
|
|
578
|
+
TrackPause: 'trackPause',
|
|
579
|
+
TrackResume: 'trackResume',
|
|
580
|
+
TrackSkip: 'trackSkip',
|
|
581
|
+
QueueEnd: 'queueEnd',
|
|
582
|
+
Stop: 'stop',
|
|
583
|
+
Destroy: 'destroy',
|
|
584
|
+
VolumeChange: 'volumeChange'
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
module.exports = MusicManager;
|