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
package/src/structures/Player.ts
CHANGED
|
@@ -15,15 +15,70 @@ import {
|
|
|
15
15
|
|
|
16
16
|
import { VoiceChannel } from "discord.js";
|
|
17
17
|
import { Readable } from "stream";
|
|
18
|
-
import {
|
|
18
|
+
import { BaseExtension } from "../extensions";
|
|
19
|
+
import {
|
|
20
|
+
Track,
|
|
21
|
+
PlayerOptions,
|
|
22
|
+
PlayerEvents,
|
|
23
|
+
SourcePlugin,
|
|
24
|
+
SearchResult,
|
|
25
|
+
ProgressBarOptions,
|
|
26
|
+
LoopMode,
|
|
27
|
+
StreamInfo,
|
|
28
|
+
} from "../types";
|
|
29
|
+
import type {
|
|
30
|
+
ExtensionContext,
|
|
31
|
+
ExtensionPlayRequest,
|
|
32
|
+
ExtensionPlayResponse,
|
|
33
|
+
ExtensionAfterPlayPayload,
|
|
34
|
+
ExtensionStreamRequest,
|
|
35
|
+
ExtensionSearchRequest,
|
|
36
|
+
} from "../types";
|
|
19
37
|
import { Queue } from "./Queue";
|
|
20
38
|
import { PluginManager } from "../plugins";
|
|
39
|
+
import { withTimeout } from "../utils/timeout";
|
|
21
40
|
import type { PlayerManager } from "./PlayerManager";
|
|
22
41
|
export declare interface Player {
|
|
23
42
|
on<K extends keyof PlayerEvents>(event: K, listener: (...args: PlayerEvents[K]) => void): this;
|
|
24
43
|
emit<K extends keyof PlayerEvents>(event: K, ...args: PlayerEvents[K]): boolean;
|
|
25
44
|
}
|
|
26
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Represents a music player for a specific Discord guild.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Create and configure player
|
|
51
|
+
* const player = await manager.create(guildId, {
|
|
52
|
+
* tts: { interrupt: true, volume: 1 },
|
|
53
|
+
* leaveOnEnd: true,
|
|
54
|
+
* leaveTimeout: 30000
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* // Connect to voice channel
|
|
58
|
+
* await player.connect(voiceChannel);
|
|
59
|
+
*
|
|
60
|
+
* // Play different types of content
|
|
61
|
+
* await player.play("Never Gonna Give You Up", userId); // Search query
|
|
62
|
+
* await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); // Direct URL
|
|
63
|
+
* await player.play("tts: Hello everyone!", userId); // Text-to-Speech
|
|
64
|
+
*
|
|
65
|
+
* // Player controls
|
|
66
|
+
* player.pause(); // Pause current track
|
|
67
|
+
* player.resume(); // Resume paused track
|
|
68
|
+
* player.skip(); // Skip to next track
|
|
69
|
+
* player.stop(); // Stop and clear queue
|
|
70
|
+
* player.setVolume(0.5); // Set volume to 50%
|
|
71
|
+
*
|
|
72
|
+
* // Event handling
|
|
73
|
+
* player.on("trackStart", (player, track) => {
|
|
74
|
+
* console.log(`Now playing: ${track.title}`);
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* player.on("queueEnd", (player) => {
|
|
78
|
+
* console.log("Queue finished");
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
*/
|
|
27
82
|
export class Player extends EventEmitter {
|
|
28
83
|
public readonly guildId: string;
|
|
29
84
|
public connection: VoiceConnection | null = null;
|
|
@@ -40,83 +95,259 @@ export class Player extends EventEmitter {
|
|
|
40
95
|
private currentResource: AudioResource | null = null;
|
|
41
96
|
private volumeInterval: NodeJS.Timeout | null = null;
|
|
42
97
|
private skipLoop = false;
|
|
98
|
+
private extensions: BaseExtension[] = [];
|
|
99
|
+
private extensionContext!: ExtensionContext;
|
|
43
100
|
|
|
44
101
|
/**
|
|
45
|
-
*
|
|
102
|
+
* Attach an extension to the player
|
|
103
|
+
*
|
|
104
|
+
* @param {BaseExtension} extension - The extension to attach
|
|
105
|
+
* @example
|
|
106
|
+
* player.attachExtension(new MyExtension());
|
|
46
107
|
*/
|
|
47
|
-
|
|
108
|
+
public attachExtension(extension: BaseExtension): void {
|
|
109
|
+
if (this.extensions.includes(extension)) return;
|
|
110
|
+
if (!extension.player) extension.player = this;
|
|
111
|
+
this.extensions.push(extension);
|
|
112
|
+
this.invokeExtensionLifecycle(extension, "onRegister");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Detach an extension from the player
|
|
117
|
+
*
|
|
118
|
+
* @param {BaseExtension} extension - The extension to detach
|
|
119
|
+
* @example
|
|
120
|
+
* player.detachExtension(new MyExtension());
|
|
121
|
+
*/
|
|
122
|
+
public detachExtension(extension: BaseExtension): void {
|
|
123
|
+
const index = this.extensions.indexOf(extension);
|
|
124
|
+
if (index === -1) return;
|
|
125
|
+
this.extensions.splice(index, 1);
|
|
126
|
+
this.invokeExtensionLifecycle(extension, "onDestroy");
|
|
127
|
+
if (extension.player === this) {
|
|
128
|
+
extension.player = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get all extensions attached to the player
|
|
134
|
+
*
|
|
135
|
+
* @returns {readonly BaseExtension[]} All attached extensions
|
|
136
|
+
* @example
|
|
137
|
+
* const extensions = player.getExtensions();
|
|
138
|
+
* console.log(`Extensions: ${extensions.length}`);
|
|
139
|
+
*/
|
|
140
|
+
public getExtensions(): readonly BaseExtension[] {
|
|
141
|
+
return this.extensions;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private invokeExtensionLifecycle(extension: BaseExtension, hook: "onRegister" | "onDestroy"): void {
|
|
145
|
+
const fn = (extension as any)[hook];
|
|
146
|
+
if (typeof fn !== "function") return;
|
|
48
147
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
148
|
+
const result = fn.call(extension, this.extensionContext);
|
|
149
|
+
if (result && typeof (result as Promise<unknown>).then === "function") {
|
|
150
|
+
(result as Promise<unknown>).catch((err) => this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err));
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
51
156
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
157
|
+
private async runBeforePlayHooks(
|
|
158
|
+
initial: ExtensionPlayRequest,
|
|
159
|
+
): Promise<{ request: ExtensionPlayRequest; response: ExtensionPlayResponse }> {
|
|
160
|
+
const request: ExtensionPlayRequest = { ...initial };
|
|
161
|
+
const response: ExtensionPlayResponse = {};
|
|
162
|
+
for (const extension of this.extensions) {
|
|
163
|
+
const hook = (extension as any).beforePlay;
|
|
164
|
+
if (typeof hook !== "function") continue;
|
|
165
|
+
try {
|
|
166
|
+
const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
|
|
167
|
+
if (!result) continue;
|
|
168
|
+
if (result.query !== undefined) {
|
|
169
|
+
request.query = result.query;
|
|
170
|
+
response.query = result.query;
|
|
171
|
+
}
|
|
172
|
+
if (result.requestedBy !== undefined) {
|
|
173
|
+
request.requestedBy = result.requestedBy;
|
|
174
|
+
response.requestedBy = result.requestedBy;
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(result.tracks)) {
|
|
177
|
+
response.tracks = result.tracks;
|
|
178
|
+
}
|
|
179
|
+
if (typeof result.isPlaylist === "boolean") {
|
|
180
|
+
response.isPlaylist = result.isPlaylist;
|
|
181
|
+
}
|
|
182
|
+
if (typeof result.success === "boolean") {
|
|
183
|
+
response.success = result.success;
|
|
184
|
+
}
|
|
185
|
+
if (result.error instanceof Error) {
|
|
186
|
+
response.error = result.error;
|
|
187
|
+
}
|
|
188
|
+
if (typeof result.handled === "boolean") {
|
|
189
|
+
response.handled = result.handled;
|
|
190
|
+
if (result.handled) break;
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
this.debug(`[Player] Extension ${extension.name} beforePlay error:`, err);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { request, response };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async runAfterPlayHooks(payload: ExtensionAfterPlayPayload): Promise<void> {
|
|
200
|
+
if (this.extensions.length === 0) return;
|
|
201
|
+
const safeTracks = payload.tracks ? [...payload.tracks] : undefined;
|
|
202
|
+
if (safeTracks) {
|
|
203
|
+
Object.freeze(safeTracks);
|
|
204
|
+
}
|
|
205
|
+
const immutablePayload = Object.freeze({ ...payload, tracks: safeTracks });
|
|
206
|
+
for (const extension of this.extensions) {
|
|
207
|
+
const hook = (extension as any).afterPlay;
|
|
208
|
+
if (typeof hook !== "function") continue;
|
|
209
|
+
try {
|
|
210
|
+
await Promise.resolve(hook.call(extension, this.extensionContext, immutablePayload));
|
|
211
|
+
} catch (err) {
|
|
212
|
+
this.debug(`[Player] Extension ${extension.name} afterPlay error:`, err);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async extensionsProvideSearch(query: string, requestedBy: string): Promise<SearchResult | null> {
|
|
218
|
+
const request: ExtensionSearchRequest = { query, requestedBy };
|
|
219
|
+
for (const extension of this.extensions) {
|
|
220
|
+
const hook = (extension as any).provideSearch;
|
|
221
|
+
if (typeof hook !== "function") continue;
|
|
222
|
+
try {
|
|
223
|
+
const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
|
|
224
|
+
if (result && Array.isArray(result.tracks) && result.tracks.length > 0) {
|
|
225
|
+
this.debug(`[Player] Extension ${extension.name} handled search for query: ${query}`);
|
|
226
|
+
return result as SearchResult;
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
this.debug(`[Player] Extension ${extension.name} provideSearch error:`, err);
|
|
55
230
|
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
56
234
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
235
|
+
private async extensionsProvideStream(track: Track): Promise<StreamInfo | null> {
|
|
236
|
+
const request: ExtensionStreamRequest = { track };
|
|
237
|
+
for (const extension of this.extensions) {
|
|
238
|
+
const hook = (extension as any).provideStream;
|
|
239
|
+
if (typeof hook !== "function") continue;
|
|
61
240
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
241
|
+
const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
|
|
242
|
+
if (result && (result as StreamInfo).stream) {
|
|
243
|
+
this.debug(`[Player] Extension ${extension.name} provided stream for track: ${track.title}`);
|
|
244
|
+
return result as StreamInfo;
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
this.debug(`[Player] Extension ${extension.name} provideStream error:`, err);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Start playing a specific track immediately, replacing the current resource.
|
|
255
|
+
*/
|
|
256
|
+
private async startTrack(track: Track): Promise<boolean> {
|
|
257
|
+
try {
|
|
258
|
+
let streamInfo: StreamInfo | null = await this.extensionsProvideStream(track);
|
|
259
|
+
let plugin: SourcePlugin | undefined;
|
|
260
|
+
|
|
261
|
+
if (!streamInfo) {
|
|
262
|
+
plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
|
|
263
|
+
|
|
264
|
+
if (!plugin) {
|
|
265
|
+
this.debug(`[Player] No plugin found for track: ${track.title}`);
|
|
266
|
+
throw new Error(`No plugin found for track: ${track.title}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.debug(`[Player] Getting stream for track: ${track.title}`);
|
|
270
|
+
this.debug(`[Player] Using plugin: ${plugin.name}`);
|
|
271
|
+
this.debug(`[Track] Track Info:`, track);
|
|
272
|
+
try {
|
|
273
|
+
streamInfo = await withTimeout(plugin.getStream(track), this.options.extractorTimeout ?? 15000, "getStream timed out");
|
|
274
|
+
} catch (streamError) {
|
|
275
|
+
this.debug(`[Player] getStream failed, trying getFallback:`, streamError);
|
|
276
|
+
const allplugs = this.pluginManager.getAll();
|
|
277
|
+
for (const p of allplugs) {
|
|
278
|
+
if (typeof (p as any).getFallback !== "function") {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
streamInfo = await withTimeout(
|
|
283
|
+
(p as any).getFallback(track),
|
|
284
|
+
this.options.extractorTimeout ?? 15000,
|
|
285
|
+
`getFallback timed out for plugin ${p.name}`,
|
|
286
|
+
);
|
|
287
|
+
if (!(streamInfo as any)?.stream) continue;
|
|
288
|
+
this.debug(`[Player] getFallback succeeded with plugin ${p.name} for track: ${track.title}`);
|
|
289
|
+
break;
|
|
290
|
+
} catch (fallbackError) {
|
|
291
|
+
this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError);
|
|
292
|
+
}
|
|
69
293
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!(streamInfo as any).stream) continue;
|
|
73
|
-
this.debug(`[Player] getFallback succeeded with plugin ${p.name} for track: ${track.title}`);
|
|
74
|
-
break;
|
|
75
|
-
} catch (fallbackError) {
|
|
76
|
-
this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError);
|
|
294
|
+
if (!(streamInfo as any)?.stream) {
|
|
295
|
+
throw new Error(`All getFallback attempts failed for track: ${track.title}`);
|
|
77
296
|
}
|
|
78
297
|
}
|
|
298
|
+
} else {
|
|
299
|
+
this.debug(`[Player] Using extension-provided stream for track: ${track.title}`);
|
|
300
|
+
}
|
|
79
301
|
|
|
80
|
-
|
|
81
|
-
throw new Error(`All getFallback attempts failed for track: ${track.title}`);
|
|
82
|
-
}
|
|
302
|
+
if (plugin) {
|
|
83
303
|
this.debug(streamInfo);
|
|
84
304
|
}
|
|
85
305
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
306
|
+
// Kiểm tra nếu có stream thực sự để tạo AudioResource
|
|
307
|
+
if (streamInfo && (streamInfo as any).stream) {
|
|
308
|
+
function mapToStreamType(type: string | undefined): StreamType {
|
|
309
|
+
switch (type) {
|
|
310
|
+
case "webm/opus":
|
|
311
|
+
return StreamType.WebmOpus;
|
|
312
|
+
case "ogg/opus":
|
|
313
|
+
return StreamType.OggOpus;
|
|
314
|
+
case "arbitrary":
|
|
315
|
+
default:
|
|
316
|
+
return StreamType.Arbitrary;
|
|
317
|
+
}
|
|
96
318
|
}
|
|
97
|
-
}
|
|
98
319
|
|
|
99
|
-
|
|
100
|
-
|
|
320
|
+
const stream: Readable = (streamInfo as StreamInfo).stream;
|
|
321
|
+
const inputType = mapToStreamType((streamInfo as StreamInfo).type);
|
|
101
322
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
323
|
+
this.currentResource = createAudioResource(stream, {
|
|
324
|
+
metadata: track,
|
|
325
|
+
inputType,
|
|
326
|
+
inlineVolume: true,
|
|
327
|
+
});
|
|
107
328
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
329
|
+
// Apply initial volume using the resource's VolumeTransformer
|
|
330
|
+
if (this.volumeInterval) {
|
|
331
|
+
clearInterval(this.volumeInterval);
|
|
332
|
+
this.volumeInterval = null;
|
|
333
|
+
}
|
|
334
|
+
this.currentResource.volume?.setVolume(this.volume / 100);
|
|
114
335
|
|
|
115
|
-
|
|
116
|
-
|
|
336
|
+
this.debug(`[Player] Playing resource for track: ${track.title}`);
|
|
337
|
+
this.audioPlayer.play(this.currentResource);
|
|
117
338
|
|
|
118
|
-
|
|
119
|
-
|
|
339
|
+
await entersState(this.audioPlayer, AudioPlayerStatus.Playing, 5_000);
|
|
340
|
+
return true;
|
|
341
|
+
} else if (streamInfo && !(streamInfo as any).stream) {
|
|
342
|
+
// Extension đang xử lý phát nhạc (như Lavalink) - chỉ đánh dấu đang phát
|
|
343
|
+
this.debug(`[Player] Extension is handling playback for track: ${track.title}`);
|
|
344
|
+
this.isPlaying = true;
|
|
345
|
+
this.isPaused = false;
|
|
346
|
+
this.emit("trackStart", track);
|
|
347
|
+
return true;
|
|
348
|
+
} else {
|
|
349
|
+
throw new Error(`No stream available for track: ${track.title}`);
|
|
350
|
+
}
|
|
120
351
|
} catch (error) {
|
|
121
352
|
this.debug(`[Player] startTrack error:`, error);
|
|
122
353
|
this.emit("playerError", error as Error, track);
|
|
@@ -136,11 +367,6 @@ export class Player extends EventEmitter {
|
|
|
136
367
|
}
|
|
137
368
|
}
|
|
138
369
|
|
|
139
|
-
private withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
|
|
140
|
-
const timeout = this.options.extractorTimeout ?? 15000;
|
|
141
|
-
return Promise.race([promise, new Promise<never>((_, reject) => setTimeout(() => reject(new Error(message)), timeout))]);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
370
|
private debug(message?: any, ...optionalParams: any[]): void {
|
|
145
371
|
if (this.listenerCount("debug") > 0) {
|
|
146
372
|
this.emit("debug", message, ...optionalParams);
|
|
@@ -184,6 +410,7 @@ export class Player extends EventEmitter {
|
|
|
184
410
|
this.volume = this.options.volume || 100;
|
|
185
411
|
this.userdata = this.options.userdata;
|
|
186
412
|
this.setupEventListeners();
|
|
413
|
+
this.extensionContext = Object.freeze({ player: this, manager });
|
|
187
414
|
|
|
188
415
|
// Optionally pre-create the TTS AudioPlayer
|
|
189
416
|
if (this.options?.tts?.createPlayer) {
|
|
@@ -272,6 +499,14 @@ export class Player extends EventEmitter {
|
|
|
272
499
|
return this.pluginManager.unregister(name);
|
|
273
500
|
}
|
|
274
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Connect to a voice channel
|
|
504
|
+
*
|
|
505
|
+
* @param {VoiceChannel} channel - Discord voice channel
|
|
506
|
+
* @returns {Promise<VoiceConnection>} The voice connection
|
|
507
|
+
* @example
|
|
508
|
+
* await player.connect(voiceChannel);
|
|
509
|
+
*/
|
|
275
510
|
async connect(channel: VoiceChannel): Promise<VoiceConnection> {
|
|
276
511
|
try {
|
|
277
512
|
this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
|
|
@@ -307,15 +542,34 @@ export class Player extends EventEmitter {
|
|
|
307
542
|
}
|
|
308
543
|
}
|
|
309
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Search for tracks using the player's extensions and plugins
|
|
547
|
+
*
|
|
548
|
+
* @param {string} query - The query to search for
|
|
549
|
+
* @param {string} requestedBy - The user ID who requested the search
|
|
550
|
+
* @returns {Promise<SearchResult>} The search result
|
|
551
|
+
* @example
|
|
552
|
+
* const result = await player.search("Never Gonna Give You Up", userId);
|
|
553
|
+
* console.log(`Search result: ${result.tracks.length} tracks`);
|
|
554
|
+
*/
|
|
310
555
|
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
311
556
|
this.debug(`[Player] Search called with query: ${query}, requestedBy: ${requestedBy}`);
|
|
557
|
+
const extensionResult = await this.extensionsProvideSearch(query, requestedBy);
|
|
558
|
+
if (extensionResult && Array.isArray(extensionResult.tracks) && extensionResult.tracks.length > 0) {
|
|
559
|
+
this.debug(`[Player] Extension handled search for query: ${query}`);
|
|
560
|
+
return extensionResult;
|
|
561
|
+
}
|
|
312
562
|
const plugins = this.pluginManager.getAll();
|
|
313
563
|
let lastError: any = null;
|
|
314
564
|
|
|
315
565
|
for (const p of plugins) {
|
|
316
566
|
try {
|
|
317
567
|
this.debug(`[Player] Trying plugin for search: ${p.name}`);
|
|
318
|
-
const res = await
|
|
568
|
+
const res = await withTimeout(
|
|
569
|
+
p.search(query, requestedBy),
|
|
570
|
+
this.options.extractorTimeout ?? 15000,
|
|
571
|
+
`Search operation timed out for ${p.name}`,
|
|
572
|
+
);
|
|
319
573
|
if (res && Array.isArray(res.tracks) && res.tracks.length > 0) {
|
|
320
574
|
this.debug(`[Player] Plugin '${p.name}' returned ${res.tracks.length} tracks`);
|
|
321
575
|
return res;
|
|
@@ -333,23 +587,63 @@ export class Player extends EventEmitter {
|
|
|
333
587
|
throw new Error(`No plugin found to handle: ${query}`);
|
|
334
588
|
}
|
|
335
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Play a track or search query
|
|
592
|
+
*
|
|
593
|
+
* @param {string | Track} query - Track URL, search query, or Track object
|
|
594
|
+
* @param {string} requestedBy - User ID who requested the track
|
|
595
|
+
* @returns {Promise<boolean>} True if playback started successfully
|
|
596
|
+
* @example
|
|
597
|
+
* await player.play("Never Gonna Give You Up", userId);
|
|
598
|
+
* await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId);
|
|
599
|
+
* await player.play("tts: Hello everyone!", userId);
|
|
600
|
+
*/
|
|
336
601
|
async play(query: string | Track, requestedBy?: string): Promise<boolean> {
|
|
602
|
+
this.debug(`[Player] Play called with query: ${typeof query === "string" ? query : query?.title}`);
|
|
603
|
+
this.clearLeaveTimeout();
|
|
604
|
+
let tracksToAdd: Track[] = [];
|
|
605
|
+
let isPlaylist = false;
|
|
606
|
+
let effectiveRequest: ExtensionPlayRequest = { query, requestedBy };
|
|
607
|
+
let hookResponse: ExtensionPlayResponse = {};
|
|
608
|
+
|
|
337
609
|
try {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const searchResult = await this.search(query, requestedBy || "Unknown");
|
|
345
|
-
tracksToAdd = searchResult.tracks;
|
|
610
|
+
const hookOutcome = await this.runBeforePlayHooks(effectiveRequest);
|
|
611
|
+
effectiveRequest = hookOutcome.request;
|
|
612
|
+
hookResponse = hookOutcome.response;
|
|
613
|
+
if (effectiveRequest.requestedBy === undefined) {
|
|
614
|
+
effectiveRequest.requestedBy = requestedBy;
|
|
615
|
+
}
|
|
346
616
|
|
|
617
|
+
const hookTracks = Array.isArray(hookResponse.tracks) ? hookResponse.tracks : undefined;
|
|
618
|
+
|
|
619
|
+
if (hookResponse.handled && (!hookTracks || hookTracks.length === 0)) {
|
|
620
|
+
const handledPayload: ExtensionAfterPlayPayload = {
|
|
621
|
+
success: hookResponse.success ?? true,
|
|
622
|
+
query: effectiveRequest.query,
|
|
623
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
624
|
+
tracks: [],
|
|
625
|
+
isPlaylist: hookResponse.isPlaylist ?? false,
|
|
626
|
+
error: hookResponse.error,
|
|
627
|
+
};
|
|
628
|
+
await this.runAfterPlayHooks(handledPayload);
|
|
629
|
+
if (hookResponse.error) {
|
|
630
|
+
this.emit("playerError", hookResponse.error);
|
|
631
|
+
}
|
|
632
|
+
return hookResponse.success ?? true;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (hookTracks && hookTracks.length > 0) {
|
|
636
|
+
tracksToAdd = hookTracks;
|
|
637
|
+
isPlaylist = hookResponse.isPlaylist ?? hookTracks.length > 1;
|
|
638
|
+
} else if (typeof effectiveRequest.query === "string") {
|
|
639
|
+
const searchResult = await this.search(effectiveRequest.query, effectiveRequest.requestedBy || "Unknown");
|
|
640
|
+
tracksToAdd = searchResult.tracks;
|
|
347
641
|
if (searchResult.playlist) {
|
|
348
642
|
isPlaylist = true;
|
|
349
643
|
this.debug(`[Player] Added playlist: ${searchResult.playlist.name} (${tracksToAdd.length} tracks)`);
|
|
350
644
|
}
|
|
351
|
-
} else {
|
|
352
|
-
tracksToAdd = [query];
|
|
645
|
+
} else if (effectiveRequest.query) {
|
|
646
|
+
tracksToAdd = [effectiveRequest.query as Track];
|
|
353
647
|
}
|
|
354
648
|
|
|
355
649
|
if (tracksToAdd.length === 0) {
|
|
@@ -357,7 +651,6 @@ export class Player extends EventEmitter {
|
|
|
357
651
|
throw new Error("No tracks found");
|
|
358
652
|
}
|
|
359
653
|
|
|
360
|
-
// If a TTS track is requested and interrupt mode is enabled, handle it separately
|
|
361
654
|
const isTTS = (t: Track | undefined) => {
|
|
362
655
|
if (!t) return false;
|
|
363
656
|
try {
|
|
@@ -367,7 +660,8 @@ export class Player extends EventEmitter {
|
|
|
367
660
|
}
|
|
368
661
|
};
|
|
369
662
|
|
|
370
|
-
const queryLooksTTS =
|
|
663
|
+
const queryLooksTTS =
|
|
664
|
+
typeof effectiveRequest.query === "string" && effectiveRequest.query.trim().toLowerCase().startsWith("tts");
|
|
371
665
|
|
|
372
666
|
if (
|
|
373
667
|
!isPlaylist &&
|
|
@@ -375,9 +669,15 @@ export class Player extends EventEmitter {
|
|
|
375
669
|
this.options?.tts?.interrupt !== false &&
|
|
376
670
|
(isTTS(tracksToAdd[0]) || queryLooksTTS)
|
|
377
671
|
) {
|
|
378
|
-
// Interrupt music playback with TTS (do not modify the music queue)
|
|
379
672
|
this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`);
|
|
380
673
|
await this.interruptWithTTSTrack(tracksToAdd[0]);
|
|
674
|
+
await this.runAfterPlayHooks({
|
|
675
|
+
success: true,
|
|
676
|
+
query: effectiveRequest.query,
|
|
677
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
678
|
+
tracks: tracksToAdd,
|
|
679
|
+
isPlaylist,
|
|
680
|
+
});
|
|
381
681
|
return true;
|
|
382
682
|
}
|
|
383
683
|
|
|
@@ -385,17 +685,30 @@ export class Player extends EventEmitter {
|
|
|
385
685
|
this.queue.addMultiple(tracksToAdd);
|
|
386
686
|
this.emit("queueAddList", tracksToAdd);
|
|
387
687
|
} else {
|
|
388
|
-
this.queue.add(tracksToAdd
|
|
389
|
-
this.emit("queueAdd", tracksToAdd
|
|
688
|
+
this.queue.add(tracksToAdd[0]);
|
|
689
|
+
this.emit("queueAdd", tracksToAdd[0]);
|
|
390
690
|
}
|
|
391
691
|
|
|
392
|
-
|
|
393
|
-
if (!this.isPlaying) {
|
|
394
|
-
return this.playNext();
|
|
395
|
-
}
|
|
692
|
+
const started = !this.isPlaying ? await this.playNext() : true;
|
|
396
693
|
|
|
397
|
-
|
|
694
|
+
await this.runAfterPlayHooks({
|
|
695
|
+
success: started,
|
|
696
|
+
query: effectiveRequest.query,
|
|
697
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
698
|
+
tracks: tracksToAdd,
|
|
699
|
+
isPlaylist,
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
return started;
|
|
398
703
|
} catch (error) {
|
|
704
|
+
await this.runAfterPlayHooks({
|
|
705
|
+
success: false,
|
|
706
|
+
query: effectiveRequest.query,
|
|
707
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
708
|
+
tracks: tracksToAdd,
|
|
709
|
+
isPlaylist,
|
|
710
|
+
error: error as Error,
|
|
711
|
+
});
|
|
399
712
|
this.debug(`[Player] Play error:`, error);
|
|
400
713
|
this.emit("playerError", error as Error);
|
|
401
714
|
return false;
|
|
@@ -405,6 +718,11 @@ export class Player extends EventEmitter {
|
|
|
405
718
|
/**
|
|
406
719
|
* Interrupt current music with a TTS track. Pauses music, swaps the
|
|
407
720
|
* subscription to a dedicated TTS player, plays TTS, then resumes.
|
|
721
|
+
*
|
|
722
|
+
* @param {Track} track - The track to interrupt with
|
|
723
|
+
* @returns {Promise<void>}
|
|
724
|
+
* @example
|
|
725
|
+
* await player.interruptWithTTSTrack(track);
|
|
408
726
|
*/
|
|
409
727
|
public async interruptWithTTSTrack(track: Track): Promise<void> {
|
|
410
728
|
this.ttsQueue.push(track);
|
|
@@ -413,7 +731,13 @@ export class Player extends EventEmitter {
|
|
|
413
731
|
}
|
|
414
732
|
}
|
|
415
733
|
|
|
416
|
-
/**
|
|
734
|
+
/**
|
|
735
|
+
* Play queued TTS items sequentially
|
|
736
|
+
*
|
|
737
|
+
* @returns {Promise<void>}
|
|
738
|
+
* @example
|
|
739
|
+
* await player.playNextTTS();
|
|
740
|
+
*/
|
|
417
741
|
private async playNextTTS(): Promise<void> {
|
|
418
742
|
const next = this.ttsQueue.shift();
|
|
419
743
|
if (!next) return;
|
|
@@ -488,15 +812,16 @@ export class Player extends EventEmitter {
|
|
|
488
812
|
|
|
489
813
|
let streamInfo: any;
|
|
490
814
|
try {
|
|
491
|
-
streamInfo = await
|
|
815
|
+
streamInfo = await withTimeout(plugin.getStream(track), this.options.extractorTimeout ?? 15000, "getStream timed out");
|
|
492
816
|
} catch (streamError) {
|
|
493
817
|
// try fallbacks
|
|
494
818
|
const allplugs = this.pluginManager.getAll();
|
|
495
819
|
for (const p of allplugs) {
|
|
496
820
|
if (typeof (p as any).getFallback !== "function") continue;
|
|
497
821
|
try {
|
|
498
|
-
streamInfo = await
|
|
822
|
+
streamInfo = await withTimeout(
|
|
499
823
|
(p as any).getFallback(track),
|
|
824
|
+
this.options.extractorTimeout ?? 15000,
|
|
500
825
|
`getFallback timed out for plugin ${(p as any).name}`,
|
|
501
826
|
);
|
|
502
827
|
if (!streamInfo?.stream) continue;
|
|
@@ -544,11 +869,12 @@ export class Player extends EventEmitter {
|
|
|
544
869
|
for (const p of candidates) {
|
|
545
870
|
try {
|
|
546
871
|
this.debug(`[Player] Trying related from plugin: ${p.name}`);
|
|
547
|
-
const related = await
|
|
872
|
+
const related = await withTimeout(
|
|
548
873
|
(p as any).getRelatedTracks(lastTrack.url, {
|
|
549
874
|
limit: 10,
|
|
550
875
|
history: this.queue.previousTracks,
|
|
551
876
|
}),
|
|
877
|
+
this.options.extractorTimeout ?? 15000,
|
|
552
878
|
`getRelatedTracks timed out for ${p.name}`,
|
|
553
879
|
);
|
|
554
880
|
|
|
@@ -606,6 +932,14 @@ export class Player extends EventEmitter {
|
|
|
606
932
|
}
|
|
607
933
|
}
|
|
608
934
|
|
|
935
|
+
/**
|
|
936
|
+
* Pause the current track
|
|
937
|
+
*
|
|
938
|
+
* @returns {boolean} True if paused successfully
|
|
939
|
+
* @example
|
|
940
|
+
* const paused = player.pause();
|
|
941
|
+
* console.log(`Paused: ${paused}`);
|
|
942
|
+
*/
|
|
609
943
|
pause(): boolean {
|
|
610
944
|
this.debug(`[Player] pause called`);
|
|
611
945
|
if (this.isPlaying && !this.isPaused) {
|
|
@@ -614,6 +948,14 @@ export class Player extends EventEmitter {
|
|
|
614
948
|
return false;
|
|
615
949
|
}
|
|
616
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Resume the current track
|
|
953
|
+
*
|
|
954
|
+
* @returns {boolean} True if resumed successfully
|
|
955
|
+
* @example
|
|
956
|
+
* const resumed = player.resume();
|
|
957
|
+
* console.log(`Resumed: ${resumed}`);
|
|
958
|
+
*/
|
|
617
959
|
resume(): boolean {
|
|
618
960
|
this.debug(`[Player] resume called`);
|
|
619
961
|
if (this.isPaused) {
|
|
@@ -630,6 +972,14 @@ export class Player extends EventEmitter {
|
|
|
630
972
|
return false;
|
|
631
973
|
}
|
|
632
974
|
|
|
975
|
+
/**
|
|
976
|
+
* Stop the current track
|
|
977
|
+
*
|
|
978
|
+
* @returns {boolean} True if stopped successfully
|
|
979
|
+
* @example
|
|
980
|
+
* const stopped = player.stop();
|
|
981
|
+
* console.log(`Stopped: ${stopped}`);
|
|
982
|
+
*/
|
|
633
983
|
stop(): boolean {
|
|
634
984
|
this.debug(`[Player] stop called`);
|
|
635
985
|
this.queue.clear();
|
|
@@ -640,6 +990,15 @@ export class Player extends EventEmitter {
|
|
|
640
990
|
return result;
|
|
641
991
|
}
|
|
642
992
|
|
|
993
|
+
/**
|
|
994
|
+
* Skip to the next track
|
|
995
|
+
*
|
|
996
|
+
* @returns {boolean} True if skipped successfully
|
|
997
|
+
* @example
|
|
998
|
+
* const skipped = player.skip();
|
|
999
|
+
* console.log(`Skipped: ${skipped}`);
|
|
1000
|
+
*/
|
|
1001
|
+
|
|
643
1002
|
skip(): boolean {
|
|
644
1003
|
this.debug(`[Player] skip called`);
|
|
645
1004
|
if (this.isPlaying || this.isPaused) {
|
|
@@ -651,6 +1010,11 @@ export class Player extends EventEmitter {
|
|
|
651
1010
|
|
|
652
1011
|
/**
|
|
653
1012
|
* Go back to the previous track in history and play it.
|
|
1013
|
+
*
|
|
1014
|
+
* @returns {Promise<boolean>} True if previous track was played successfully
|
|
1015
|
+
* @example
|
|
1016
|
+
* const previous = await player.previous();
|
|
1017
|
+
* console.log(`Previous: ${previous}`);
|
|
654
1018
|
*/
|
|
655
1019
|
async previous(): Promise<boolean> {
|
|
656
1020
|
this.debug(`[Player] previous called`);
|
|
@@ -661,14 +1025,41 @@ export class Player extends EventEmitter {
|
|
|
661
1025
|
return this.startTrack(track);
|
|
662
1026
|
}
|
|
663
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* Loop the current track
|
|
1030
|
+
*
|
|
1031
|
+
* @param {LoopMode} mode - The loop mode to set
|
|
1032
|
+
* @returns {LoopMode} The loop mode
|
|
1033
|
+
* @example
|
|
1034
|
+
* const loopMode = player.loop("track");
|
|
1035
|
+
* console.log(`Loop mode: ${loopMode}`);
|
|
1036
|
+
*/
|
|
664
1037
|
loop(mode?: LoopMode): LoopMode {
|
|
665
1038
|
return this.queue.loop(mode);
|
|
666
1039
|
}
|
|
667
1040
|
|
|
1041
|
+
/**
|
|
1042
|
+
* Set the auto-play mode
|
|
1043
|
+
*
|
|
1044
|
+
* @param {boolean} mode - The auto-play mode to set
|
|
1045
|
+
* @returns {boolean} The auto-play mode
|
|
1046
|
+
* @example
|
|
1047
|
+
* const autoPlayMode = player.autoPlay(true);
|
|
1048
|
+
* console.log(`Auto-play mode: ${autoPlayMode}`);
|
|
1049
|
+
*/
|
|
668
1050
|
autoPlay(mode?: boolean): boolean {
|
|
669
1051
|
return this.queue.autoPlay(mode);
|
|
670
1052
|
}
|
|
671
1053
|
|
|
1054
|
+
/**
|
|
1055
|
+
* Set the volume of the current track
|
|
1056
|
+
*
|
|
1057
|
+
* @param {number} volume - The volume to set
|
|
1058
|
+
* @returns {boolean} True if volume was set successfully
|
|
1059
|
+
* @example
|
|
1060
|
+
* const volumeSet = player.setVolume(50);
|
|
1061
|
+
* console.log(`Volume set: ${volumeSet}`);
|
|
1062
|
+
*/
|
|
672
1063
|
setVolume(volume: number): boolean {
|
|
673
1064
|
this.debug(`[Player] setVolume called: ${volume}`);
|
|
674
1065
|
if (volume < 0 || volume > 200) return false;
|
|
@@ -700,11 +1091,25 @@ export class Player extends EventEmitter {
|
|
|
700
1091
|
return true;
|
|
701
1092
|
}
|
|
702
1093
|
|
|
1094
|
+
/**
|
|
1095
|
+
* Shuffle the queue
|
|
1096
|
+
*
|
|
1097
|
+
* @returns {void}
|
|
1098
|
+
* @example
|
|
1099
|
+
* player.shuffle();
|
|
1100
|
+
*/
|
|
703
1101
|
shuffle(): void {
|
|
704
1102
|
this.debug(`[Player] shuffle called`);
|
|
705
1103
|
this.queue.shuffle();
|
|
706
1104
|
}
|
|
707
1105
|
|
|
1106
|
+
/**
|
|
1107
|
+
* Clear the queue
|
|
1108
|
+
*
|
|
1109
|
+
* @returns {void}
|
|
1110
|
+
* @example
|
|
1111
|
+
* player.clearQueue();
|
|
1112
|
+
*/
|
|
708
1113
|
clearQueue(): void {
|
|
709
1114
|
this.debug(`[Player] clearQueue called`);
|
|
710
1115
|
this.queue.clear();
|
|
@@ -715,6 +1120,14 @@ export class Player extends EventEmitter {
|
|
|
715
1120
|
* - If `query` is a string, performs a search and inserts resulting tracks (playlist supported).
|
|
716
1121
|
* - If a Track or Track[] is provided, inserts directly.
|
|
717
1122
|
* Does not auto-start playback; it only modifies the queue.
|
|
1123
|
+
*
|
|
1124
|
+
* @param {string | Track | Track[]} query - The track or tracks to insert
|
|
1125
|
+
* @param {number} index - The index to insert the tracks at
|
|
1126
|
+
* @param {string} requestedBy - The user ID who requested the insert
|
|
1127
|
+
* @returns {Promise<boolean>} True if the tracks were inserted successfully
|
|
1128
|
+
* @example
|
|
1129
|
+
* const inserted = await player.insert("Song Name", 0, userId);
|
|
1130
|
+
* console.log(`Inserted: ${inserted}`);
|
|
718
1131
|
*/
|
|
719
1132
|
async insert(query: string | Track | Track[], index: number, requestedBy?: string): Promise<boolean> {
|
|
720
1133
|
try {
|
|
@@ -756,6 +1169,15 @@ export class Player extends EventEmitter {
|
|
|
756
1169
|
}
|
|
757
1170
|
}
|
|
758
1171
|
|
|
1172
|
+
/**
|
|
1173
|
+
* Remove a track from the queue
|
|
1174
|
+
*
|
|
1175
|
+
* @param {number} index - The index of the track to remove
|
|
1176
|
+
* @returns {Track | null} The removed track or null
|
|
1177
|
+
* @example
|
|
1178
|
+
* const removed = player.remove(0);
|
|
1179
|
+
* console.log(`Removed: ${removed?.title}`);
|
|
1180
|
+
*/
|
|
759
1181
|
remove(index: number): Track | null {
|
|
760
1182
|
this.debug(`[Player] remove called for index: ${index}`);
|
|
761
1183
|
const track = this.queue.remove(index);
|
|
@@ -765,6 +1187,15 @@ export class Player extends EventEmitter {
|
|
|
765
1187
|
return track;
|
|
766
1188
|
}
|
|
767
1189
|
|
|
1190
|
+
/**
|
|
1191
|
+
* Get the progress bar of the current track
|
|
1192
|
+
*
|
|
1193
|
+
* @param {ProgressBarOptions} options - The options for the progress bar
|
|
1194
|
+
* @returns {string} The progress bar
|
|
1195
|
+
* @example
|
|
1196
|
+
* const progressBar = player.getProgressBar();
|
|
1197
|
+
* console.log(`Progress bar: ${progressBar}`);
|
|
1198
|
+
*/
|
|
768
1199
|
getProgressBar(options: ProgressBarOptions = {}): string {
|
|
769
1200
|
const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
|
|
770
1201
|
const track = this.queue.currentTrack;
|
|
@@ -782,6 +1213,42 @@ export class Player extends EventEmitter {
|
|
|
782
1213
|
return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`;
|
|
783
1214
|
}
|
|
784
1215
|
|
|
1216
|
+
/**
|
|
1217
|
+
* Get the time of the current track
|
|
1218
|
+
*
|
|
1219
|
+
* @returns {Object} The time of the current track
|
|
1220
|
+
* @example
|
|
1221
|
+
* const time = player.getTime();
|
|
1222
|
+
* console.log(`Time: ${time.current}`);
|
|
1223
|
+
*/
|
|
1224
|
+
getTime() {
|
|
1225
|
+
const resource = this.currentResource;
|
|
1226
|
+
const track = this.queue.currentTrack;
|
|
1227
|
+
if (!track || !resource)
|
|
1228
|
+
return {
|
|
1229
|
+
current: 0,
|
|
1230
|
+
total: 0,
|
|
1231
|
+
format: "00:00",
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
1235
|
+
|
|
1236
|
+
return {
|
|
1237
|
+
current: resource?.playbackDuration,
|
|
1238
|
+
total: total,
|
|
1239
|
+
format: this.formatTime(resource.playbackDuration),
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Format the time in the format of HH:MM:SS
|
|
1245
|
+
*
|
|
1246
|
+
* @param {number} ms - The time in milliseconds
|
|
1247
|
+
* @returns {string} The formatted time
|
|
1248
|
+
* @example
|
|
1249
|
+
* const formattedTime = player.formatTime(1000);
|
|
1250
|
+
* console.log(`Formatted time: ${formattedTime}`);
|
|
1251
|
+
*/
|
|
785
1252
|
formatTime(ms: number): string {
|
|
786
1253
|
const totalSeconds = Math.floor(ms / 1000);
|
|
787
1254
|
const hours = Math.floor(totalSeconds / 3600);
|
|
@@ -808,6 +1275,13 @@ export class Player extends EventEmitter {
|
|
|
808
1275
|
}
|
|
809
1276
|
}
|
|
810
1277
|
|
|
1278
|
+
/**
|
|
1279
|
+
* Destroy the player
|
|
1280
|
+
*
|
|
1281
|
+
* @returns {void}
|
|
1282
|
+
* @example
|
|
1283
|
+
* player.destroy();
|
|
1284
|
+
*/
|
|
811
1285
|
destroy(): void {
|
|
812
1286
|
this.debug(`[Player] destroy called`);
|
|
813
1287
|
if (this.leaveTimeout) {
|
|
@@ -831,36 +1305,99 @@ export class Player extends EventEmitter {
|
|
|
831
1305
|
|
|
832
1306
|
this.queue.clear();
|
|
833
1307
|
this.pluginManager.clear();
|
|
1308
|
+
for (const extension of [...this.extensions]) {
|
|
1309
|
+
this.invokeExtensionLifecycle(extension, "onDestroy");
|
|
1310
|
+
if (extension.player === this) {
|
|
1311
|
+
extension.player = null;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
this.extensions = [];
|
|
834
1315
|
this.isPlaying = false;
|
|
835
1316
|
this.isPaused = false;
|
|
836
1317
|
this.emit("playerDestroy");
|
|
837
1318
|
this.removeAllListeners();
|
|
838
1319
|
}
|
|
839
1320
|
|
|
840
|
-
|
|
1321
|
+
/**
|
|
1322
|
+
* Get the size of the queue
|
|
1323
|
+
*
|
|
1324
|
+
* @returns {number} The size of the queue
|
|
1325
|
+
* @example
|
|
1326
|
+
* const queueSize = player.queueSize;
|
|
1327
|
+
* console.log(`Queue size: ${queueSize}`);
|
|
1328
|
+
*/
|
|
841
1329
|
get queueSize(): number {
|
|
842
1330
|
return this.queue.size;
|
|
843
1331
|
}
|
|
844
1332
|
|
|
1333
|
+
/**
|
|
1334
|
+
* Get the current track
|
|
1335
|
+
*
|
|
1336
|
+
* @returns {Track | null} The current track or null
|
|
1337
|
+
* @example
|
|
1338
|
+
* const currentTrack = player.currentTrack;
|
|
1339
|
+
* console.log(`Current track: ${currentTrack?.title}`);
|
|
1340
|
+
*/
|
|
845
1341
|
get currentTrack(): Track | null {
|
|
846
1342
|
return this.queue.currentTrack;
|
|
847
1343
|
}
|
|
848
1344
|
|
|
1345
|
+
/**
|
|
1346
|
+
* Get the previous track
|
|
1347
|
+
*
|
|
1348
|
+
* @returns {Track | null} The previous track or null
|
|
1349
|
+
* @example
|
|
1350
|
+
* const previousTrack = player.previousTrack;
|
|
1351
|
+
* console.log(`Previous track: ${previousTrack?.title}`);
|
|
1352
|
+
*/
|
|
849
1353
|
get previousTrack(): Track | null {
|
|
850
1354
|
return this.queue.previousTracks?.at(-1) ?? null;
|
|
851
1355
|
}
|
|
852
1356
|
|
|
1357
|
+
/**
|
|
1358
|
+
* Get the upcoming tracks
|
|
1359
|
+
*
|
|
1360
|
+
* @returns {Track[]} The upcoming tracks
|
|
1361
|
+
* @example
|
|
1362
|
+
* const upcomingTracks = player.upcomingTracks;
|
|
1363
|
+
* console.log(`Upcoming tracks: ${upcomingTracks.length}`);
|
|
1364
|
+
*/
|
|
853
1365
|
get upcomingTracks(): Track[] {
|
|
854
1366
|
return this.queue.getTracks();
|
|
855
1367
|
}
|
|
856
1368
|
|
|
1369
|
+
/**
|
|
1370
|
+
* Get the previous tracks
|
|
1371
|
+
*
|
|
1372
|
+
* @returns {Track[]} The previous tracks
|
|
1373
|
+
* @example
|
|
1374
|
+
* const previousTracks = player.previousTracks;
|
|
1375
|
+
* console.log(`Previous tracks: ${previousTracks.length}`);
|
|
1376
|
+
*/
|
|
857
1377
|
get previousTracks(): Track[] {
|
|
858
1378
|
return this.queue.previousTracks;
|
|
859
1379
|
}
|
|
860
1380
|
|
|
1381
|
+
/**
|
|
1382
|
+
* Get the available plugins
|
|
1383
|
+
*
|
|
1384
|
+
* @returns {string[]} The available plugins
|
|
1385
|
+
* @example
|
|
1386
|
+
* const availablePlugins = player.availablePlugins;
|
|
1387
|
+
* console.log(`Available plugins: ${availablePlugins.length}`);
|
|
1388
|
+
*/
|
|
861
1389
|
get availablePlugins(): string[] {
|
|
862
1390
|
return this.pluginManager.getAll().map((p) => p.name);
|
|
863
1391
|
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Get the related tracks
|
|
1395
|
+
*
|
|
1396
|
+
* @returns {Track[] | null} The related tracks or null
|
|
1397
|
+
* @example
|
|
1398
|
+
* const relatedTracks = player.relatedTracks;
|
|
1399
|
+
* console.log(`Related tracks: ${relatedTracks?.length}`);
|
|
1400
|
+
*/
|
|
864
1401
|
get relatedTracks(): Track[] | null {
|
|
865
1402
|
return this.queue.relatedTracks();
|
|
866
1403
|
}
|