ziplayer 0.0.9 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +325 -2
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +729 -101
- 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 +795 -102
- 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 {
|
|
46
|
+
/**
|
|
47
|
+
* Attach an extension to the player
|
|
48
|
+
*
|
|
49
|
+
* @param {BaseExtension} extension - The extension to attach
|
|
50
|
+
* @example
|
|
51
|
+
* player.attachExtension(new MyExtension());
|
|
52
|
+
*/
|
|
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;
|
|
93
|
+
try {
|
|
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;
|
|
158
|
+
try {
|
|
159
|
+
await Promise.resolve(hook.call(extension, this.extensionContext, immutablePayload));
|
|
160
|
+
}
|
|
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
|
+
}
|
|
9
204
|
/**
|
|
10
205
|
* Start playing a specific track immediately, replacing the current resource.
|
|
11
206
|
*/
|
|
12
207
|
async startTrack(track) {
|
|
13
208
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
streamInfo = await this.withTimeout(p.getFallback(track), `getFallback timed out for plugin ${p.name}`);
|
|
36
|
-
if (!streamInfo.stream)
|
|
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,11 @@ class Player extends events_1.EventEmitter {
|
|
|
109
320
|
this.currentResource = null;
|
|
110
321
|
this.volumeInterval = null;
|
|
111
322
|
this.skipLoop = false;
|
|
323
|
+
this.extensions = [];
|
|
324
|
+
// Cache for plugin matching to improve performance
|
|
325
|
+
this.pluginCache = new Map();
|
|
326
|
+
this.PLUGIN_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
327
|
+
this.pluginCacheTimestamps = new Map();
|
|
112
328
|
// TTS support
|
|
113
329
|
this.ttsPlayer = null;
|
|
114
330
|
this.ttsQueue = [];
|
|
@@ -145,6 +361,7 @@ class Player extends events_1.EventEmitter {
|
|
|
145
361
|
this.volume = this.options.volume || 100;
|
|
146
362
|
this.userdata = this.options.userdata;
|
|
147
363
|
this.setupEventListeners();
|
|
364
|
+
this.extensionContext = Object.freeze({ player: this, manager });
|
|
148
365
|
// Optionally pre-create the TTS AudioPlayer
|
|
149
366
|
if (this.options?.tts?.createPlayer) {
|
|
150
367
|
this.ensureTTSPlayer();
|
|
@@ -230,6 +447,14 @@ class Player extends events_1.EventEmitter {
|
|
|
230
447
|
this.debug(`[Player] Removing plugin: ${name}`);
|
|
231
448
|
return this.pluginManager.unregister(name);
|
|
232
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Connect to a voice channel
|
|
452
|
+
*
|
|
453
|
+
* @param {VoiceChannel} channel - Discord voice channel
|
|
454
|
+
* @returns {Promise<VoiceConnection>} The voice connection
|
|
455
|
+
* @example
|
|
456
|
+
* await player.connect(voiceChannel);
|
|
457
|
+
*/
|
|
233
458
|
async connect(channel) {
|
|
234
459
|
try {
|
|
235
460
|
this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
|
|
@@ -261,14 +486,29 @@ class Player extends events_1.EventEmitter {
|
|
|
261
486
|
throw error;
|
|
262
487
|
}
|
|
263
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Search for tracks using the player's extensions and plugins
|
|
491
|
+
*
|
|
492
|
+
* @param {string} query - The query to search for
|
|
493
|
+
* @param {string} requestedBy - The user ID who requested the search
|
|
494
|
+
* @returns {Promise<SearchResult>} The search result
|
|
495
|
+
* @example
|
|
496
|
+
* const result = await player.search("Never Gonna Give You Up", userId);
|
|
497
|
+
* console.log(`Search result: ${result.tracks.length} tracks`);
|
|
498
|
+
*/
|
|
264
499
|
async search(query, requestedBy) {
|
|
265
500
|
this.debug(`[Player] Search called with query: ${query}, requestedBy: ${requestedBy}`);
|
|
501
|
+
const extensionResult = await this.extensionsProvideSearch(query, requestedBy);
|
|
502
|
+
if (extensionResult && Array.isArray(extensionResult.tracks) && extensionResult.tracks.length > 0) {
|
|
503
|
+
this.debug(`[Player] Extension handled search for query: ${query}`);
|
|
504
|
+
return extensionResult;
|
|
505
|
+
}
|
|
266
506
|
const plugins = this.pluginManager.getAll();
|
|
267
507
|
let lastError = null;
|
|
268
508
|
for (const p of plugins) {
|
|
269
509
|
try {
|
|
270
510
|
this.debug(`[Player] Trying plugin for search: ${p.name}`);
|
|
271
|
-
const res = await
|
|
511
|
+
const res = await (0, timeout_1.withTimeout)(p.search(query, requestedBy), this.options.extractorTimeout ?? 15000, `Search operation timed out for ${p.name}`);
|
|
272
512
|
if (res && Array.isArray(res.tracks) && res.tracks.length > 0) {
|
|
273
513
|
this.debug(`[Player] Plugin '${p.name}' returned ${res.tracks.length} tracks`);
|
|
274
514
|
return res;
|
|
@@ -286,29 +526,66 @@ class Player extends events_1.EventEmitter {
|
|
|
286
526
|
this.emit("playerError", lastError);
|
|
287
527
|
throw new Error(`No plugin found to handle: ${query}`);
|
|
288
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Play a track or search query
|
|
531
|
+
*
|
|
532
|
+
* @param {string | Track} query - Track URL, search query, or Track object
|
|
533
|
+
* @param {string} requestedBy - User ID who requested the track
|
|
534
|
+
* @returns {Promise<boolean>} True if playback started successfully
|
|
535
|
+
* @example
|
|
536
|
+
* await player.play("Never Gonna Give You Up", userId);
|
|
537
|
+
* await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId);
|
|
538
|
+
* await player.play("tts: Hello everyone!", userId);
|
|
539
|
+
*/
|
|
289
540
|
async play(query, requestedBy) {
|
|
541
|
+
this.debug(`[Player] Play called with query: ${typeof query === "string" ? query : query?.title}`);
|
|
542
|
+
this.clearLeaveTimeout();
|
|
543
|
+
let tracksToAdd = [];
|
|
544
|
+
let isPlaylist = false;
|
|
545
|
+
let effectiveRequest = { query, requestedBy };
|
|
546
|
+
let hookResponse = {};
|
|
290
547
|
try {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
548
|
+
const hookOutcome = await this.runBeforePlayHooks(effectiveRequest);
|
|
549
|
+
effectiveRequest = hookOutcome.request;
|
|
550
|
+
hookResponse = hookOutcome.response;
|
|
551
|
+
if (effectiveRequest.requestedBy === undefined) {
|
|
552
|
+
effectiveRequest.requestedBy = requestedBy;
|
|
553
|
+
}
|
|
554
|
+
const hookTracks = Array.isArray(hookResponse.tracks) ? hookResponse.tracks : undefined;
|
|
555
|
+
if (hookResponse.handled && (!hookTracks || hookTracks.length === 0)) {
|
|
556
|
+
const handledPayload = {
|
|
557
|
+
success: hookResponse.success ?? true,
|
|
558
|
+
query: effectiveRequest.query,
|
|
559
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
560
|
+
tracks: [],
|
|
561
|
+
isPlaylist: hookResponse.isPlaylist ?? false,
|
|
562
|
+
error: hookResponse.error,
|
|
563
|
+
};
|
|
564
|
+
await this.runAfterPlayHooks(handledPayload);
|
|
565
|
+
if (hookResponse.error) {
|
|
566
|
+
this.emit("playerError", hookResponse.error);
|
|
567
|
+
}
|
|
568
|
+
return hookResponse.success ?? true;
|
|
569
|
+
}
|
|
570
|
+
if (hookTracks && hookTracks.length > 0) {
|
|
571
|
+
tracksToAdd = hookTracks;
|
|
572
|
+
isPlaylist = hookResponse.isPlaylist ?? hookTracks.length > 1;
|
|
573
|
+
}
|
|
574
|
+
else if (typeof effectiveRequest.query === "string") {
|
|
575
|
+
const searchResult = await this.search(effectiveRequest.query, effectiveRequest.requestedBy || "Unknown");
|
|
298
576
|
tracksToAdd = searchResult.tracks;
|
|
299
577
|
if (searchResult.playlist) {
|
|
300
578
|
isPlaylist = true;
|
|
301
579
|
this.debug(`[Player] Added playlist: ${searchResult.playlist.name} (${tracksToAdd.length} tracks)`);
|
|
302
580
|
}
|
|
303
581
|
}
|
|
304
|
-
else {
|
|
305
|
-
tracksToAdd = [query];
|
|
582
|
+
else if (effectiveRequest.query) {
|
|
583
|
+
tracksToAdd = [effectiveRequest.query];
|
|
306
584
|
}
|
|
307
585
|
if (tracksToAdd.length === 0) {
|
|
308
586
|
this.debug(`[Player] No tracks found for play`);
|
|
309
587
|
throw new Error("No tracks found");
|
|
310
588
|
}
|
|
311
|
-
// If a TTS track is requested and interrupt mode is enabled, handle it separately
|
|
312
589
|
const isTTS = (t) => {
|
|
313
590
|
if (!t)
|
|
314
591
|
return false;
|
|
@@ -319,14 +596,20 @@ class Player extends events_1.EventEmitter {
|
|
|
319
596
|
return false;
|
|
320
597
|
}
|
|
321
598
|
};
|
|
322
|
-
const queryLooksTTS = typeof query === "string" && query.trim().toLowerCase().startsWith("tts");
|
|
599
|
+
const queryLooksTTS = typeof effectiveRequest.query === "string" && effectiveRequest.query.trim().toLowerCase().startsWith("tts");
|
|
323
600
|
if (!isPlaylist &&
|
|
324
601
|
tracksToAdd.length > 0 &&
|
|
325
602
|
this.options?.tts?.interrupt !== false &&
|
|
326
603
|
(isTTS(tracksToAdd[0]) || queryLooksTTS)) {
|
|
327
|
-
// Interrupt music playback with TTS (do not modify the music queue)
|
|
328
604
|
this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`);
|
|
329
605
|
await this.interruptWithTTSTrack(tracksToAdd[0]);
|
|
606
|
+
await this.runAfterPlayHooks({
|
|
607
|
+
success: true,
|
|
608
|
+
query: effectiveRequest.query,
|
|
609
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
610
|
+
tracks: tracksToAdd,
|
|
611
|
+
isPlaylist,
|
|
612
|
+
});
|
|
330
613
|
return true;
|
|
331
614
|
}
|
|
332
615
|
if (isPlaylist) {
|
|
@@ -334,16 +617,28 @@ class Player extends events_1.EventEmitter {
|
|
|
334
617
|
this.emit("queueAddList", tracksToAdd);
|
|
335
618
|
}
|
|
336
619
|
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();
|
|
620
|
+
this.queue.add(tracksToAdd[0]);
|
|
621
|
+
this.emit("queueAdd", tracksToAdd[0]);
|
|
343
622
|
}
|
|
344
|
-
|
|
623
|
+
const started = !this.isPlaying ? await this.playNext() : true;
|
|
624
|
+
await this.runAfterPlayHooks({
|
|
625
|
+
success: started,
|
|
626
|
+
query: effectiveRequest.query,
|
|
627
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
628
|
+
tracks: tracksToAdd,
|
|
629
|
+
isPlaylist,
|
|
630
|
+
});
|
|
631
|
+
return started;
|
|
345
632
|
}
|
|
346
633
|
catch (error) {
|
|
634
|
+
await this.runAfterPlayHooks({
|
|
635
|
+
success: false,
|
|
636
|
+
query: effectiveRequest.query,
|
|
637
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
638
|
+
tracks: tracksToAdd,
|
|
639
|
+
isPlaylist,
|
|
640
|
+
error: error,
|
|
641
|
+
});
|
|
347
642
|
this.debug(`[Player] Play error:`, error);
|
|
348
643
|
this.emit("playerError", error);
|
|
349
644
|
return false;
|
|
@@ -352,6 +647,11 @@ class Player extends events_1.EventEmitter {
|
|
|
352
647
|
/**
|
|
353
648
|
* Interrupt current music with a TTS track. Pauses music, swaps the
|
|
354
649
|
* subscription to a dedicated TTS player, plays TTS, then resumes.
|
|
650
|
+
*
|
|
651
|
+
* @param {Track} track - The track to interrupt with
|
|
652
|
+
* @returns {Promise<void>}
|
|
653
|
+
* @example
|
|
654
|
+
* await player.interruptWithTTSTrack(track);
|
|
355
655
|
*/
|
|
356
656
|
async interruptWithTTSTrack(track) {
|
|
357
657
|
this.ttsQueue.push(track);
|
|
@@ -359,7 +659,13 @@ class Player extends events_1.EventEmitter {
|
|
|
359
659
|
void this.playNextTTS();
|
|
360
660
|
}
|
|
361
661
|
}
|
|
362
|
-
/**
|
|
662
|
+
/**
|
|
663
|
+
* Play queued TTS items sequentially
|
|
664
|
+
*
|
|
665
|
+
* @returns {Promise<void>}
|
|
666
|
+
* @example
|
|
667
|
+
* await player.playNextTTS();
|
|
668
|
+
*/
|
|
363
669
|
async playNextTTS() {
|
|
364
670
|
const next = this.ttsQueue.shift();
|
|
365
671
|
if (!next)
|
|
@@ -415,33 +721,160 @@ class Player extends events_1.EventEmitter {
|
|
|
415
721
|
}
|
|
416
722
|
}
|
|
417
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* Get cached plugin or find and cache a new one
|
|
726
|
+
* @param track The track to find plugin for
|
|
727
|
+
* @returns The matching plugin or null if not found
|
|
728
|
+
*/
|
|
729
|
+
getCachedPlugin(track) {
|
|
730
|
+
const cacheKey = `${track.source}:${track.url}`;
|
|
731
|
+
const now = Date.now();
|
|
732
|
+
// Check if cache is still valid
|
|
733
|
+
const cachedTimestamp = this.pluginCacheTimestamps.get(cacheKey);
|
|
734
|
+
if (cachedTimestamp && now - cachedTimestamp < this.PLUGIN_CACHE_TTL) {
|
|
735
|
+
const cachedPlugin = this.pluginCache.get(cacheKey);
|
|
736
|
+
if (cachedPlugin) {
|
|
737
|
+
this.debug(`[PluginCache] Using cached plugin for ${track.source}: ${cachedPlugin.name}`);
|
|
738
|
+
return cachedPlugin;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Find new plugin and cache it
|
|
742
|
+
this.debug(`[PluginCache] Finding plugin for track: ${track.title} (${track.source})`);
|
|
743
|
+
const plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
|
|
744
|
+
if (plugin) {
|
|
745
|
+
this.pluginCache.set(cacheKey, plugin);
|
|
746
|
+
this.pluginCacheTimestamps.set(cacheKey, now);
|
|
747
|
+
this.debug(`[PluginCache] Cached plugin: ${plugin.name} for ${track.source}`);
|
|
748
|
+
return plugin;
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Clear expired cache entries
|
|
754
|
+
*/
|
|
755
|
+
clearExpiredCache() {
|
|
756
|
+
const now = Date.now();
|
|
757
|
+
for (const [key, timestamp] of this.pluginCacheTimestamps.entries()) {
|
|
758
|
+
if (now - timestamp >= this.PLUGIN_CACHE_TTL) {
|
|
759
|
+
this.pluginCache.delete(key);
|
|
760
|
+
this.pluginCacheTimestamps.delete(key);
|
|
761
|
+
this.debug(`[PluginCache] Cleared expired cache entry: ${key}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Clear all plugin cache entries
|
|
767
|
+
* @example
|
|
768
|
+
* player.clearPluginCache();
|
|
769
|
+
*/
|
|
770
|
+
clearPluginCache() {
|
|
771
|
+
const cacheSize = this.pluginCache.size;
|
|
772
|
+
this.pluginCache.clear();
|
|
773
|
+
this.pluginCacheTimestamps.clear();
|
|
774
|
+
this.debug(`[PluginCache] Cleared all ${cacheSize} cache entries`);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Get plugin cache statistics
|
|
778
|
+
* @returns Cache statistics
|
|
779
|
+
* @example
|
|
780
|
+
* const stats = player.getPluginCacheStats();
|
|
781
|
+
* console.log(`Cache size: ${stats.size}, Hit rate: ${stats.hitRate}%`);
|
|
782
|
+
*/
|
|
783
|
+
getPluginCacheStats() {
|
|
784
|
+
const now = Date.now();
|
|
785
|
+
let expiredEntries = 0;
|
|
786
|
+
for (const timestamp of this.pluginCacheTimestamps.values()) {
|
|
787
|
+
if (now - timestamp >= this.PLUGIN_CACHE_TTL) {
|
|
788
|
+
expiredEntries++;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
size: this.pluginCache.size,
|
|
793
|
+
hitRate: 0, // Would need to track hits/misses to calculate this
|
|
794
|
+
expiredEntries,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
418
797
|
/** Build AudioResource for a given track using the plugin pipeline */
|
|
419
798
|
async resourceFromTrack(track) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (
|
|
799
|
+
this.debug(`[ResourceFromTrack] Starting resource creation for track: ${track.title} (${track.source})`);
|
|
800
|
+
// Clear expired cache entries periodically
|
|
801
|
+
if (Math.random() < 0.1) {
|
|
802
|
+
// 10% chance to clean cache
|
|
803
|
+
this.clearExpiredCache();
|
|
804
|
+
}
|
|
805
|
+
// Resolve plugin using cache
|
|
806
|
+
const plugin = this.getCachedPlugin(track);
|
|
807
|
+
if (!plugin) {
|
|
808
|
+
this.debug(`[ResourceFromTrack] No plugin found for track: ${track.title} (${track.source})`);
|
|
423
809
|
throw new Error(`No plugin found for track: ${track.title}`);
|
|
424
|
-
|
|
810
|
+
}
|
|
811
|
+
this.debug(`[ResourceFromTrack] Using plugin: ${plugin.name} for track: ${track.title}`);
|
|
812
|
+
let streamInfo = null;
|
|
813
|
+
const timeoutMs = this.options.extractorTimeout ?? 15000;
|
|
425
814
|
try {
|
|
426
|
-
|
|
815
|
+
this.debug(`[ResourceFromTrack] Attempting getStream with ${plugin.name}, timeout: ${timeoutMs}ms`);
|
|
816
|
+
const startTime = Date.now();
|
|
817
|
+
streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), timeoutMs, "getStream timed out");
|
|
818
|
+
const duration = Date.now() - startTime;
|
|
819
|
+
this.debug(`[ResourceFromTrack] getStream successful with ${plugin.name} in ${duration}ms`);
|
|
820
|
+
if (!streamInfo?.stream) {
|
|
821
|
+
this.debug(`[ResourceFromTrack] getStream returned no stream from ${plugin.name}`);
|
|
822
|
+
throw new Error(`No stream returned from ${plugin.name}`);
|
|
823
|
+
}
|
|
427
824
|
}
|
|
428
825
|
catch (streamError) {
|
|
826
|
+
const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
|
|
827
|
+
this.debug(`[ResourceFromTrack] getStream failed with ${plugin.name}: ${errorMessage}`);
|
|
828
|
+
// Log more details for debugging
|
|
829
|
+
if (streamError instanceof Error && streamError.stack) {
|
|
830
|
+
this.debug(`[ResourceFromTrack] getStream error stack:`, streamError.stack);
|
|
831
|
+
}
|
|
429
832
|
// try fallbacks
|
|
833
|
+
this.debug(`[ResourceFromTrack] Attempting fallback plugins for track: ${track.title}`);
|
|
430
834
|
const allplugs = this.pluginManager.getAll();
|
|
835
|
+
let fallbackAttempts = 0;
|
|
431
836
|
for (const p of allplugs) {
|
|
432
|
-
if (typeof p.getFallback !== "function")
|
|
837
|
+
if (typeof p.getFallback !== "function" && typeof p.getStream !== "function") {
|
|
838
|
+
this.debug(`[ResourceFromTrack] Skipping plugin ${p.name} - no getFallback or getStream method`);
|
|
433
839
|
continue;
|
|
840
|
+
}
|
|
841
|
+
fallbackAttempts++;
|
|
842
|
+
this.debug(`[ResourceFromTrack] Trying fallback plugin ${p.name} (attempt ${fallbackAttempts})`);
|
|
434
843
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
844
|
+
// Try getStream first
|
|
845
|
+
const startTime = Date.now();
|
|
846
|
+
streamInfo = await (0, timeout_1.withTimeout)(p.getStream(track), timeoutMs, "getStream timed out");
|
|
847
|
+
const duration = Date.now() - startTime;
|
|
848
|
+
if (streamInfo?.stream) {
|
|
849
|
+
this.debug(`[ResourceFromTrack] Fallback getStream successful with ${p.name} in ${duration}ms`);
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
// Try getFallback if getStream didn't work
|
|
853
|
+
this.debug(`[ResourceFromTrack] Trying getFallback with ${p.name}`);
|
|
854
|
+
const fallbackStartTime = Date.now();
|
|
855
|
+
streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), timeoutMs, `getFallback timed out for plugin ${p.name}`);
|
|
856
|
+
const fallbackDuration = Date.now() - fallbackStartTime;
|
|
857
|
+
if (streamInfo?.stream) {
|
|
858
|
+
this.debug(`[ResourceFromTrack] Fallback getFallback successful with ${p.name} in ${fallbackDuration}ms`);
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
this.debug(`[ResourceFromTrack] Fallback plugin ${p.name} returned no stream`);
|
|
862
|
+
}
|
|
863
|
+
catch (fallbackError) {
|
|
864
|
+
const errorMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
865
|
+
this.debug(`[ResourceFromTrack] Fallback plugin ${p.name} failed: ${errorMessage}`);
|
|
866
|
+
// Log more details for debugging
|
|
867
|
+
if (fallbackError instanceof Error && fallbackError.stack) {
|
|
868
|
+
this.debug(`[ResourceFromTrack] Fallback error stack:`, fallbackError.stack);
|
|
869
|
+
}
|
|
439
870
|
}
|
|
440
|
-
catch { }
|
|
441
871
|
}
|
|
442
|
-
if (!streamInfo?.stream)
|
|
872
|
+
if (!streamInfo?.stream) {
|
|
873
|
+
this.debug(`[ResourceFromTrack] All ${fallbackAttempts} fallback attempts failed for track: ${track.title}`);
|
|
443
874
|
throw new Error(`All getFallback attempts failed for track: ${track.title}`);
|
|
875
|
+
}
|
|
444
876
|
}
|
|
877
|
+
this.debug(`[ResourceFromTrack] Stream obtained, type: ${streamInfo.type}, metadata keys: ${Object.keys(streamInfo.metadata || {}).join(", ")}`);
|
|
445
878
|
const mapToStreamType = (type) => {
|
|
446
879
|
switch (type) {
|
|
447
880
|
case "webm/opus":
|
|
@@ -454,15 +887,20 @@ class Player extends events_1.EventEmitter {
|
|
|
454
887
|
}
|
|
455
888
|
};
|
|
456
889
|
const inputType = mapToStreamType(streamInfo.type);
|
|
457
|
-
|
|
890
|
+
this.debug(`[ResourceFromTrack] Creating AudioResource with inputType: ${inputType}`);
|
|
891
|
+
// Merge metadata safely
|
|
892
|
+
const mergedMetadata = {
|
|
893
|
+
...track,
|
|
894
|
+
...(streamInfo.metadata || {}),
|
|
895
|
+
};
|
|
896
|
+
const audioResource = (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
458
897
|
// Prefer plugin-provided metadata (e.g., precise duration), fallback to track fields
|
|
459
|
-
metadata:
|
|
460
|
-
...track,
|
|
461
|
-
...(streamInfo?.metadata || {}),
|
|
462
|
-
},
|
|
898
|
+
metadata: mergedMetadata,
|
|
463
899
|
inputType,
|
|
464
900
|
inlineVolume: true,
|
|
465
901
|
});
|
|
902
|
+
this.debug(`[ResourceFromTrack] AudioResource created successfully for track: ${track.title}`);
|
|
903
|
+
return audioResource;
|
|
466
904
|
}
|
|
467
905
|
async generateWillNext() {
|
|
468
906
|
const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack;
|
|
@@ -475,10 +913,10 @@ class Player extends events_1.EventEmitter {
|
|
|
475
913
|
for (const p of candidates) {
|
|
476
914
|
try {
|
|
477
915
|
this.debug(`[Player] Trying related from plugin: ${p.name}`);
|
|
478
|
-
const related = await
|
|
916
|
+
const related = await (0, timeout_1.withTimeout)(p.getRelatedTracks(lastTrack.url, {
|
|
479
917
|
limit: 10,
|
|
480
918
|
history: this.queue.previousTracks,
|
|
481
|
-
}), `getRelatedTracks timed out for ${p.name}`);
|
|
919
|
+
}), this.options.extractorTimeout ?? 15000, `getRelatedTracks timed out for ${p.name}`);
|
|
482
920
|
if (Array.isArray(related) && related.length > 0) {
|
|
483
921
|
const randomchoice = Math.floor(Math.random() * related.length);
|
|
484
922
|
const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
|
|
@@ -529,6 +967,14 @@ class Player extends events_1.EventEmitter {
|
|
|
529
967
|
return this.playNext();
|
|
530
968
|
}
|
|
531
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* Pause the current track
|
|
972
|
+
*
|
|
973
|
+
* @returns {boolean} True if paused successfully
|
|
974
|
+
* @example
|
|
975
|
+
* const paused = player.pause();
|
|
976
|
+
* console.log(`Paused: ${paused}`);
|
|
977
|
+
*/
|
|
532
978
|
pause() {
|
|
533
979
|
this.debug(`[Player] pause called`);
|
|
534
980
|
if (this.isPlaying && !this.isPaused) {
|
|
@@ -536,6 +982,14 @@ class Player extends events_1.EventEmitter {
|
|
|
536
982
|
}
|
|
537
983
|
return false;
|
|
538
984
|
}
|
|
985
|
+
/**
|
|
986
|
+
* Resume the current track
|
|
987
|
+
*
|
|
988
|
+
* @returns {boolean} True if resumed successfully
|
|
989
|
+
* @example
|
|
990
|
+
* const resumed = player.resume();
|
|
991
|
+
* console.log(`Resumed: ${resumed}`);
|
|
992
|
+
*/
|
|
539
993
|
resume() {
|
|
540
994
|
this.debug(`[Player] resume called`);
|
|
541
995
|
if (this.isPaused) {
|
|
@@ -551,6 +1005,14 @@ class Player extends events_1.EventEmitter {
|
|
|
551
1005
|
}
|
|
552
1006
|
return false;
|
|
553
1007
|
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Stop the current track
|
|
1010
|
+
*
|
|
1011
|
+
* @returns {boolean} True if stopped successfully
|
|
1012
|
+
* @example
|
|
1013
|
+
* const stopped = player.stop();
|
|
1014
|
+
* console.log(`Stopped: ${stopped}`);
|
|
1015
|
+
*/
|
|
554
1016
|
stop() {
|
|
555
1017
|
this.debug(`[Player] stop called`);
|
|
556
1018
|
this.queue.clear();
|
|
@@ -560,6 +1022,14 @@ class Player extends events_1.EventEmitter {
|
|
|
560
1022
|
this.emit("playerStop");
|
|
561
1023
|
return result;
|
|
562
1024
|
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Skip to the next track
|
|
1027
|
+
*
|
|
1028
|
+
* @returns {boolean} True if skipped successfully
|
|
1029
|
+
* @example
|
|
1030
|
+
* const skipped = player.skip();
|
|
1031
|
+
* console.log(`Skipped: ${skipped}`);
|
|
1032
|
+
*/
|
|
563
1033
|
skip() {
|
|
564
1034
|
this.debug(`[Player] skip called`);
|
|
565
1035
|
if (this.isPlaying || this.isPaused) {
|
|
@@ -570,6 +1040,11 @@ class Player extends events_1.EventEmitter {
|
|
|
570
1040
|
}
|
|
571
1041
|
/**
|
|
572
1042
|
* Go back to the previous track in history and play it.
|
|
1043
|
+
*
|
|
1044
|
+
* @returns {Promise<boolean>} True if previous track was played successfully
|
|
1045
|
+
* @example
|
|
1046
|
+
* const previous = await player.previous();
|
|
1047
|
+
* console.log(`Previous: ${previous}`);
|
|
573
1048
|
*/
|
|
574
1049
|
async previous() {
|
|
575
1050
|
this.debug(`[Player] previous called`);
|
|
@@ -581,12 +1056,39 @@ class Player extends events_1.EventEmitter {
|
|
|
581
1056
|
this.clearLeaveTimeout();
|
|
582
1057
|
return this.startTrack(track);
|
|
583
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Loop the current track
|
|
1061
|
+
*
|
|
1062
|
+
* @param {LoopMode} mode - The loop mode to set
|
|
1063
|
+
* @returns {LoopMode} The loop mode
|
|
1064
|
+
* @example
|
|
1065
|
+
* const loopMode = player.loop("track");
|
|
1066
|
+
* console.log(`Loop mode: ${loopMode}`);
|
|
1067
|
+
*/
|
|
584
1068
|
loop(mode) {
|
|
585
1069
|
return this.queue.loop(mode);
|
|
586
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Set the auto-play mode
|
|
1073
|
+
*
|
|
1074
|
+
* @param {boolean} mode - The auto-play mode to set
|
|
1075
|
+
* @returns {boolean} The auto-play mode
|
|
1076
|
+
* @example
|
|
1077
|
+
* const autoPlayMode = player.autoPlay(true);
|
|
1078
|
+
* console.log(`Auto-play mode: ${autoPlayMode}`);
|
|
1079
|
+
*/
|
|
587
1080
|
autoPlay(mode) {
|
|
588
1081
|
return this.queue.autoPlay(mode);
|
|
589
1082
|
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Set the volume of the current track
|
|
1085
|
+
*
|
|
1086
|
+
* @param {number} volume - The volume to set
|
|
1087
|
+
* @returns {boolean} True if volume was set successfully
|
|
1088
|
+
* @example
|
|
1089
|
+
* const volumeSet = player.setVolume(50);
|
|
1090
|
+
* console.log(`Volume set: ${volumeSet}`);
|
|
1091
|
+
*/
|
|
590
1092
|
setVolume(volume) {
|
|
591
1093
|
this.debug(`[Player] setVolume called: ${volume}`);
|
|
592
1094
|
if (volume < 0 || volume > 200)
|
|
@@ -614,10 +1116,24 @@ class Player extends events_1.EventEmitter {
|
|
|
614
1116
|
this.emit("volumeChange", oldVolume, volume);
|
|
615
1117
|
return true;
|
|
616
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Shuffle the queue
|
|
1121
|
+
*
|
|
1122
|
+
* @returns {void}
|
|
1123
|
+
* @example
|
|
1124
|
+
* player.shuffle();
|
|
1125
|
+
*/
|
|
617
1126
|
shuffle() {
|
|
618
1127
|
this.debug(`[Player] shuffle called`);
|
|
619
1128
|
this.queue.shuffle();
|
|
620
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Clear the queue
|
|
1132
|
+
*
|
|
1133
|
+
* @returns {void}
|
|
1134
|
+
* @example
|
|
1135
|
+
* player.clearQueue();
|
|
1136
|
+
*/
|
|
621
1137
|
clearQueue() {
|
|
622
1138
|
this.debug(`[Player] clearQueue called`);
|
|
623
1139
|
this.queue.clear();
|
|
@@ -627,6 +1143,14 @@ class Player extends events_1.EventEmitter {
|
|
|
627
1143
|
* - If `query` is a string, performs a search and inserts resulting tracks (playlist supported).
|
|
628
1144
|
* - If a Track or Track[] is provided, inserts directly.
|
|
629
1145
|
* Does not auto-start playback; it only modifies the queue.
|
|
1146
|
+
*
|
|
1147
|
+
* @param {string | Track | Track[]} query - The track or tracks to insert
|
|
1148
|
+
* @param {number} index - The index to insert the tracks at
|
|
1149
|
+
* @param {string} requestedBy - The user ID who requested the insert
|
|
1150
|
+
* @returns {Promise<boolean>} True if the tracks were inserted successfully
|
|
1151
|
+
* @example
|
|
1152
|
+
* const inserted = await player.insert("Song Name", 0, userId);
|
|
1153
|
+
* console.log(`Inserted: ${inserted}`);
|
|
630
1154
|
*/
|
|
631
1155
|
async insert(query, index, requestedBy) {
|
|
632
1156
|
try {
|
|
@@ -667,6 +1191,15 @@ class Player extends events_1.EventEmitter {
|
|
|
667
1191
|
return false;
|
|
668
1192
|
}
|
|
669
1193
|
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Remove a track from the queue
|
|
1196
|
+
*
|
|
1197
|
+
* @param {number} index - The index of the track to remove
|
|
1198
|
+
* @returns {Track | null} The removed track or null
|
|
1199
|
+
* @example
|
|
1200
|
+
* const removed = player.remove(0);
|
|
1201
|
+
* console.log(`Removed: ${removed?.title}`);
|
|
1202
|
+
*/
|
|
670
1203
|
remove(index) {
|
|
671
1204
|
this.debug(`[Player] remove called for index: ${index}`);
|
|
672
1205
|
const track = this.queue.remove(index);
|
|
@@ -675,6 +1208,15 @@ class Player extends events_1.EventEmitter {
|
|
|
675
1208
|
}
|
|
676
1209
|
return track;
|
|
677
1210
|
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Get the progress bar of the current track
|
|
1213
|
+
*
|
|
1214
|
+
* @param {ProgressBarOptions} options - The options for the progress bar
|
|
1215
|
+
* @returns {string} The progress bar
|
|
1216
|
+
* @example
|
|
1217
|
+
* const progressBar = player.getProgressBar();
|
|
1218
|
+
* console.log(`Progress bar: ${progressBar}`);
|
|
1219
|
+
*/
|
|
678
1220
|
getProgressBar(options = {}) {
|
|
679
1221
|
const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
|
|
680
1222
|
const track = this.queue.currentTrack;
|
|
@@ -690,6 +1232,14 @@ class Player extends events_1.EventEmitter {
|
|
|
690
1232
|
const bar = barChar.repeat(progress) + progressChar + barChar.repeat(size - progress);
|
|
691
1233
|
return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`;
|
|
692
1234
|
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Get the time of the current track
|
|
1237
|
+
*
|
|
1238
|
+
* @returns {Object} The time of the current track
|
|
1239
|
+
* @example
|
|
1240
|
+
* const time = player.getTime();
|
|
1241
|
+
* console.log(`Time: ${time.current}`);
|
|
1242
|
+
*/
|
|
693
1243
|
getTime() {
|
|
694
1244
|
const resource = this.currentResource;
|
|
695
1245
|
const track = this.queue.currentTrack;
|
|
@@ -706,6 +1256,15 @@ class Player extends events_1.EventEmitter {
|
|
|
706
1256
|
format: this.formatTime(resource.playbackDuration),
|
|
707
1257
|
};
|
|
708
1258
|
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Format the time in the format of HH:MM:SS
|
|
1261
|
+
*
|
|
1262
|
+
* @param {number} ms - The time in milliseconds
|
|
1263
|
+
* @returns {string} The formatted time
|
|
1264
|
+
* @example
|
|
1265
|
+
* const formattedTime = player.formatTime(1000);
|
|
1266
|
+
* console.log(`Formatted time: ${formattedTime}`);
|
|
1267
|
+
*/
|
|
709
1268
|
formatTime(ms) {
|
|
710
1269
|
const totalSeconds = Math.floor(ms / 1000);
|
|
711
1270
|
const hours = Math.floor(totalSeconds / 3600);
|
|
@@ -730,6 +1289,13 @@ class Player extends events_1.EventEmitter {
|
|
|
730
1289
|
}, this.options.leaveTimeout);
|
|
731
1290
|
}
|
|
732
1291
|
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Destroy the player
|
|
1294
|
+
*
|
|
1295
|
+
* @returns {void}
|
|
1296
|
+
* @example
|
|
1297
|
+
* player.destroy();
|
|
1298
|
+
*/
|
|
733
1299
|
destroy() {
|
|
734
1300
|
this.debug(`[Player] destroy called`);
|
|
735
1301
|
if (this.leaveTimeout) {
|
|
@@ -750,30 +1316,92 @@ class Player extends events_1.EventEmitter {
|
|
|
750
1316
|
}
|
|
751
1317
|
this.queue.clear();
|
|
752
1318
|
this.pluginManager.clear();
|
|
1319
|
+
for (const extension of [...this.extensions]) {
|
|
1320
|
+
this.invokeExtensionLifecycle(extension, "onDestroy");
|
|
1321
|
+
if (extension.player === this) {
|
|
1322
|
+
extension.player = null;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
this.extensions = [];
|
|
753
1326
|
this.isPlaying = false;
|
|
754
1327
|
this.isPaused = false;
|
|
755
1328
|
this.emit("playerDestroy");
|
|
756
1329
|
this.removeAllListeners();
|
|
757
1330
|
}
|
|
758
|
-
|
|
1331
|
+
/**
|
|
1332
|
+
* Get the size of the queue
|
|
1333
|
+
*
|
|
1334
|
+
* @returns {number} The size of the queue
|
|
1335
|
+
* @example
|
|
1336
|
+
* const queueSize = player.queueSize;
|
|
1337
|
+
* console.log(`Queue size: ${queueSize}`);
|
|
1338
|
+
*/
|
|
759
1339
|
get queueSize() {
|
|
760
1340
|
return this.queue.size;
|
|
761
1341
|
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Get the current track
|
|
1344
|
+
*
|
|
1345
|
+
* @returns {Track | null} The current track or null
|
|
1346
|
+
* @example
|
|
1347
|
+
* const currentTrack = player.currentTrack;
|
|
1348
|
+
* console.log(`Current track: ${currentTrack?.title}`);
|
|
1349
|
+
*/
|
|
762
1350
|
get currentTrack() {
|
|
763
1351
|
return this.queue.currentTrack;
|
|
764
1352
|
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Get the previous track
|
|
1355
|
+
*
|
|
1356
|
+
* @returns {Track | null} The previous track or null
|
|
1357
|
+
* @example
|
|
1358
|
+
* const previousTrack = player.previousTrack;
|
|
1359
|
+
* console.log(`Previous track: ${previousTrack?.title}`);
|
|
1360
|
+
*/
|
|
765
1361
|
get previousTrack() {
|
|
766
1362
|
return this.queue.previousTracks?.at(-1) ?? null;
|
|
767
1363
|
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Get the upcoming tracks
|
|
1366
|
+
*
|
|
1367
|
+
* @returns {Track[]} The upcoming tracks
|
|
1368
|
+
* @example
|
|
1369
|
+
* const upcomingTracks = player.upcomingTracks;
|
|
1370
|
+
* console.log(`Upcoming tracks: ${upcomingTracks.length}`);
|
|
1371
|
+
*/
|
|
768
1372
|
get upcomingTracks() {
|
|
769
1373
|
return this.queue.getTracks();
|
|
770
1374
|
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Get the previous tracks
|
|
1377
|
+
*
|
|
1378
|
+
* @returns {Track[]} The previous tracks
|
|
1379
|
+
* @example
|
|
1380
|
+
* const previousTracks = player.previousTracks;
|
|
1381
|
+
* console.log(`Previous tracks: ${previousTracks.length}`);
|
|
1382
|
+
*/
|
|
771
1383
|
get previousTracks() {
|
|
772
1384
|
return this.queue.previousTracks;
|
|
773
1385
|
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Get the available plugins
|
|
1388
|
+
*
|
|
1389
|
+
* @returns {string[]} The available plugins
|
|
1390
|
+
* @example
|
|
1391
|
+
* const availablePlugins = player.availablePlugins;
|
|
1392
|
+
* console.log(`Available plugins: ${availablePlugins.length}`);
|
|
1393
|
+
*/
|
|
774
1394
|
get availablePlugins() {
|
|
775
1395
|
return this.pluginManager.getAll().map((p) => p.name);
|
|
776
1396
|
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Get the related tracks
|
|
1399
|
+
*
|
|
1400
|
+
* @returns {Track[] | null} The related tracks or null
|
|
1401
|
+
* @example
|
|
1402
|
+
* const relatedTracks = player.relatedTracks;
|
|
1403
|
+
* console.log(`Related tracks: ${relatedTracks?.length}`);
|
|
1404
|
+
*/
|
|
777
1405
|
get relatedTracks() {
|
|
778
1406
|
return this.queue.relatedTracks();
|
|
779
1407
|
}
|