ziplayer 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -30
- package/dist/extensions/BaseExtension.d.ts +9 -3
- package/dist/extensions/BaseExtension.d.ts.map +1 -1
- package/dist/extensions/BaseExtension.js.map +1 -1
- package/dist/structures/Player.d.ts +294 -2
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +586 -88
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +166 -2
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +182 -8
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +193 -2
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +193 -2
- package/dist/structures/Queue.js.map +1 -1
- package/dist/types/index.d.ts +327 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/timeout.d.ts +9 -0
- package/dist/utils/timeout.d.ts.map +1 -0
- package/dist/utils/timeout.js +14 -0
- package/dist/utils/timeout.js.map +1 -0
- package/package.json +1 -1
- package/src/extensions/BaseExtension.ts +35 -10
- package/src/structures/Player.ts +615 -90
- package/src/structures/PlayerManager.ts +189 -8
- package/src/structures/Queue.ts +196 -2
- package/src/types/index.ts +343 -4
- package/src/utils/timeout.ts +10 -0
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);
|
|
55
194
|
}
|
|
195
|
+
}
|
|
196
|
+
return { request, response };
|
|
197
|
+
}
|
|
56
198
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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;
|
|
61
209
|
try {
|
|
62
|
-
|
|
63
|
-
} catch (
|
|
64
|
-
this.debug(`[Player]
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
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;
|
|
240
|
+
try {
|
|
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
|
-
|
|
345
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
346
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;
|
|
@@ -448,8 +772,15 @@ export class Player extends EventEmitter {
|
|
|
448
772
|
// Derive timeout from resource/track duration when available, with a sensible cap
|
|
449
773
|
const md: any = (resource as any)?.metadata ?? {};
|
|
450
774
|
const declared =
|
|
451
|
-
typeof md.duration === "number" ? md.duration
|
|
452
|
-
|
|
775
|
+
typeof md.duration === "number" ? md.duration
|
|
776
|
+
: typeof next?.duration === "number" ? next.duration
|
|
777
|
+
: undefined;
|
|
778
|
+
const declaredMs =
|
|
779
|
+
declared ?
|
|
780
|
+
declared > 1000 ?
|
|
781
|
+
declared
|
|
782
|
+
: declared * 1000
|
|
783
|
+
: undefined;
|
|
453
784
|
const cap = this.options?.tts?.Max_Time_TTS ?? 60_000;
|
|
454
785
|
const idleTimeout = declaredMs ? Math.min(cap, Math.max(1_000, declaredMs + 1_500)) : cap;
|
|
455
786
|
await entersState(ttsPlayer, AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
|
|
@@ -481,15 +812,16 @@ export class Player extends EventEmitter {
|
|
|
481
812
|
|
|
482
813
|
let streamInfo: any;
|
|
483
814
|
try {
|
|
484
|
-
streamInfo = await
|
|
815
|
+
streamInfo = await withTimeout(plugin.getStream(track), this.options.extractorTimeout ?? 15000, "getStream timed out");
|
|
485
816
|
} catch (streamError) {
|
|
486
817
|
// try fallbacks
|
|
487
818
|
const allplugs = this.pluginManager.getAll();
|
|
488
819
|
for (const p of allplugs) {
|
|
489
820
|
if (typeof (p as any).getFallback !== "function") continue;
|
|
490
821
|
try {
|
|
491
|
-
streamInfo = await
|
|
822
|
+
streamInfo = await withTimeout(
|
|
492
823
|
(p as any).getFallback(track),
|
|
824
|
+
this.options.extractorTimeout ?? 15000,
|
|
493
825
|
`getFallback timed out for plugin ${(p as any).name}`,
|
|
494
826
|
);
|
|
495
827
|
if (!streamInfo?.stream) continue;
|
|
@@ -537,11 +869,12 @@ export class Player extends EventEmitter {
|
|
|
537
869
|
for (const p of candidates) {
|
|
538
870
|
try {
|
|
539
871
|
this.debug(`[Player] Trying related from plugin: ${p.name}`);
|
|
540
|
-
const related = await
|
|
872
|
+
const related = await withTimeout(
|
|
541
873
|
(p as any).getRelatedTracks(lastTrack.url, {
|
|
542
874
|
limit: 10,
|
|
543
875
|
history: this.queue.previousTracks,
|
|
544
876
|
}),
|
|
877
|
+
this.options.extractorTimeout ?? 15000,
|
|
545
878
|
`getRelatedTracks timed out for ${p.name}`,
|
|
546
879
|
);
|
|
547
880
|
|
|
@@ -599,6 +932,14 @@ export class Player extends EventEmitter {
|
|
|
599
932
|
}
|
|
600
933
|
}
|
|
601
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
|
+
*/
|
|
602
943
|
pause(): boolean {
|
|
603
944
|
this.debug(`[Player] pause called`);
|
|
604
945
|
if (this.isPlaying && !this.isPaused) {
|
|
@@ -607,6 +948,14 @@ export class Player extends EventEmitter {
|
|
|
607
948
|
return false;
|
|
608
949
|
}
|
|
609
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
|
+
*/
|
|
610
959
|
resume(): boolean {
|
|
611
960
|
this.debug(`[Player] resume called`);
|
|
612
961
|
if (this.isPaused) {
|
|
@@ -623,6 +972,14 @@ export class Player extends EventEmitter {
|
|
|
623
972
|
return false;
|
|
624
973
|
}
|
|
625
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
|
+
*/
|
|
626
983
|
stop(): boolean {
|
|
627
984
|
this.debug(`[Player] stop called`);
|
|
628
985
|
this.queue.clear();
|
|
@@ -633,6 +990,15 @@ export class Player extends EventEmitter {
|
|
|
633
990
|
return result;
|
|
634
991
|
}
|
|
635
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
|
+
|
|
636
1002
|
skip(): boolean {
|
|
637
1003
|
this.debug(`[Player] skip called`);
|
|
638
1004
|
if (this.isPlaying || this.isPaused) {
|
|
@@ -644,6 +1010,11 @@ export class Player extends EventEmitter {
|
|
|
644
1010
|
|
|
645
1011
|
/**
|
|
646
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}`);
|
|
647
1018
|
*/
|
|
648
1019
|
async previous(): Promise<boolean> {
|
|
649
1020
|
this.debug(`[Player] previous called`);
|
|
@@ -654,14 +1025,41 @@ export class Player extends EventEmitter {
|
|
|
654
1025
|
return this.startTrack(track);
|
|
655
1026
|
}
|
|
656
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
|
+
*/
|
|
657
1037
|
loop(mode?: LoopMode): LoopMode {
|
|
658
1038
|
return this.queue.loop(mode);
|
|
659
1039
|
}
|
|
660
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
|
+
*/
|
|
661
1050
|
autoPlay(mode?: boolean): boolean {
|
|
662
1051
|
return this.queue.autoPlay(mode);
|
|
663
1052
|
}
|
|
664
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
|
+
*/
|
|
665
1063
|
setVolume(volume: number): boolean {
|
|
666
1064
|
this.debug(`[Player] setVolume called: ${volume}`);
|
|
667
1065
|
if (volume < 0 || volume > 200) return false;
|
|
@@ -693,11 +1091,25 @@ export class Player extends EventEmitter {
|
|
|
693
1091
|
return true;
|
|
694
1092
|
}
|
|
695
1093
|
|
|
1094
|
+
/**
|
|
1095
|
+
* Shuffle the queue
|
|
1096
|
+
*
|
|
1097
|
+
* @returns {void}
|
|
1098
|
+
* @example
|
|
1099
|
+
* player.shuffle();
|
|
1100
|
+
*/
|
|
696
1101
|
shuffle(): void {
|
|
697
1102
|
this.debug(`[Player] shuffle called`);
|
|
698
1103
|
this.queue.shuffle();
|
|
699
1104
|
}
|
|
700
1105
|
|
|
1106
|
+
/**
|
|
1107
|
+
* Clear the queue
|
|
1108
|
+
*
|
|
1109
|
+
* @returns {void}
|
|
1110
|
+
* @example
|
|
1111
|
+
* player.clearQueue();
|
|
1112
|
+
*/
|
|
701
1113
|
clearQueue(): void {
|
|
702
1114
|
this.debug(`[Player] clearQueue called`);
|
|
703
1115
|
this.queue.clear();
|
|
@@ -708,6 +1120,14 @@ export class Player extends EventEmitter {
|
|
|
708
1120
|
* - If `query` is a string, performs a search and inserts resulting tracks (playlist supported).
|
|
709
1121
|
* - If a Track or Track[] is provided, inserts directly.
|
|
710
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}`);
|
|
711
1131
|
*/
|
|
712
1132
|
async insert(query: string | Track | Track[], index: number, requestedBy?: string): Promise<boolean> {
|
|
713
1133
|
try {
|
|
@@ -749,6 +1169,15 @@ export class Player extends EventEmitter {
|
|
|
749
1169
|
}
|
|
750
1170
|
}
|
|
751
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
|
+
*/
|
|
752
1181
|
remove(index: number): Track | null {
|
|
753
1182
|
this.debug(`[Player] remove called for index: ${index}`);
|
|
754
1183
|
const track = this.queue.remove(index);
|
|
@@ -758,6 +1187,15 @@ export class Player extends EventEmitter {
|
|
|
758
1187
|
return track;
|
|
759
1188
|
}
|
|
760
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
|
+
*/
|
|
761
1199
|
getProgressBar(options: ProgressBarOptions = {}): string {
|
|
762
1200
|
const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
|
|
763
1201
|
const track = this.queue.currentTrack;
|
|
@@ -775,6 +1213,14 @@ export class Player extends EventEmitter {
|
|
|
775
1213
|
return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`;
|
|
776
1214
|
}
|
|
777
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
|
+
*/
|
|
778
1224
|
getTime() {
|
|
779
1225
|
const resource = this.currentResource;
|
|
780
1226
|
const track = this.queue.currentTrack;
|
|
@@ -794,6 +1240,15 @@ export class Player extends EventEmitter {
|
|
|
794
1240
|
};
|
|
795
1241
|
}
|
|
796
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
|
+
*/
|
|
797
1252
|
formatTime(ms: number): string {
|
|
798
1253
|
const totalSeconds = Math.floor(ms / 1000);
|
|
799
1254
|
const hours = Math.floor(totalSeconds / 3600);
|
|
@@ -820,6 +1275,13 @@ export class Player extends EventEmitter {
|
|
|
820
1275
|
}
|
|
821
1276
|
}
|
|
822
1277
|
|
|
1278
|
+
/**
|
|
1279
|
+
* Destroy the player
|
|
1280
|
+
*
|
|
1281
|
+
* @returns {void}
|
|
1282
|
+
* @example
|
|
1283
|
+
* player.destroy();
|
|
1284
|
+
*/
|
|
823
1285
|
destroy(): void {
|
|
824
1286
|
this.debug(`[Player] destroy called`);
|
|
825
1287
|
if (this.leaveTimeout) {
|
|
@@ -843,36 +1305,99 @@ export class Player extends EventEmitter {
|
|
|
843
1305
|
|
|
844
1306
|
this.queue.clear();
|
|
845
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 = [];
|
|
846
1315
|
this.isPlaying = false;
|
|
847
1316
|
this.isPaused = false;
|
|
848
1317
|
this.emit("playerDestroy");
|
|
849
1318
|
this.removeAllListeners();
|
|
850
1319
|
}
|
|
851
1320
|
|
|
852
|
-
|
|
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
|
+
*/
|
|
853
1329
|
get queueSize(): number {
|
|
854
1330
|
return this.queue.size;
|
|
855
1331
|
}
|
|
856
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
|
+
*/
|
|
857
1341
|
get currentTrack(): Track | null {
|
|
858
1342
|
return this.queue.currentTrack;
|
|
859
1343
|
}
|
|
860
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
|
+
*/
|
|
861
1353
|
get previousTrack(): Track | null {
|
|
862
1354
|
return this.queue.previousTracks?.at(-1) ?? null;
|
|
863
1355
|
}
|
|
864
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
|
+
*/
|
|
865
1365
|
get upcomingTracks(): Track[] {
|
|
866
1366
|
return this.queue.getTracks();
|
|
867
1367
|
}
|
|
868
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
|
+
*/
|
|
869
1377
|
get previousTracks(): Track[] {
|
|
870
1378
|
return this.queue.previousTracks;
|
|
871
1379
|
}
|
|
872
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
|
+
*/
|
|
873
1389
|
get availablePlugins(): string[] {
|
|
874
1390
|
return this.pluginManager.getAll().map((p) => p.name);
|
|
875
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
|
+
*/
|
|
876
1401
|
get relatedTracks(): Track[] | null {
|
|
877
1402
|
return this.queue.relatedTracks();
|
|
878
1403
|
}
|