ziplayer 0.0.9 → 0.1.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/README.md +61 -30
- package/dist/extensions/BaseExtension.d.ts +9 -3
- package/dist/extensions/BaseExtension.d.ts.map +1 -1
- package/dist/extensions/BaseExtension.js.map +1 -1
- package/dist/structures/Player.d.ts +294 -2
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +586 -88
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +166 -2
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +182 -8
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +193 -2
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +193 -2
- package/dist/structures/Queue.js.map +1 -1
- package/dist/types/index.d.ts +327 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/timeout.d.ts +9 -0
- package/dist/utils/timeout.d.ts.map +1 -0
- package/dist/utils/timeout.js +14 -0
- package/dist/utils/timeout.js.map +1 -0
- package/package.json +1 -1
- package/src/extensions/BaseExtension.ts +35 -10
- package/src/structures/Player.ts +615 -90
- package/src/structures/PlayerManager.ts +189 -8
- package/src/structures/Queue.ts +196 -2
- package/src/types/index.ts +343 -4
- package/src/utils/timeout.ts +10 -0
|
@@ -5,77 +5,292 @@ const events_1 = require("events");
|
|
|
5
5
|
const voice_1 = require("@discordjs/voice");
|
|
6
6
|
const Queue_1 = require("./Queue");
|
|
7
7
|
const plugins_1 = require("../plugins");
|
|
8
|
+
const timeout_1 = require("../utils/timeout");
|
|
9
|
+
/**
|
|
10
|
+
* Represents a music player for a specific Discord guild.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Create and configure player
|
|
14
|
+
* const player = await manager.create(guildId, {
|
|
15
|
+
* tts: { interrupt: true, volume: 1 },
|
|
16
|
+
* leaveOnEnd: true,
|
|
17
|
+
* leaveTimeout: 30000
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // Connect to voice channel
|
|
21
|
+
* await player.connect(voiceChannel);
|
|
22
|
+
*
|
|
23
|
+
* // Play different types of content
|
|
24
|
+
* await player.play("Never Gonna Give You Up", userId); // Search query
|
|
25
|
+
* await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); // Direct URL
|
|
26
|
+
* await player.play("tts: Hello everyone!", userId); // Text-to-Speech
|
|
27
|
+
*
|
|
28
|
+
* // Player controls
|
|
29
|
+
* player.pause(); // Pause current track
|
|
30
|
+
* player.resume(); // Resume paused track
|
|
31
|
+
* player.skip(); // Skip to next track
|
|
32
|
+
* player.stop(); // Stop and clear queue
|
|
33
|
+
* player.setVolume(0.5); // Set volume to 50%
|
|
34
|
+
*
|
|
35
|
+
* // Event handling
|
|
36
|
+
* player.on("trackStart", (player, track) => {
|
|
37
|
+
* console.log(`Now playing: ${track.title}`);
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* player.on("queueEnd", (player) => {
|
|
41
|
+
* console.log("Queue finished");
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
*/
|
|
8
45
|
class Player extends events_1.EventEmitter {
|
|
9
46
|
/**
|
|
10
|
-
*
|
|
47
|
+
* Attach an extension to the player
|
|
48
|
+
*
|
|
49
|
+
* @param {BaseExtension} extension - The extension to attach
|
|
50
|
+
* @example
|
|
51
|
+
* player.attachExtension(new MyExtension());
|
|
11
52
|
*/
|
|
12
|
-
|
|
53
|
+
attachExtension(extension) {
|
|
54
|
+
if (this.extensions.includes(extension))
|
|
55
|
+
return;
|
|
56
|
+
if (!extension.player)
|
|
57
|
+
extension.player = this;
|
|
58
|
+
this.extensions.push(extension);
|
|
59
|
+
this.invokeExtensionLifecycle(extension, "onRegister");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Detach an extension from the player
|
|
63
|
+
*
|
|
64
|
+
* @param {BaseExtension} extension - The extension to detach
|
|
65
|
+
* @example
|
|
66
|
+
* player.detachExtension(new MyExtension());
|
|
67
|
+
*/
|
|
68
|
+
detachExtension(extension) {
|
|
69
|
+
const index = this.extensions.indexOf(extension);
|
|
70
|
+
if (index === -1)
|
|
71
|
+
return;
|
|
72
|
+
this.extensions.splice(index, 1);
|
|
73
|
+
this.invokeExtensionLifecycle(extension, "onDestroy");
|
|
74
|
+
if (extension.player === this) {
|
|
75
|
+
extension.player = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get all extensions attached to the player
|
|
80
|
+
*
|
|
81
|
+
* @returns {readonly BaseExtension[]} All attached extensions
|
|
82
|
+
* @example
|
|
83
|
+
* const extensions = player.getExtensions();
|
|
84
|
+
* console.log(`Extensions: ${extensions.length}`);
|
|
85
|
+
*/
|
|
86
|
+
getExtensions() {
|
|
87
|
+
return this.extensions;
|
|
88
|
+
}
|
|
89
|
+
invokeExtensionLifecycle(extension, hook) {
|
|
90
|
+
const fn = extension[hook];
|
|
91
|
+
if (typeof fn !== "function")
|
|
92
|
+
return;
|
|
13
93
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.debug(`[Player]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
94
|
+
const result = fn.call(extension, this.extensionContext);
|
|
95
|
+
if (result && typeof result.then === "function") {
|
|
96
|
+
result.catch((err) => this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async runBeforePlayHooks(initial) {
|
|
104
|
+
const request = { ...initial };
|
|
105
|
+
const response = {};
|
|
106
|
+
for (const extension of this.extensions) {
|
|
107
|
+
const hook = extension.beforePlay;
|
|
108
|
+
if (typeof hook !== "function")
|
|
109
|
+
continue;
|
|
110
|
+
try {
|
|
111
|
+
const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
|
|
112
|
+
if (!result)
|
|
113
|
+
continue;
|
|
114
|
+
if (result.query !== undefined) {
|
|
115
|
+
request.query = result.query;
|
|
116
|
+
response.query = result.query;
|
|
117
|
+
}
|
|
118
|
+
if (result.requestedBy !== undefined) {
|
|
119
|
+
request.requestedBy = result.requestedBy;
|
|
120
|
+
response.requestedBy = result.requestedBy;
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(result.tracks)) {
|
|
123
|
+
response.tracks = result.tracks;
|
|
124
|
+
}
|
|
125
|
+
if (typeof result.isPlaylist === "boolean") {
|
|
126
|
+
response.isPlaylist = result.isPlaylist;
|
|
127
|
+
}
|
|
128
|
+
if (typeof result.success === "boolean") {
|
|
129
|
+
response.success = result.success;
|
|
130
|
+
}
|
|
131
|
+
if (result.error instanceof Error) {
|
|
132
|
+
response.error = result.error;
|
|
133
|
+
}
|
|
134
|
+
if (typeof result.handled === "boolean") {
|
|
135
|
+
response.handled = result.handled;
|
|
136
|
+
if (result.handled)
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
this.debug(`[Player] Extension ${extension.name} beforePlay error:`, err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { request, response };
|
|
145
|
+
}
|
|
146
|
+
async runAfterPlayHooks(payload) {
|
|
147
|
+
if (this.extensions.length === 0)
|
|
148
|
+
return;
|
|
149
|
+
const safeTracks = payload.tracks ? [...payload.tracks] : undefined;
|
|
150
|
+
if (safeTracks) {
|
|
151
|
+
Object.freeze(safeTracks);
|
|
152
|
+
}
|
|
153
|
+
const immutablePayload = Object.freeze({ ...payload, tracks: safeTracks });
|
|
154
|
+
for (const extension of this.extensions) {
|
|
155
|
+
const hook = extension.afterPlay;
|
|
156
|
+
if (typeof hook !== "function")
|
|
157
|
+
continue;
|
|
24
158
|
try {
|
|
25
|
-
|
|
159
|
+
await Promise.resolve(hook.call(extension, this.extensionContext, immutablePayload));
|
|
26
160
|
}
|
|
27
|
-
catch (
|
|
28
|
-
this.debug(`[Player]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
161
|
+
catch (err) {
|
|
162
|
+
this.debug(`[Player] Extension ${extension.name} afterPlay error:`, err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async extensionsProvideSearch(query, requestedBy) {
|
|
167
|
+
const request = { query, requestedBy };
|
|
168
|
+
for (const extension of this.extensions) {
|
|
169
|
+
const hook = extension.provideSearch;
|
|
170
|
+
if (typeof hook !== "function")
|
|
171
|
+
continue;
|
|
172
|
+
try {
|
|
173
|
+
const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
|
|
174
|
+
if (result && Array.isArray(result.tracks) && result.tracks.length > 0) {
|
|
175
|
+
this.debug(`[Player] Extension ${extension.name} handled search for query: ${query}`);
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
this.debug(`[Player] Extension ${extension.name} provideSearch error:`, err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
async extensionsProvideStream(track) {
|
|
186
|
+
const request = { track };
|
|
187
|
+
for (const extension of this.extensions) {
|
|
188
|
+
const hook = extension.provideStream;
|
|
189
|
+
if (typeof hook !== "function")
|
|
190
|
+
continue;
|
|
191
|
+
try {
|
|
192
|
+
const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
|
|
193
|
+
if (result && result.stream) {
|
|
194
|
+
this.debug(`[Player] Extension ${extension.name} provided stream for track: ${track.title}`);
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
this.debug(`[Player] Extension ${extension.name} provideStream error:`, err);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Start playing a specific track immediately, replacing the current resource.
|
|
206
|
+
*/
|
|
207
|
+
async startTrack(track) {
|
|
208
|
+
try {
|
|
209
|
+
let streamInfo = await this.extensionsProvideStream(track);
|
|
210
|
+
let plugin;
|
|
211
|
+
if (!streamInfo) {
|
|
212
|
+
plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
|
|
213
|
+
if (!plugin) {
|
|
214
|
+
this.debug(`[Player] No plugin found for track: ${track.title}`);
|
|
215
|
+
throw new Error(`No plugin found for track: ${track.title}`);
|
|
216
|
+
}
|
|
217
|
+
this.debug(`[Player] Getting stream for track: ${track.title}`);
|
|
218
|
+
this.debug(`[Player] Using plugin: ${plugin.name}`);
|
|
219
|
+
this.debug(`[Track] Track Info:`, track);
|
|
220
|
+
try {
|
|
221
|
+
streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), this.options.extractorTimeout ?? 15000, "getStream timed out");
|
|
222
|
+
}
|
|
223
|
+
catch (streamError) {
|
|
224
|
+
this.debug(`[Player] getStream failed, trying getFallback:`, streamError);
|
|
225
|
+
const allplugs = this.pluginManager.getAll();
|
|
226
|
+
for (const p of allplugs) {
|
|
227
|
+
if (typeof p.getFallback !== "function") {
|
|
37
228
|
continue;
|
|
38
|
-
|
|
39
|
-
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), this.options.extractorTimeout ?? 15000, `getFallback timed out for plugin ${p.name}`);
|
|
232
|
+
if (!streamInfo?.stream)
|
|
233
|
+
continue;
|
|
234
|
+
this.debug(`[Player] getFallback succeeded with plugin ${p.name} for track: ${track.title}`);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
catch (fallbackError) {
|
|
238
|
+
this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError);
|
|
239
|
+
}
|
|
40
240
|
}
|
|
41
|
-
|
|
42
|
-
|
|
241
|
+
if (!streamInfo?.stream) {
|
|
242
|
+
throw new Error(`All getFallback attempts failed for track: ${track.title}`);
|
|
43
243
|
}
|
|
44
244
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.debug(`[Player] Using extension-provided stream for track: ${track.title}`);
|
|
248
|
+
}
|
|
249
|
+
if (plugin) {
|
|
48
250
|
this.debug(streamInfo);
|
|
49
251
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
252
|
+
// Kiểm tra nếu có stream thực sự để tạo AudioResource
|
|
253
|
+
if (streamInfo && streamInfo.stream) {
|
|
254
|
+
function mapToStreamType(type) {
|
|
255
|
+
switch (type) {
|
|
256
|
+
case "webm/opus":
|
|
257
|
+
return voice_1.StreamType.WebmOpus;
|
|
258
|
+
case "ogg/opus":
|
|
259
|
+
return voice_1.StreamType.OggOpus;
|
|
260
|
+
case "arbitrary":
|
|
261
|
+
default:
|
|
262
|
+
return voice_1.StreamType.Arbitrary;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const stream = streamInfo.stream;
|
|
266
|
+
const inputType = mapToStreamType(streamInfo.type);
|
|
267
|
+
this.currentResource = (0, voice_1.createAudioResource)(stream, {
|
|
268
|
+
metadata: track,
|
|
269
|
+
inputType,
|
|
270
|
+
inlineVolume: true,
|
|
271
|
+
});
|
|
272
|
+
// Apply initial volume using the resource's VolumeTransformer
|
|
273
|
+
if (this.volumeInterval) {
|
|
274
|
+
clearInterval(this.volumeInterval);
|
|
275
|
+
this.volumeInterval = null;
|
|
60
276
|
}
|
|
277
|
+
this.currentResource.volume?.setVolume(this.volume / 100);
|
|
278
|
+
this.debug(`[Player] Playing resource for track: ${track.title}`);
|
|
279
|
+
this.audioPlayer.play(this.currentResource);
|
|
280
|
+
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 5000);
|
|
281
|
+
return true;
|
|
61
282
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.volumeInterval = null;
|
|
283
|
+
else if (streamInfo && !streamInfo.stream) {
|
|
284
|
+
// Extension đang xử lý phát nhạc (như Lavalink) - chỉ đánh dấu đang phát
|
|
285
|
+
this.debug(`[Player] Extension is handling playback for track: ${track.title}`);
|
|
286
|
+
this.isPlaying = true;
|
|
287
|
+
this.isPaused = false;
|
|
288
|
+
this.emit("trackStart", track);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
throw new Error(`No stream available for track: ${track.title}`);
|
|
73
293
|
}
|
|
74
|
-
this.currentResource.volume?.setVolume(this.volume / 100);
|
|
75
|
-
this.debug(`[Player] Playing resource for track: ${track.title}`);
|
|
76
|
-
this.audioPlayer.play(this.currentResource);
|
|
77
|
-
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 5000);
|
|
78
|
-
return true;
|
|
79
294
|
}
|
|
80
295
|
catch (error) {
|
|
81
296
|
this.debug(`[Player] startTrack error:`, error);
|
|
@@ -90,10 +305,6 @@ class Player extends events_1.EventEmitter {
|
|
|
90
305
|
this.debug(`[Player] Cleared leave timeout`);
|
|
91
306
|
}
|
|
92
307
|
}
|
|
93
|
-
withTimeout(promise, message) {
|
|
94
|
-
const timeout = this.options.extractorTimeout ?? 15000;
|
|
95
|
-
return Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error(message)), timeout))]);
|
|
96
|
-
}
|
|
97
308
|
debug(message, ...optionalParams) {
|
|
98
309
|
if (this.listenerCount("debug") > 0) {
|
|
99
310
|
this.emit("debug", message, ...optionalParams);
|
|
@@ -109,6 +320,7 @@ class Player extends events_1.EventEmitter {
|
|
|
109
320
|
this.currentResource = null;
|
|
110
321
|
this.volumeInterval = null;
|
|
111
322
|
this.skipLoop = false;
|
|
323
|
+
this.extensions = [];
|
|
112
324
|
// TTS support
|
|
113
325
|
this.ttsPlayer = null;
|
|
114
326
|
this.ttsQueue = [];
|
|
@@ -145,6 +357,7 @@ class Player extends events_1.EventEmitter {
|
|
|
145
357
|
this.volume = this.options.volume || 100;
|
|
146
358
|
this.userdata = this.options.userdata;
|
|
147
359
|
this.setupEventListeners();
|
|
360
|
+
this.extensionContext = Object.freeze({ player: this, manager });
|
|
148
361
|
// Optionally pre-create the TTS AudioPlayer
|
|
149
362
|
if (this.options?.tts?.createPlayer) {
|
|
150
363
|
this.ensureTTSPlayer();
|
|
@@ -230,6 +443,14 @@ class Player extends events_1.EventEmitter {
|
|
|
230
443
|
this.debug(`[Player] Removing plugin: ${name}`);
|
|
231
444
|
return this.pluginManager.unregister(name);
|
|
232
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Connect to a voice channel
|
|
448
|
+
*
|
|
449
|
+
* @param {VoiceChannel} channel - Discord voice channel
|
|
450
|
+
* @returns {Promise<VoiceConnection>} The voice connection
|
|
451
|
+
* @example
|
|
452
|
+
* await player.connect(voiceChannel);
|
|
453
|
+
*/
|
|
233
454
|
async connect(channel) {
|
|
234
455
|
try {
|
|
235
456
|
this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
|
|
@@ -261,14 +482,29 @@ class Player extends events_1.EventEmitter {
|
|
|
261
482
|
throw error;
|
|
262
483
|
}
|
|
263
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Search for tracks using the player's extensions and plugins
|
|
487
|
+
*
|
|
488
|
+
* @param {string} query - The query to search for
|
|
489
|
+
* @param {string} requestedBy - The user ID who requested the search
|
|
490
|
+
* @returns {Promise<SearchResult>} The search result
|
|
491
|
+
* @example
|
|
492
|
+
* const result = await player.search("Never Gonna Give You Up", userId);
|
|
493
|
+
* console.log(`Search result: ${result.tracks.length} tracks`);
|
|
494
|
+
*/
|
|
264
495
|
async search(query, requestedBy) {
|
|
265
496
|
this.debug(`[Player] Search called with query: ${query}, requestedBy: ${requestedBy}`);
|
|
497
|
+
const extensionResult = await this.extensionsProvideSearch(query, requestedBy);
|
|
498
|
+
if (extensionResult && Array.isArray(extensionResult.tracks) && extensionResult.tracks.length > 0) {
|
|
499
|
+
this.debug(`[Player] Extension handled search for query: ${query}`);
|
|
500
|
+
return extensionResult;
|
|
501
|
+
}
|
|
266
502
|
const plugins = this.pluginManager.getAll();
|
|
267
503
|
let lastError = null;
|
|
268
504
|
for (const p of plugins) {
|
|
269
505
|
try {
|
|
270
506
|
this.debug(`[Player] Trying plugin for search: ${p.name}`);
|
|
271
|
-
const res = await
|
|
507
|
+
const res = await (0, timeout_1.withTimeout)(p.search(query, requestedBy), this.options.extractorTimeout ?? 15000, `Search operation timed out for ${p.name}`);
|
|
272
508
|
if (res && Array.isArray(res.tracks) && res.tracks.length > 0) {
|
|
273
509
|
this.debug(`[Player] Plugin '${p.name}' returned ${res.tracks.length} tracks`);
|
|
274
510
|
return res;
|
|
@@ -286,29 +522,66 @@ class Player extends events_1.EventEmitter {
|
|
|
286
522
|
this.emit("playerError", lastError);
|
|
287
523
|
throw new Error(`No plugin found to handle: ${query}`);
|
|
288
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* Play a track or search query
|
|
527
|
+
*
|
|
528
|
+
* @param {string | Track} query - Track URL, search query, or Track object
|
|
529
|
+
* @param {string} requestedBy - User ID who requested the track
|
|
530
|
+
* @returns {Promise<boolean>} True if playback started successfully
|
|
531
|
+
* @example
|
|
532
|
+
* await player.play("Never Gonna Give You Up", userId);
|
|
533
|
+
* await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId);
|
|
534
|
+
* await player.play("tts: Hello everyone!", userId);
|
|
535
|
+
*/
|
|
289
536
|
async play(query, requestedBy) {
|
|
537
|
+
this.debug(`[Player] Play called with query: ${typeof query === "string" ? query : query?.title}`);
|
|
538
|
+
this.clearLeaveTimeout();
|
|
539
|
+
let tracksToAdd = [];
|
|
540
|
+
let isPlaylist = false;
|
|
541
|
+
let effectiveRequest = { query, requestedBy };
|
|
542
|
+
let hookResponse = {};
|
|
290
543
|
try {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
544
|
+
const hookOutcome = await this.runBeforePlayHooks(effectiveRequest);
|
|
545
|
+
effectiveRequest = hookOutcome.request;
|
|
546
|
+
hookResponse = hookOutcome.response;
|
|
547
|
+
if (effectiveRequest.requestedBy === undefined) {
|
|
548
|
+
effectiveRequest.requestedBy = requestedBy;
|
|
549
|
+
}
|
|
550
|
+
const hookTracks = Array.isArray(hookResponse.tracks) ? hookResponse.tracks : undefined;
|
|
551
|
+
if (hookResponse.handled && (!hookTracks || hookTracks.length === 0)) {
|
|
552
|
+
const handledPayload = {
|
|
553
|
+
success: hookResponse.success ?? true,
|
|
554
|
+
query: effectiveRequest.query,
|
|
555
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
556
|
+
tracks: [],
|
|
557
|
+
isPlaylist: hookResponse.isPlaylist ?? false,
|
|
558
|
+
error: hookResponse.error,
|
|
559
|
+
};
|
|
560
|
+
await this.runAfterPlayHooks(handledPayload);
|
|
561
|
+
if (hookResponse.error) {
|
|
562
|
+
this.emit("playerError", hookResponse.error);
|
|
563
|
+
}
|
|
564
|
+
return hookResponse.success ?? true;
|
|
565
|
+
}
|
|
566
|
+
if (hookTracks && hookTracks.length > 0) {
|
|
567
|
+
tracksToAdd = hookTracks;
|
|
568
|
+
isPlaylist = hookResponse.isPlaylist ?? hookTracks.length > 1;
|
|
569
|
+
}
|
|
570
|
+
else if (typeof effectiveRequest.query === "string") {
|
|
571
|
+
const searchResult = await this.search(effectiveRequest.query, effectiveRequest.requestedBy || "Unknown");
|
|
298
572
|
tracksToAdd = searchResult.tracks;
|
|
299
573
|
if (searchResult.playlist) {
|
|
300
574
|
isPlaylist = true;
|
|
301
575
|
this.debug(`[Player] Added playlist: ${searchResult.playlist.name} (${tracksToAdd.length} tracks)`);
|
|
302
576
|
}
|
|
303
577
|
}
|
|
304
|
-
else {
|
|
305
|
-
tracksToAdd = [query];
|
|
578
|
+
else if (effectiveRequest.query) {
|
|
579
|
+
tracksToAdd = [effectiveRequest.query];
|
|
306
580
|
}
|
|
307
581
|
if (tracksToAdd.length === 0) {
|
|
308
582
|
this.debug(`[Player] No tracks found for play`);
|
|
309
583
|
throw new Error("No tracks found");
|
|
310
584
|
}
|
|
311
|
-
// If a TTS track is requested and interrupt mode is enabled, handle it separately
|
|
312
585
|
const isTTS = (t) => {
|
|
313
586
|
if (!t)
|
|
314
587
|
return false;
|
|
@@ -319,14 +592,20 @@ class Player extends events_1.EventEmitter {
|
|
|
319
592
|
return false;
|
|
320
593
|
}
|
|
321
594
|
};
|
|
322
|
-
const queryLooksTTS = typeof query === "string" && query.trim().toLowerCase().startsWith("tts");
|
|
595
|
+
const queryLooksTTS = typeof effectiveRequest.query === "string" && effectiveRequest.query.trim().toLowerCase().startsWith("tts");
|
|
323
596
|
if (!isPlaylist &&
|
|
324
597
|
tracksToAdd.length > 0 &&
|
|
325
598
|
this.options?.tts?.interrupt !== false &&
|
|
326
599
|
(isTTS(tracksToAdd[0]) || queryLooksTTS)) {
|
|
327
|
-
// Interrupt music playback with TTS (do not modify the music queue)
|
|
328
600
|
this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`);
|
|
329
601
|
await this.interruptWithTTSTrack(tracksToAdd[0]);
|
|
602
|
+
await this.runAfterPlayHooks({
|
|
603
|
+
success: true,
|
|
604
|
+
query: effectiveRequest.query,
|
|
605
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
606
|
+
tracks: tracksToAdd,
|
|
607
|
+
isPlaylist,
|
|
608
|
+
});
|
|
330
609
|
return true;
|
|
331
610
|
}
|
|
332
611
|
if (isPlaylist) {
|
|
@@ -334,16 +613,28 @@ class Player extends events_1.EventEmitter {
|
|
|
334
613
|
this.emit("queueAddList", tracksToAdd);
|
|
335
614
|
}
|
|
336
615
|
else {
|
|
337
|
-
this.queue.add(tracksToAdd
|
|
338
|
-
this.emit("queueAdd", tracksToAdd
|
|
339
|
-
}
|
|
340
|
-
// Start playing if not already playing
|
|
341
|
-
if (!this.isPlaying) {
|
|
342
|
-
return this.playNext();
|
|
616
|
+
this.queue.add(tracksToAdd[0]);
|
|
617
|
+
this.emit("queueAdd", tracksToAdd[0]);
|
|
343
618
|
}
|
|
344
|
-
|
|
619
|
+
const started = !this.isPlaying ? await this.playNext() : true;
|
|
620
|
+
await this.runAfterPlayHooks({
|
|
621
|
+
success: started,
|
|
622
|
+
query: effectiveRequest.query,
|
|
623
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
624
|
+
tracks: tracksToAdd,
|
|
625
|
+
isPlaylist,
|
|
626
|
+
});
|
|
627
|
+
return started;
|
|
345
628
|
}
|
|
346
629
|
catch (error) {
|
|
630
|
+
await this.runAfterPlayHooks({
|
|
631
|
+
success: false,
|
|
632
|
+
query: effectiveRequest.query,
|
|
633
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
634
|
+
tracks: tracksToAdd,
|
|
635
|
+
isPlaylist,
|
|
636
|
+
error: error,
|
|
637
|
+
});
|
|
347
638
|
this.debug(`[Player] Play error:`, error);
|
|
348
639
|
this.emit("playerError", error);
|
|
349
640
|
return false;
|
|
@@ -352,6 +643,11 @@ class Player extends events_1.EventEmitter {
|
|
|
352
643
|
/**
|
|
353
644
|
* Interrupt current music with a TTS track. Pauses music, swaps the
|
|
354
645
|
* subscription to a dedicated TTS player, plays TTS, then resumes.
|
|
646
|
+
*
|
|
647
|
+
* @param {Track} track - The track to interrupt with
|
|
648
|
+
* @returns {Promise<void>}
|
|
649
|
+
* @example
|
|
650
|
+
* await player.interruptWithTTSTrack(track);
|
|
355
651
|
*/
|
|
356
652
|
async interruptWithTTSTrack(track) {
|
|
357
653
|
this.ttsQueue.push(track);
|
|
@@ -359,7 +655,13 @@ class Player extends events_1.EventEmitter {
|
|
|
359
655
|
void this.playNextTTS();
|
|
360
656
|
}
|
|
361
657
|
}
|
|
362
|
-
/**
|
|
658
|
+
/**
|
|
659
|
+
* Play queued TTS items sequentially
|
|
660
|
+
*
|
|
661
|
+
* @returns {Promise<void>}
|
|
662
|
+
* @example
|
|
663
|
+
* await player.playNextTTS();
|
|
664
|
+
*/
|
|
363
665
|
async playNextTTS() {
|
|
364
666
|
const next = this.ttsQueue.shift();
|
|
365
667
|
if (!next)
|
|
@@ -389,8 +691,14 @@ class Player extends events_1.EventEmitter {
|
|
|
389
691
|
await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Playing, 5000).catch(() => null);
|
|
390
692
|
// Derive timeout from resource/track duration when available, with a sensible cap
|
|
391
693
|
const md = resource?.metadata ?? {};
|
|
392
|
-
const declared = typeof md.duration === "number" ? md.duration
|
|
393
|
-
|
|
694
|
+
const declared = typeof md.duration === "number" ? md.duration
|
|
695
|
+
: typeof next?.duration === "number" ? next.duration
|
|
696
|
+
: undefined;
|
|
697
|
+
const declaredMs = declared ?
|
|
698
|
+
declared > 1000 ?
|
|
699
|
+
declared
|
|
700
|
+
: declared * 1000
|
|
701
|
+
: undefined;
|
|
394
702
|
const cap = this.options?.tts?.Max_Time_TTS ?? 60000;
|
|
395
703
|
const idleTimeout = declaredMs ? Math.min(cap, Math.max(1000, declaredMs + 1500)) : cap;
|
|
396
704
|
await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
|
|
@@ -423,7 +731,7 @@ class Player extends events_1.EventEmitter {
|
|
|
423
731
|
throw new Error(`No plugin found for track: ${track.title}`);
|
|
424
732
|
let streamInfo;
|
|
425
733
|
try {
|
|
426
|
-
streamInfo = await
|
|
734
|
+
streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), this.options.extractorTimeout ?? 15000, "getStream timed out");
|
|
427
735
|
}
|
|
428
736
|
catch (streamError) {
|
|
429
737
|
// try fallbacks
|
|
@@ -432,7 +740,7 @@ class Player extends events_1.EventEmitter {
|
|
|
432
740
|
if (typeof p.getFallback !== "function")
|
|
433
741
|
continue;
|
|
434
742
|
try {
|
|
435
|
-
streamInfo = await
|
|
743
|
+
streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), this.options.extractorTimeout ?? 15000, `getFallback timed out for plugin ${p.name}`);
|
|
436
744
|
if (!streamInfo?.stream)
|
|
437
745
|
continue;
|
|
438
746
|
break;
|
|
@@ -475,10 +783,10 @@ class Player extends events_1.EventEmitter {
|
|
|
475
783
|
for (const p of candidates) {
|
|
476
784
|
try {
|
|
477
785
|
this.debug(`[Player] Trying related from plugin: ${p.name}`);
|
|
478
|
-
const related = await
|
|
786
|
+
const related = await (0, timeout_1.withTimeout)(p.getRelatedTracks(lastTrack.url, {
|
|
479
787
|
limit: 10,
|
|
480
788
|
history: this.queue.previousTracks,
|
|
481
|
-
}), `getRelatedTracks timed out for ${p.name}`);
|
|
789
|
+
}), this.options.extractorTimeout ?? 15000, `getRelatedTracks timed out for ${p.name}`);
|
|
482
790
|
if (Array.isArray(related) && related.length > 0) {
|
|
483
791
|
const randomchoice = Math.floor(Math.random() * related.length);
|
|
484
792
|
const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
|
|
@@ -529,6 +837,14 @@ class Player extends events_1.EventEmitter {
|
|
|
529
837
|
return this.playNext();
|
|
530
838
|
}
|
|
531
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* Pause the current track
|
|
842
|
+
*
|
|
843
|
+
* @returns {boolean} True if paused successfully
|
|
844
|
+
* @example
|
|
845
|
+
* const paused = player.pause();
|
|
846
|
+
* console.log(`Paused: ${paused}`);
|
|
847
|
+
*/
|
|
532
848
|
pause() {
|
|
533
849
|
this.debug(`[Player] pause called`);
|
|
534
850
|
if (this.isPlaying && !this.isPaused) {
|
|
@@ -536,6 +852,14 @@ class Player extends events_1.EventEmitter {
|
|
|
536
852
|
}
|
|
537
853
|
return false;
|
|
538
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* Resume the current track
|
|
857
|
+
*
|
|
858
|
+
* @returns {boolean} True if resumed successfully
|
|
859
|
+
* @example
|
|
860
|
+
* const resumed = player.resume();
|
|
861
|
+
* console.log(`Resumed: ${resumed}`);
|
|
862
|
+
*/
|
|
539
863
|
resume() {
|
|
540
864
|
this.debug(`[Player] resume called`);
|
|
541
865
|
if (this.isPaused) {
|
|
@@ -551,6 +875,14 @@ class Player extends events_1.EventEmitter {
|
|
|
551
875
|
}
|
|
552
876
|
return false;
|
|
553
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Stop the current track
|
|
880
|
+
*
|
|
881
|
+
* @returns {boolean} True if stopped successfully
|
|
882
|
+
* @example
|
|
883
|
+
* const stopped = player.stop();
|
|
884
|
+
* console.log(`Stopped: ${stopped}`);
|
|
885
|
+
*/
|
|
554
886
|
stop() {
|
|
555
887
|
this.debug(`[Player] stop called`);
|
|
556
888
|
this.queue.clear();
|
|
@@ -560,6 +892,14 @@ class Player extends events_1.EventEmitter {
|
|
|
560
892
|
this.emit("playerStop");
|
|
561
893
|
return result;
|
|
562
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* Skip to the next track
|
|
897
|
+
*
|
|
898
|
+
* @returns {boolean} True if skipped successfully
|
|
899
|
+
* @example
|
|
900
|
+
* const skipped = player.skip();
|
|
901
|
+
* console.log(`Skipped: ${skipped}`);
|
|
902
|
+
*/
|
|
563
903
|
skip() {
|
|
564
904
|
this.debug(`[Player] skip called`);
|
|
565
905
|
if (this.isPlaying || this.isPaused) {
|
|
@@ -570,6 +910,11 @@ class Player extends events_1.EventEmitter {
|
|
|
570
910
|
}
|
|
571
911
|
/**
|
|
572
912
|
* Go back to the previous track in history and play it.
|
|
913
|
+
*
|
|
914
|
+
* @returns {Promise<boolean>} True if previous track was played successfully
|
|
915
|
+
* @example
|
|
916
|
+
* const previous = await player.previous();
|
|
917
|
+
* console.log(`Previous: ${previous}`);
|
|
573
918
|
*/
|
|
574
919
|
async previous() {
|
|
575
920
|
this.debug(`[Player] previous called`);
|
|
@@ -581,12 +926,39 @@ class Player extends events_1.EventEmitter {
|
|
|
581
926
|
this.clearLeaveTimeout();
|
|
582
927
|
return this.startTrack(track);
|
|
583
928
|
}
|
|
929
|
+
/**
|
|
930
|
+
* Loop the current track
|
|
931
|
+
*
|
|
932
|
+
* @param {LoopMode} mode - The loop mode to set
|
|
933
|
+
* @returns {LoopMode} The loop mode
|
|
934
|
+
* @example
|
|
935
|
+
* const loopMode = player.loop("track");
|
|
936
|
+
* console.log(`Loop mode: ${loopMode}`);
|
|
937
|
+
*/
|
|
584
938
|
loop(mode) {
|
|
585
939
|
return this.queue.loop(mode);
|
|
586
940
|
}
|
|
941
|
+
/**
|
|
942
|
+
* Set the auto-play mode
|
|
943
|
+
*
|
|
944
|
+
* @param {boolean} mode - The auto-play mode to set
|
|
945
|
+
* @returns {boolean} The auto-play mode
|
|
946
|
+
* @example
|
|
947
|
+
* const autoPlayMode = player.autoPlay(true);
|
|
948
|
+
* console.log(`Auto-play mode: ${autoPlayMode}`);
|
|
949
|
+
*/
|
|
587
950
|
autoPlay(mode) {
|
|
588
951
|
return this.queue.autoPlay(mode);
|
|
589
952
|
}
|
|
953
|
+
/**
|
|
954
|
+
* Set the volume of the current track
|
|
955
|
+
*
|
|
956
|
+
* @param {number} volume - The volume to set
|
|
957
|
+
* @returns {boolean} True if volume was set successfully
|
|
958
|
+
* @example
|
|
959
|
+
* const volumeSet = player.setVolume(50);
|
|
960
|
+
* console.log(`Volume set: ${volumeSet}`);
|
|
961
|
+
*/
|
|
590
962
|
setVolume(volume) {
|
|
591
963
|
this.debug(`[Player] setVolume called: ${volume}`);
|
|
592
964
|
if (volume < 0 || volume > 200)
|
|
@@ -614,10 +986,24 @@ class Player extends events_1.EventEmitter {
|
|
|
614
986
|
this.emit("volumeChange", oldVolume, volume);
|
|
615
987
|
return true;
|
|
616
988
|
}
|
|
989
|
+
/**
|
|
990
|
+
* Shuffle the queue
|
|
991
|
+
*
|
|
992
|
+
* @returns {void}
|
|
993
|
+
* @example
|
|
994
|
+
* player.shuffle();
|
|
995
|
+
*/
|
|
617
996
|
shuffle() {
|
|
618
997
|
this.debug(`[Player] shuffle called`);
|
|
619
998
|
this.queue.shuffle();
|
|
620
999
|
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Clear the queue
|
|
1002
|
+
*
|
|
1003
|
+
* @returns {void}
|
|
1004
|
+
* @example
|
|
1005
|
+
* player.clearQueue();
|
|
1006
|
+
*/
|
|
621
1007
|
clearQueue() {
|
|
622
1008
|
this.debug(`[Player] clearQueue called`);
|
|
623
1009
|
this.queue.clear();
|
|
@@ -627,6 +1013,14 @@ class Player extends events_1.EventEmitter {
|
|
|
627
1013
|
* - If `query` is a string, performs a search and inserts resulting tracks (playlist supported).
|
|
628
1014
|
* - If a Track or Track[] is provided, inserts directly.
|
|
629
1015
|
* Does not auto-start playback; it only modifies the queue.
|
|
1016
|
+
*
|
|
1017
|
+
* @param {string | Track | Track[]} query - The track or tracks to insert
|
|
1018
|
+
* @param {number} index - The index to insert the tracks at
|
|
1019
|
+
* @param {string} requestedBy - The user ID who requested the insert
|
|
1020
|
+
* @returns {Promise<boolean>} True if the tracks were inserted successfully
|
|
1021
|
+
* @example
|
|
1022
|
+
* const inserted = await player.insert("Song Name", 0, userId);
|
|
1023
|
+
* console.log(`Inserted: ${inserted}`);
|
|
630
1024
|
*/
|
|
631
1025
|
async insert(query, index, requestedBy) {
|
|
632
1026
|
try {
|
|
@@ -667,6 +1061,15 @@ class Player extends events_1.EventEmitter {
|
|
|
667
1061
|
return false;
|
|
668
1062
|
}
|
|
669
1063
|
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Remove a track from the queue
|
|
1066
|
+
*
|
|
1067
|
+
* @param {number} index - The index of the track to remove
|
|
1068
|
+
* @returns {Track | null} The removed track or null
|
|
1069
|
+
* @example
|
|
1070
|
+
* const removed = player.remove(0);
|
|
1071
|
+
* console.log(`Removed: ${removed?.title}`);
|
|
1072
|
+
*/
|
|
670
1073
|
remove(index) {
|
|
671
1074
|
this.debug(`[Player] remove called for index: ${index}`);
|
|
672
1075
|
const track = this.queue.remove(index);
|
|
@@ -675,6 +1078,15 @@ class Player extends events_1.EventEmitter {
|
|
|
675
1078
|
}
|
|
676
1079
|
return track;
|
|
677
1080
|
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Get the progress bar of the current track
|
|
1083
|
+
*
|
|
1084
|
+
* @param {ProgressBarOptions} options - The options for the progress bar
|
|
1085
|
+
* @returns {string} The progress bar
|
|
1086
|
+
* @example
|
|
1087
|
+
* const progressBar = player.getProgressBar();
|
|
1088
|
+
* console.log(`Progress bar: ${progressBar}`);
|
|
1089
|
+
*/
|
|
678
1090
|
getProgressBar(options = {}) {
|
|
679
1091
|
const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
|
|
680
1092
|
const track = this.queue.currentTrack;
|
|
@@ -690,6 +1102,14 @@ class Player extends events_1.EventEmitter {
|
|
|
690
1102
|
const bar = barChar.repeat(progress) + progressChar + barChar.repeat(size - progress);
|
|
691
1103
|
return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`;
|
|
692
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Get the time of the current track
|
|
1107
|
+
*
|
|
1108
|
+
* @returns {Object} The time of the current track
|
|
1109
|
+
* @example
|
|
1110
|
+
* const time = player.getTime();
|
|
1111
|
+
* console.log(`Time: ${time.current}`);
|
|
1112
|
+
*/
|
|
693
1113
|
getTime() {
|
|
694
1114
|
const resource = this.currentResource;
|
|
695
1115
|
const track = this.queue.currentTrack;
|
|
@@ -706,6 +1126,15 @@ class Player extends events_1.EventEmitter {
|
|
|
706
1126
|
format: this.formatTime(resource.playbackDuration),
|
|
707
1127
|
};
|
|
708
1128
|
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Format the time in the format of HH:MM:SS
|
|
1131
|
+
*
|
|
1132
|
+
* @param {number} ms - The time in milliseconds
|
|
1133
|
+
* @returns {string} The formatted time
|
|
1134
|
+
* @example
|
|
1135
|
+
* const formattedTime = player.formatTime(1000);
|
|
1136
|
+
* console.log(`Formatted time: ${formattedTime}`);
|
|
1137
|
+
*/
|
|
709
1138
|
formatTime(ms) {
|
|
710
1139
|
const totalSeconds = Math.floor(ms / 1000);
|
|
711
1140
|
const hours = Math.floor(totalSeconds / 3600);
|
|
@@ -730,6 +1159,13 @@ class Player extends events_1.EventEmitter {
|
|
|
730
1159
|
}, this.options.leaveTimeout);
|
|
731
1160
|
}
|
|
732
1161
|
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Destroy the player
|
|
1164
|
+
*
|
|
1165
|
+
* @returns {void}
|
|
1166
|
+
* @example
|
|
1167
|
+
* player.destroy();
|
|
1168
|
+
*/
|
|
733
1169
|
destroy() {
|
|
734
1170
|
this.debug(`[Player] destroy called`);
|
|
735
1171
|
if (this.leaveTimeout) {
|
|
@@ -750,30 +1186,92 @@ class Player extends events_1.EventEmitter {
|
|
|
750
1186
|
}
|
|
751
1187
|
this.queue.clear();
|
|
752
1188
|
this.pluginManager.clear();
|
|
1189
|
+
for (const extension of [...this.extensions]) {
|
|
1190
|
+
this.invokeExtensionLifecycle(extension, "onDestroy");
|
|
1191
|
+
if (extension.player === this) {
|
|
1192
|
+
extension.player = null;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
this.extensions = [];
|
|
753
1196
|
this.isPlaying = false;
|
|
754
1197
|
this.isPaused = false;
|
|
755
1198
|
this.emit("playerDestroy");
|
|
756
1199
|
this.removeAllListeners();
|
|
757
1200
|
}
|
|
758
|
-
|
|
1201
|
+
/**
|
|
1202
|
+
* Get the size of the queue
|
|
1203
|
+
*
|
|
1204
|
+
* @returns {number} The size of the queue
|
|
1205
|
+
* @example
|
|
1206
|
+
* const queueSize = player.queueSize;
|
|
1207
|
+
* console.log(`Queue size: ${queueSize}`);
|
|
1208
|
+
*/
|
|
759
1209
|
get queueSize() {
|
|
760
1210
|
return this.queue.size;
|
|
761
1211
|
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Get the current track
|
|
1214
|
+
*
|
|
1215
|
+
* @returns {Track | null} The current track or null
|
|
1216
|
+
* @example
|
|
1217
|
+
* const currentTrack = player.currentTrack;
|
|
1218
|
+
* console.log(`Current track: ${currentTrack?.title}`);
|
|
1219
|
+
*/
|
|
762
1220
|
get currentTrack() {
|
|
763
1221
|
return this.queue.currentTrack;
|
|
764
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Get the previous track
|
|
1225
|
+
*
|
|
1226
|
+
* @returns {Track | null} The previous track or null
|
|
1227
|
+
* @example
|
|
1228
|
+
* const previousTrack = player.previousTrack;
|
|
1229
|
+
* console.log(`Previous track: ${previousTrack?.title}`);
|
|
1230
|
+
*/
|
|
765
1231
|
get previousTrack() {
|
|
766
1232
|
return this.queue.previousTracks?.at(-1) ?? null;
|
|
767
1233
|
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Get the upcoming tracks
|
|
1236
|
+
*
|
|
1237
|
+
* @returns {Track[]} The upcoming tracks
|
|
1238
|
+
* @example
|
|
1239
|
+
* const upcomingTracks = player.upcomingTracks;
|
|
1240
|
+
* console.log(`Upcoming tracks: ${upcomingTracks.length}`);
|
|
1241
|
+
*/
|
|
768
1242
|
get upcomingTracks() {
|
|
769
1243
|
return this.queue.getTracks();
|
|
770
1244
|
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Get the previous tracks
|
|
1247
|
+
*
|
|
1248
|
+
* @returns {Track[]} The previous tracks
|
|
1249
|
+
* @example
|
|
1250
|
+
* const previousTracks = player.previousTracks;
|
|
1251
|
+
* console.log(`Previous tracks: ${previousTracks.length}`);
|
|
1252
|
+
*/
|
|
771
1253
|
get previousTracks() {
|
|
772
1254
|
return this.queue.previousTracks;
|
|
773
1255
|
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Get the available plugins
|
|
1258
|
+
*
|
|
1259
|
+
* @returns {string[]} The available plugins
|
|
1260
|
+
* @example
|
|
1261
|
+
* const availablePlugins = player.availablePlugins;
|
|
1262
|
+
* console.log(`Available plugins: ${availablePlugins.length}`);
|
|
1263
|
+
*/
|
|
774
1264
|
get availablePlugins() {
|
|
775
1265
|
return this.pluginManager.getAll().map((p) => p.name);
|
|
776
1266
|
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Get the related tracks
|
|
1269
|
+
*
|
|
1270
|
+
* @returns {Track[] | null} The related tracks or null
|
|
1271
|
+
* @example
|
|
1272
|
+
* const relatedTracks = player.relatedTracks;
|
|
1273
|
+
* console.log(`Related tracks: ${relatedTracks?.length}`);
|
|
1274
|
+
*/
|
|
777
1275
|
get relatedTracks() {
|
|
778
1276
|
return this.queue.relatedTracks();
|
|
779
1277
|
}
|