ziplayer 0.0.8 → 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 +299 -2
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +594 -86
- 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 +625 -88
- 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)
|
|
@@ -429,7 +731,7 @@ class Player extends events_1.EventEmitter {
|
|
|
429
731
|
throw new Error(`No plugin found for track: ${track.title}`);
|
|
430
732
|
let streamInfo;
|
|
431
733
|
try {
|
|
432
|
-
streamInfo = await
|
|
734
|
+
streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), this.options.extractorTimeout ?? 15000, "getStream timed out");
|
|
433
735
|
}
|
|
434
736
|
catch (streamError) {
|
|
435
737
|
// try fallbacks
|
|
@@ -438,7 +740,7 @@ class Player extends events_1.EventEmitter {
|
|
|
438
740
|
if (typeof p.getFallback !== "function")
|
|
439
741
|
continue;
|
|
440
742
|
try {
|
|
441
|
-
streamInfo = await
|
|
743
|
+
streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), this.options.extractorTimeout ?? 15000, `getFallback timed out for plugin ${p.name}`);
|
|
442
744
|
if (!streamInfo?.stream)
|
|
443
745
|
continue;
|
|
444
746
|
break;
|
|
@@ -481,10 +783,10 @@ class Player extends events_1.EventEmitter {
|
|
|
481
783
|
for (const p of candidates) {
|
|
482
784
|
try {
|
|
483
785
|
this.debug(`[Player] Trying related from plugin: ${p.name}`);
|
|
484
|
-
const related = await
|
|
786
|
+
const related = await (0, timeout_1.withTimeout)(p.getRelatedTracks(lastTrack.url, {
|
|
485
787
|
limit: 10,
|
|
486
788
|
history: this.queue.previousTracks,
|
|
487
|
-
}), `getRelatedTracks timed out for ${p.name}`);
|
|
789
|
+
}), this.options.extractorTimeout ?? 15000, `getRelatedTracks timed out for ${p.name}`);
|
|
488
790
|
if (Array.isArray(related) && related.length > 0) {
|
|
489
791
|
const randomchoice = Math.floor(Math.random() * related.length);
|
|
490
792
|
const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
|
|
@@ -535,6 +837,14 @@ class Player extends events_1.EventEmitter {
|
|
|
535
837
|
return this.playNext();
|
|
536
838
|
}
|
|
537
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
|
+
*/
|
|
538
848
|
pause() {
|
|
539
849
|
this.debug(`[Player] pause called`);
|
|
540
850
|
if (this.isPlaying && !this.isPaused) {
|
|
@@ -542,6 +852,14 @@ class Player extends events_1.EventEmitter {
|
|
|
542
852
|
}
|
|
543
853
|
return false;
|
|
544
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
|
+
*/
|
|
545
863
|
resume() {
|
|
546
864
|
this.debug(`[Player] resume called`);
|
|
547
865
|
if (this.isPaused) {
|
|
@@ -557,6 +875,14 @@ class Player extends events_1.EventEmitter {
|
|
|
557
875
|
}
|
|
558
876
|
return false;
|
|
559
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
|
+
*/
|
|
560
886
|
stop() {
|
|
561
887
|
this.debug(`[Player] stop called`);
|
|
562
888
|
this.queue.clear();
|
|
@@ -566,6 +892,14 @@ class Player extends events_1.EventEmitter {
|
|
|
566
892
|
this.emit("playerStop");
|
|
567
893
|
return result;
|
|
568
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
|
+
*/
|
|
569
903
|
skip() {
|
|
570
904
|
this.debug(`[Player] skip called`);
|
|
571
905
|
if (this.isPlaying || this.isPaused) {
|
|
@@ -576,6 +910,11 @@ class Player extends events_1.EventEmitter {
|
|
|
576
910
|
}
|
|
577
911
|
/**
|
|
578
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}`);
|
|
579
918
|
*/
|
|
580
919
|
async previous() {
|
|
581
920
|
this.debug(`[Player] previous called`);
|
|
@@ -587,12 +926,39 @@ class Player extends events_1.EventEmitter {
|
|
|
587
926
|
this.clearLeaveTimeout();
|
|
588
927
|
return this.startTrack(track);
|
|
589
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
|
+
*/
|
|
590
938
|
loop(mode) {
|
|
591
939
|
return this.queue.loop(mode);
|
|
592
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
|
+
*/
|
|
593
950
|
autoPlay(mode) {
|
|
594
951
|
return this.queue.autoPlay(mode);
|
|
595
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
|
+
*/
|
|
596
962
|
setVolume(volume) {
|
|
597
963
|
this.debug(`[Player] setVolume called: ${volume}`);
|
|
598
964
|
if (volume < 0 || volume > 200)
|
|
@@ -620,10 +986,24 @@ class Player extends events_1.EventEmitter {
|
|
|
620
986
|
this.emit("volumeChange", oldVolume, volume);
|
|
621
987
|
return true;
|
|
622
988
|
}
|
|
989
|
+
/**
|
|
990
|
+
* Shuffle the queue
|
|
991
|
+
*
|
|
992
|
+
* @returns {void}
|
|
993
|
+
* @example
|
|
994
|
+
* player.shuffle();
|
|
995
|
+
*/
|
|
623
996
|
shuffle() {
|
|
624
997
|
this.debug(`[Player] shuffle called`);
|
|
625
998
|
this.queue.shuffle();
|
|
626
999
|
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Clear the queue
|
|
1002
|
+
*
|
|
1003
|
+
* @returns {void}
|
|
1004
|
+
* @example
|
|
1005
|
+
* player.clearQueue();
|
|
1006
|
+
*/
|
|
627
1007
|
clearQueue() {
|
|
628
1008
|
this.debug(`[Player] clearQueue called`);
|
|
629
1009
|
this.queue.clear();
|
|
@@ -633,6 +1013,14 @@ class Player extends events_1.EventEmitter {
|
|
|
633
1013
|
* - If `query` is a string, performs a search and inserts resulting tracks (playlist supported).
|
|
634
1014
|
* - If a Track or Track[] is provided, inserts directly.
|
|
635
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}`);
|
|
636
1024
|
*/
|
|
637
1025
|
async insert(query, index, requestedBy) {
|
|
638
1026
|
try {
|
|
@@ -673,6 +1061,15 @@ class Player extends events_1.EventEmitter {
|
|
|
673
1061
|
return false;
|
|
674
1062
|
}
|
|
675
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
|
+
*/
|
|
676
1073
|
remove(index) {
|
|
677
1074
|
this.debug(`[Player] remove called for index: ${index}`);
|
|
678
1075
|
const track = this.queue.remove(index);
|
|
@@ -681,6 +1078,15 @@ class Player extends events_1.EventEmitter {
|
|
|
681
1078
|
}
|
|
682
1079
|
return track;
|
|
683
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
|
+
*/
|
|
684
1090
|
getProgressBar(options = {}) {
|
|
685
1091
|
const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
|
|
686
1092
|
const track = this.queue.currentTrack;
|
|
@@ -696,6 +1102,39 @@ class Player extends events_1.EventEmitter {
|
|
|
696
1102
|
const bar = barChar.repeat(progress) + progressChar + barChar.repeat(size - progress);
|
|
697
1103
|
return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`;
|
|
698
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
|
+
*/
|
|
1113
|
+
getTime() {
|
|
1114
|
+
const resource = this.currentResource;
|
|
1115
|
+
const track = this.queue.currentTrack;
|
|
1116
|
+
if (!track || !resource)
|
|
1117
|
+
return {
|
|
1118
|
+
current: 0,
|
|
1119
|
+
total: 0,
|
|
1120
|
+
format: "00:00",
|
|
1121
|
+
};
|
|
1122
|
+
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
1123
|
+
return {
|
|
1124
|
+
current: resource?.playbackDuration,
|
|
1125
|
+
total: total,
|
|
1126
|
+
format: this.formatTime(resource.playbackDuration),
|
|
1127
|
+
};
|
|
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
|
+
*/
|
|
699
1138
|
formatTime(ms) {
|
|
700
1139
|
const totalSeconds = Math.floor(ms / 1000);
|
|
701
1140
|
const hours = Math.floor(totalSeconds / 3600);
|
|
@@ -720,6 +1159,13 @@ class Player extends events_1.EventEmitter {
|
|
|
720
1159
|
}, this.options.leaveTimeout);
|
|
721
1160
|
}
|
|
722
1161
|
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Destroy the player
|
|
1164
|
+
*
|
|
1165
|
+
* @returns {void}
|
|
1166
|
+
* @example
|
|
1167
|
+
* player.destroy();
|
|
1168
|
+
*/
|
|
723
1169
|
destroy() {
|
|
724
1170
|
this.debug(`[Player] destroy called`);
|
|
725
1171
|
if (this.leaveTimeout) {
|
|
@@ -740,30 +1186,92 @@ class Player extends events_1.EventEmitter {
|
|
|
740
1186
|
}
|
|
741
1187
|
this.queue.clear();
|
|
742
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 = [];
|
|
743
1196
|
this.isPlaying = false;
|
|
744
1197
|
this.isPaused = false;
|
|
745
1198
|
this.emit("playerDestroy");
|
|
746
1199
|
this.removeAllListeners();
|
|
747
1200
|
}
|
|
748
|
-
|
|
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
|
+
*/
|
|
749
1209
|
get queueSize() {
|
|
750
1210
|
return this.queue.size;
|
|
751
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
|
+
*/
|
|
752
1220
|
get currentTrack() {
|
|
753
1221
|
return this.queue.currentTrack;
|
|
754
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
|
+
*/
|
|
755
1231
|
get previousTrack() {
|
|
756
1232
|
return this.queue.previousTracks?.at(-1) ?? null;
|
|
757
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
|
+
*/
|
|
758
1242
|
get upcomingTracks() {
|
|
759
1243
|
return this.queue.getTracks();
|
|
760
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
|
+
*/
|
|
761
1253
|
get previousTracks() {
|
|
762
1254
|
return this.queue.previousTracks;
|
|
763
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
|
+
*/
|
|
764
1264
|
get availablePlugins() {
|
|
765
1265
|
return this.pluginManager.getAll().map((p) => p.name);
|
|
766
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
|
+
*/
|
|
767
1275
|
get relatedTracks() {
|
|
768
1276
|
return this.queue.relatedTracks();
|
|
769
1277
|
}
|