ziplayer 0.3.11 → 0.3.12-dev.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/dist/extensions/index.d.ts +1 -0
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +9 -1
- package/dist/extensions/index.js.map +1 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +106 -61
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.d.ts.map +1 -1
- package/dist/structures/FilterManager.js +8 -4
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/Player.d.ts +1 -1
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +43 -18
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +12 -7
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +113 -79
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/PreloadManager.d.ts.map +1 -1
- package/dist/structures/PreloadManager.js +11 -8
- package/dist/structures/PreloadManager.js.map +1 -1
- package/dist/structures/Queue.js +2 -2
- package/dist/structures/Queue.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/timeout.d.ts.map +1 -1
- package/dist/utils/timeout.js +8 -1
- package/dist/utils/timeout.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/index.ts +9 -1
- package/src/plugins/index.ts +1027 -975
- package/src/structures/FilterManager.ts +8 -4
- package/src/structures/Player.ts +54 -24
- package/src/structures/PlayerManager.ts +125 -84
- package/src/structures/PreloadManager.ts +12 -8
- package/src/structures/Queue.ts +2 -2
- package/src/types/index.ts +2 -0
- package/src/utils/timeout.ts +8 -1
|
@@ -244,8 +244,7 @@ export class FilterManager {
|
|
|
244
244
|
wasRecreated = true;
|
|
245
245
|
|
|
246
246
|
position = -1;
|
|
247
|
-
streamInfo
|
|
248
|
-
if (!filterString) return { ...streamInfo, stream: sourceStream, wasRecreated };
|
|
247
|
+
if (!filterString) return { ...streamInfo, type: "arbitrary", stream: sourceStream, wasRecreated };
|
|
249
248
|
}
|
|
250
249
|
|
|
251
250
|
this.debug(`Applying filters and seek — filters: ${filterString || "none"}, seek: ${position}ms`);
|
|
@@ -259,7 +258,12 @@ export class FilterManager {
|
|
|
259
258
|
this.ffmpegAbortController = abortController;
|
|
260
259
|
|
|
261
260
|
// Nếu có vị trí seek, ưu tiên dùng spawnFFmpegInputSeek
|
|
262
|
-
if (position >= 0
|
|
261
|
+
if (position >= 0) {
|
|
262
|
+
if (!ffmpegPath) {
|
|
263
|
+
this.debug("[FilterManager] ffmpeg-static path not found, seeking may fail");
|
|
264
|
+
// Fallback or throw based on preference, here we try to proceed or throw
|
|
265
|
+
throw new Error("FFmpeg binary not found. Seeking is unavailable.");
|
|
266
|
+
}
|
|
263
267
|
const stream = await this.spawnFFmpegInputSeek(sourceStream, position, filterString, abortController.signal, generation);
|
|
264
268
|
return { ...streamInfo, stream };
|
|
265
269
|
}
|
|
@@ -370,7 +374,7 @@ export class FilterManager {
|
|
|
370
374
|
// Already aborted before we even spawned
|
|
371
375
|
onAbort();
|
|
372
376
|
} else {
|
|
373
|
-
signal.addEventListener("abort", onAbort);
|
|
377
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
374
378
|
}
|
|
375
379
|
|
|
376
380
|
// Pipe source → ffmpeg stdin
|
package/src/structures/Player.ts
CHANGED
|
@@ -125,6 +125,7 @@ export class Player extends EventEmitter {
|
|
|
125
125
|
resource: null,
|
|
126
126
|
track: null,
|
|
127
127
|
streamId: null,
|
|
128
|
+
processedStreamId: null,
|
|
128
129
|
abortController: null,
|
|
129
130
|
isValid: false,
|
|
130
131
|
isLoading: false,
|
|
@@ -331,7 +332,11 @@ export class Player extends EventEmitter {
|
|
|
331
332
|
const stream = (this.currentResource as any)?.metadata?.stream ?? (this.currentResource as any)?.stream;
|
|
332
333
|
|
|
333
334
|
if (stream && typeof stream.destroy === "function") {
|
|
334
|
-
|
|
335
|
+
try {
|
|
336
|
+
stream.destroy();
|
|
337
|
+
} catch (e: any) {
|
|
338
|
+
this.debug("Stream destroy error:", e);
|
|
339
|
+
}
|
|
335
340
|
}
|
|
336
341
|
|
|
337
342
|
this.currentResource = null;
|
|
@@ -464,13 +469,22 @@ export class Player extends EventEmitter {
|
|
|
464
469
|
private async generateWillNext(): Promise<void> {
|
|
465
470
|
const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack;
|
|
466
471
|
if (!lastTrack) return;
|
|
467
|
-
|
|
472
|
+
let related = await this.pluginManager.getRelatedTracks(lastTrack);
|
|
468
473
|
if (!related || related.length === 0) return;
|
|
469
|
-
|
|
474
|
+
|
|
475
|
+
// Lọc bỏ các bài đã có trong hàng đợi sắp tới
|
|
476
|
+
const upcomingUrls = new Set(this.queue.getTracks().map((t) => t.url));
|
|
477
|
+
related = related.filter((t) => !upcomingUrls.has(t.url));
|
|
478
|
+
|
|
479
|
+
if (related.length === 0) return;
|
|
480
|
+
|
|
481
|
+
// Ưu tiên chọn trong top 5 để đảm bảo chất lượng cao nhất
|
|
482
|
+
const poolSize = Math.min(5, related.length);
|
|
483
|
+
const randomchoice = Math.floor(Math.random() * poolSize);
|
|
470
484
|
const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
|
|
471
485
|
this.queue.willNextTrack(nextTrack);
|
|
472
486
|
this.queue.relatedTracks(related);
|
|
473
|
-
this.debug(`[Player] Will next track if autoplay: ${nextTrack?.title}
|
|
487
|
+
this.debug(`[Player] Will next track if autoplay: ${nextTrack?.title}`);
|
|
474
488
|
this.emit("willPlay", nextTrack, related);
|
|
475
489
|
}
|
|
476
490
|
//#endregion
|
|
@@ -1128,17 +1142,20 @@ export class Player extends EventEmitter {
|
|
|
1128
1142
|
return await this.playRemote(track, streamInfo);
|
|
1129
1143
|
}
|
|
1130
1144
|
|
|
1131
|
-
if (!streamInfo?.stream) {
|
|
1145
|
+
if (!streamInfo?.stream && !streamInfo?.url) {
|
|
1132
1146
|
throw new Error(`No stream available`);
|
|
1133
1147
|
}
|
|
1134
1148
|
|
|
1135
1149
|
// Register the RAW source stream — this is what we can reuse on seek
|
|
1136
|
-
const rawStreamId =
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1150
|
+
const rawStreamId =
|
|
1151
|
+
streamInfo.stream ?
|
|
1152
|
+
this.streamManager.registerStream(streamInfo.stream, track, {
|
|
1153
|
+
source: track.source || "stream",
|
|
1154
|
+
isPreload: false,
|
|
1155
|
+
isRemote: !!streamInfo?.remote,
|
|
1156
|
+
priority: 10,
|
|
1157
|
+
})
|
|
1158
|
+
: null;
|
|
1142
1159
|
|
|
1143
1160
|
// createResource now returns both the AudioResource
|
|
1144
1161
|
// AND the processedStream (ffmpeg stdout) when filters/seek are involved.
|
|
@@ -1159,14 +1176,14 @@ export class Player extends EventEmitter {
|
|
|
1159
1176
|
if (this.currentSlot.streamId && this.currentSlot.streamId !== rawStreamId) {
|
|
1160
1177
|
this.streamManager.unregisterStream(this.currentSlot.streamId, true);
|
|
1161
1178
|
}
|
|
1162
|
-
if (
|
|
1163
|
-
this.streamManager.unregisterStream(
|
|
1179
|
+
if (this.currentSlot.processedStreamId && this.currentSlot.processedStreamId !== playStreamId) {
|
|
1180
|
+
this.streamManager.unregisterStream(this.currentSlot.processedStreamId, true);
|
|
1164
1181
|
}
|
|
1165
1182
|
|
|
1166
1183
|
this.currentSlot.resource = resource;
|
|
1167
1184
|
this.currentSlot.track = track;
|
|
1168
1185
|
this.currentSlot.streamId = rawStreamId;
|
|
1169
|
-
|
|
1186
|
+
this.currentSlot.processedStreamId = processedStream ? playStreamId : null;
|
|
1170
1187
|
this.currentSlot.isValid = true;
|
|
1171
1188
|
this.currentResource = resource;
|
|
1172
1189
|
this.seekOffset = 0;
|
|
@@ -1449,7 +1466,7 @@ export class Player extends EventEmitter {
|
|
|
1449
1466
|
*/
|
|
1450
1467
|
async connect(
|
|
1451
1468
|
channel: VoiceChannel,
|
|
1452
|
-
options: { group: string; selfDeaf: boolean; selfMute: boolean },
|
|
1469
|
+
options: { group: string; selfDeaf: boolean; selfMute: boolean } = {} as any,
|
|
1453
1470
|
): Promise<VoiceConnection> {
|
|
1454
1471
|
try {
|
|
1455
1472
|
this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
|
|
@@ -2508,10 +2525,10 @@ export class Player extends EventEmitter {
|
|
|
2508
2525
|
}
|
|
2509
2526
|
|
|
2510
2527
|
// Clean up processedStream first (it's what AudioResource reads)
|
|
2511
|
-
const processedStreamId =
|
|
2528
|
+
const processedStreamId = this.currentSlot.processedStreamId;
|
|
2512
2529
|
if (processedStreamId && processedStreamId !== currentStreamId) {
|
|
2513
2530
|
this.streamManager.unregisterStream(processedStreamId, true);
|
|
2514
|
-
|
|
2531
|
+
this.currentSlot.processedStreamId = null;
|
|
2515
2532
|
}
|
|
2516
2533
|
|
|
2517
2534
|
if (currentStreamId) {
|
|
@@ -2542,9 +2559,19 @@ export class Player extends EventEmitter {
|
|
|
2542
2559
|
this.extensionManager.clearCache("stream");
|
|
2543
2560
|
this.debug(`[Player] Fetching fresh stream${!isForwardSeek ? " (backward seek)" : " (reuse failed)"}`);
|
|
2544
2561
|
streaminfo = await this.getStream(track);
|
|
2562
|
+
|
|
2563
|
+
if (this.destroyed) {
|
|
2564
|
+
this.debug(`[Player] refreshPlayerResource: Player destroyed during stream fetch`);
|
|
2565
|
+
return false;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
|
|
2569
|
+
this.debug(`[Player] refreshPlayerResource: Player state changed during stream fetch, aborting`);
|
|
2570
|
+
return false;
|
|
2571
|
+
}
|
|
2545
2572
|
}
|
|
2546
2573
|
|
|
2547
|
-
if (!streaminfo?.stream) {
|
|
2574
|
+
if (!streaminfo?.stream && !streaminfo?.url) {
|
|
2548
2575
|
this.debug(`[Player] No stream available for refresh`);
|
|
2549
2576
|
return false;
|
|
2550
2577
|
}
|
|
@@ -2554,11 +2581,14 @@ export class Player extends EventEmitter {
|
|
|
2554
2581
|
const { resource, processedStream } = await this.createResource(streaminfo, track, createPosition);
|
|
2555
2582
|
|
|
2556
2583
|
// Register raw source stream
|
|
2557
|
-
const newStreamId =
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2584
|
+
const newStreamId =
|
|
2585
|
+
streaminfo.stream ?
|
|
2586
|
+
this.streamManager.registerStream(streaminfo.stream, track, {
|
|
2587
|
+
source: track.source || "stream",
|
|
2588
|
+
isPreload: false,
|
|
2589
|
+
priority: 10,
|
|
2590
|
+
})
|
|
2591
|
+
: null;
|
|
2562
2592
|
|
|
2563
2593
|
let newProcessedStreamId: string | null = null;
|
|
2564
2594
|
if (processedStream && processedStream !== streaminfo.stream) {
|
|
@@ -2572,7 +2602,7 @@ export class Player extends EventEmitter {
|
|
|
2572
2602
|
this.currentSlot.resource = resource;
|
|
2573
2603
|
this.currentSlot.track = track;
|
|
2574
2604
|
this.currentSlot.streamId = newStreamId;
|
|
2575
|
-
|
|
2605
|
+
this.currentSlot.processedStreamId = newProcessedStreamId;
|
|
2576
2606
|
this.currentSlot.isValid = true;
|
|
2577
2607
|
this.currentResource = resource;
|
|
2578
2608
|
|
|
@@ -15,9 +15,10 @@ import {
|
|
|
15
15
|
} from "../types";
|
|
16
16
|
import type { BaseExtension } from "../extensions";
|
|
17
17
|
import { withTimeout } from "../utils/timeout";
|
|
18
|
-
import { PluginManager } from "../plugins";
|
|
19
18
|
|
|
20
19
|
const GLOBAL_MANAGER_KEY: symbol = Symbol.for("ziplayer.PlayerManager.instance");
|
|
20
|
+
/** Guild id for the internal search-only player (never stored in {@link PlayerManager.players}). */
|
|
21
|
+
const SEARCH_PLAYER_GUILD_ID = "__ziplayer_search__";
|
|
21
22
|
|
|
22
23
|
export const getGlobalManager = (): PlayerManager | null => {
|
|
23
24
|
try {
|
|
@@ -90,6 +91,7 @@ interface ManagerCacheEntry<T> {
|
|
|
90
91
|
export class PlayerManager extends EventEmitter {
|
|
91
92
|
private static instance: PlayerManager | null = null;
|
|
92
93
|
private players: Map<string, Player> = new Map();
|
|
94
|
+
private pendingPlayers: Map<string, Promise<Player>> = new Map();
|
|
93
95
|
private searchCache: Map<string, ManagerCacheEntry<SearchResult>>;
|
|
94
96
|
private readonly SEARCH_CACHE_TTL = 60 * 1000; // 1 minute
|
|
95
97
|
private readonly MAX_CACHE_SIZE = 100;
|
|
@@ -105,7 +107,8 @@ export class PlayerManager extends EventEmitter {
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
private plugins: SourcePlugin[];
|
|
108
|
-
|
|
110
|
+
/** Reused player for {@link search}; not registered in {@link players}. */
|
|
111
|
+
private searchPlayer: Player | null = null;
|
|
109
112
|
private extensions: any[];
|
|
110
113
|
private B_debug: boolean = false;
|
|
111
114
|
private extractorTimeout: number = 10000;
|
|
@@ -126,9 +129,6 @@ export class PlayerManager extends EventEmitter {
|
|
|
126
129
|
constructor(options: PlayerManagerOptions = {}) {
|
|
127
130
|
super();
|
|
128
131
|
this.plugins = [];
|
|
129
|
-
this.pluginManager = new PluginManager(null as any, this, {
|
|
130
|
-
extractorTimeout: this.extractorTimeout,
|
|
131
|
-
});
|
|
132
132
|
this.searchCache = new Map();
|
|
133
133
|
|
|
134
134
|
// Initialize plugins
|
|
@@ -145,7 +145,6 @@ export class PlayerManager extends EventEmitter {
|
|
|
145
145
|
|
|
146
146
|
if (instance) {
|
|
147
147
|
this.plugins.push(instance);
|
|
148
|
-
this.pluginManager.register(instance);
|
|
149
148
|
}
|
|
150
149
|
this.debug(`Registered plugin: ${p.name || "unnamed"}`);
|
|
151
150
|
} catch (e) {
|
|
@@ -249,6 +248,25 @@ export class PlayerManager extends EventEmitter {
|
|
|
249
248
|
this.debug(`Auto-cleanup started with interval: ${this.cleanupTimeout}ms`);
|
|
250
249
|
}
|
|
251
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Lazy internal player used only for {@link search}.
|
|
253
|
+
* Not added to {@link players} and does not forward manager events.
|
|
254
|
+
*/
|
|
255
|
+
private getSearchPlayer(): Player {
|
|
256
|
+
if (this.searchPlayer && !this.searchPlayer.destroyed) {
|
|
257
|
+
return this.searchPlayer;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const player = new Player(SEARCH_PLAYER_GUILD_ID, { extractorTimeout: this.extractorTimeout }, this);
|
|
261
|
+
for (const plugin of this.plugins) {
|
|
262
|
+
player.addPlugin(plugin);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.searchPlayer = player;
|
|
266
|
+
this.debug(`Created internal search player (not stored in players map)`);
|
|
267
|
+
return player;
|
|
268
|
+
}
|
|
269
|
+
|
|
252
270
|
private startStatsCollection(): void {
|
|
253
271
|
if (this.statsInterval) {
|
|
254
272
|
clearInterval(this.statsInterval);
|
|
@@ -291,85 +309,104 @@ export class PlayerManager extends EventEmitter {
|
|
|
291
309
|
async create(guildOrId: string | { id: string }, options?: PlayerOptions): Promise<Player> {
|
|
292
310
|
const guildId = this.resolveGuildId(guildOrId);
|
|
293
311
|
|
|
312
|
+
if (guildId === SEARCH_PLAYER_GUILD_ID) {
|
|
313
|
+
throw new Error(`Guild id "${SEARCH_PLAYER_GUILD_ID}" is reserved for internal search.`);
|
|
314
|
+
}
|
|
315
|
+
|
|
294
316
|
if (this.players.has(guildId)) {
|
|
295
317
|
this.debug(`Player already exists for guildId: ${guildId}, returning existing`);
|
|
296
318
|
return this.players.get(guildId)!;
|
|
297
319
|
}
|
|
298
320
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
this.plugins.forEach((plugin) => player.addPlugin(plugin));
|
|
304
|
-
|
|
305
|
-
// Activate extensions
|
|
306
|
-
let extsToActivate: any[] = [];
|
|
307
|
-
const optExts = (options as any)?.extensions as any[] | string[] | undefined;
|
|
308
|
-
|
|
309
|
-
if (Array.isArray(optExts)) {
|
|
310
|
-
if (optExts.length === 0) {
|
|
311
|
-
extsToActivate = [];
|
|
312
|
-
} else if (typeof optExts[0] === "string") {
|
|
313
|
-
const wanted = new Set(optExts as string[]);
|
|
314
|
-
extsToActivate = this.extensions.filter((ext) => {
|
|
315
|
-
const name = typeof ext === "function" ? ext.name : ext?.name;
|
|
316
|
-
return !!name && wanted.has(name);
|
|
317
|
-
});
|
|
318
|
-
} else {
|
|
319
|
-
extsToActivate = optExts;
|
|
320
|
-
}
|
|
321
|
-
} else {
|
|
322
|
-
// Use all extensions by default
|
|
323
|
-
extsToActivate = this.extensions;
|
|
321
|
+
// Check if a player is already being created for this guild
|
|
322
|
+
if (this.pendingPlayers.has(guildId)) {
|
|
323
|
+
this.debug(`Player creation already in progress for guildId: ${guildId}, awaiting...`);
|
|
324
|
+
return this.pendingPlayers.get(guildId)!;
|
|
324
325
|
}
|
|
325
326
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
327
|
+
const creationPromise = (async () => {
|
|
328
|
+
try {
|
|
329
|
+
this.debug(`Creating player for guildId: ${guildId}`);
|
|
330
|
+
const player = new Player(guildId, options, this);
|
|
331
|
+
|
|
332
|
+
// Add all registered plugins
|
|
333
|
+
this.plugins.forEach((plugin) => player.addPlugin(plugin));
|
|
334
|
+
|
|
335
|
+
// Activate extensions
|
|
336
|
+
let extsToActivate: any[] = [];
|
|
337
|
+
const optExts = (options as any)?.extensions as any[] | string[] | undefined;
|
|
338
|
+
|
|
339
|
+
if (Array.isArray(optExts)) {
|
|
340
|
+
if (optExts.length === 0) {
|
|
341
|
+
extsToActivate = [];
|
|
342
|
+
} else if (typeof optExts[0] === "string") {
|
|
343
|
+
const wanted = new Set(optExts as string[]);
|
|
344
|
+
extsToActivate = this.extensions.filter((ext) => {
|
|
345
|
+
const name = typeof ext === "function" ? ext.name : ext?.name;
|
|
346
|
+
return !!name && wanted.has(name);
|
|
347
|
+
});
|
|
348
|
+
} else {
|
|
349
|
+
extsToActivate = optExts;
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
// Use all extensions by default
|
|
353
|
+
extsToActivate = this.extensions;
|
|
334
354
|
}
|
|
335
|
-
}
|
|
336
355
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
Promise.resolve(extInstance.active({ manager: this, player })),
|
|
347
|
-
player.options.extractorTimeout ?? 15000,
|
|
348
|
-
`Extension ${extInstance?.name} activation timed out`,
|
|
349
|
-
);
|
|
350
|
-
this.debug(`Extension ${extInstance?.name} active check returned: ${activated}`);
|
|
351
|
-
} catch (e) {
|
|
352
|
-
activated = false;
|
|
353
|
-
this.debug(`Extension activation error for ${extInstance?.name}:`, e);
|
|
356
|
+
for (const ext of extsToActivate) {
|
|
357
|
+
let instance = ext;
|
|
358
|
+
if (typeof ext === "function") {
|
|
359
|
+
try {
|
|
360
|
+
instance = new ext(player);
|
|
361
|
+
} catch (e) {
|
|
362
|
+
this.debug(`Extension constructor error for ${ext.name}:`, e);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
354
365
|
}
|
|
355
366
|
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
367
|
+
if (instance && typeof instance === "object") {
|
|
368
|
+
const extInstance = instance as BaseExtension;
|
|
369
|
+
if ("player" in extInstance && !extInstance.player) extInstance.player = player;
|
|
370
|
+
player.attachExtension(extInstance);
|
|
371
|
+
|
|
372
|
+
if (typeof extInstance.active === "function") {
|
|
373
|
+
let activated: boolean | void = true;
|
|
374
|
+
try {
|
|
375
|
+
activated = await withTimeout(
|
|
376
|
+
Promise.resolve(extInstance.active({ manager: this, player })),
|
|
377
|
+
player.options.extractorTimeout ?? 15000,
|
|
378
|
+
`Extension ${extInstance?.name} activation timed out`,
|
|
379
|
+
);
|
|
380
|
+
this.debug(`Extension ${extInstance?.name} active check returned: ${activated}`);
|
|
381
|
+
} catch (e) {
|
|
382
|
+
activated = false;
|
|
383
|
+
this.debug(`Extension activation error for ${extInstance?.name}:`, e);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (activated === false) {
|
|
387
|
+
player.detachExtension(extInstance);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
359
391
|
}
|
|
360
392
|
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
393
|
|
|
364
|
-
|
|
365
|
-
|
|
394
|
+
// Forward all player events to manager
|
|
395
|
+
this.setupEventForwarding(player, guildId);
|
|
366
396
|
|
|
367
|
-
|
|
368
|
-
|
|
397
|
+
// Mark last activity
|
|
398
|
+
(player as any)._lastActivity = Date.now();
|
|
369
399
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
400
|
+
this.players.set(guildId, player);
|
|
401
|
+
this.debug(`Player created for guildId: ${guildId}`);
|
|
402
|
+
return player;
|
|
403
|
+
} finally {
|
|
404
|
+
this.pendingPlayers.delete(guildId);
|
|
405
|
+
}
|
|
406
|
+
})();
|
|
407
|
+
|
|
408
|
+
this.pendingPlayers.set(guildId, creationPromise);
|
|
409
|
+
return creationPromise;
|
|
373
410
|
}
|
|
374
411
|
|
|
375
412
|
private setupEventForwarding(player: Player, guildId: string): void {
|
|
@@ -770,6 +807,11 @@ export class PlayerManager extends EventEmitter {
|
|
|
770
807
|
player.destroy();
|
|
771
808
|
}
|
|
772
809
|
|
|
810
|
+
if (this.searchPlayer && !this.searchPlayer.destroyed) {
|
|
811
|
+
this.searchPlayer.destroy();
|
|
812
|
+
}
|
|
813
|
+
this.searchPlayer = null;
|
|
814
|
+
|
|
773
815
|
this.players.clear();
|
|
774
816
|
this.searchCache.clear();
|
|
775
817
|
this.removeAllListeners();
|
|
@@ -777,13 +819,11 @@ export class PlayerManager extends EventEmitter {
|
|
|
777
819
|
}
|
|
778
820
|
|
|
779
821
|
/**
|
|
780
|
-
* Search
|
|
822
|
+
* Search via an internal Player instance (all registered plugins) without
|
|
823
|
+
* storing it in {@link players}.
|
|
781
824
|
*
|
|
782
|
-
* Uses the same search pipeline as Player.search
|
|
783
|
-
*
|
|
784
|
-
* - plugin deduplication
|
|
785
|
-
* - plugin scoring/evaluation
|
|
786
|
-
* - fallback handling
|
|
825
|
+
* Uses the same search pipeline as {@link Player.search}:
|
|
826
|
+
* extension hooks, plugin deduplication, scoring, and fallback handling.
|
|
787
827
|
*
|
|
788
828
|
* @param {string} query
|
|
789
829
|
* @param {string} requestedBy
|
|
@@ -792,20 +832,15 @@ export class PlayerManager extends EventEmitter {
|
|
|
792
832
|
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
793
833
|
this.debug(`Search called with query: ${query}, requestedBy: ${requestedBy}`);
|
|
794
834
|
|
|
795
|
-
// Cache
|
|
796
835
|
const cached = this.getCachedSearch(query);
|
|
797
836
|
if (cached) {
|
|
798
837
|
return cached;
|
|
799
838
|
}
|
|
800
839
|
|
|
801
840
|
try {
|
|
802
|
-
const result = await this.
|
|
803
|
-
|
|
804
|
-
if (!result || !Array.isArray(result.tracks) || result.tracks.length === 0) {
|
|
805
|
-
throw new Error(`No results found for: ${query}`);
|
|
806
|
-
}
|
|
841
|
+
const result = await this.getSearchPlayer().search(query, requestedBy);
|
|
807
842
|
|
|
808
|
-
this.debug(`
|
|
843
|
+
this.debug(`Search returned ${result.tracks.length} tracks (score: ${result.score?.score ?? "unknown"}%)`);
|
|
809
844
|
|
|
810
845
|
if (result.score) {
|
|
811
846
|
this.debug(`Search evaluation - ${result.score.reason}`);
|
|
@@ -836,10 +871,13 @@ export class PlayerManager extends EventEmitter {
|
|
|
836
871
|
*/
|
|
837
872
|
registerPlugin(plugin: SourcePlugin): void {
|
|
838
873
|
this.plugins.push(plugin);
|
|
839
|
-
this.pluginManager.register(plugin);
|
|
840
874
|
|
|
841
875
|
this.debug(`Registered plugin: ${plugin.name}`);
|
|
842
876
|
|
|
877
|
+
if (this.searchPlayer && !this.searchPlayer.destroyed) {
|
|
878
|
+
this.searchPlayer.addPlugin(plugin);
|
|
879
|
+
}
|
|
880
|
+
|
|
843
881
|
for (const player of this.players.values()) {
|
|
844
882
|
player.addPlugin(plugin);
|
|
845
883
|
}
|
|
@@ -856,7 +894,10 @@ export class PlayerManager extends EventEmitter {
|
|
|
856
894
|
if (index === -1) return false;
|
|
857
895
|
|
|
858
896
|
this.plugins.splice(index, 1);
|
|
859
|
-
|
|
897
|
+
|
|
898
|
+
if (this.searchPlayer && !this.searchPlayer.destroyed) {
|
|
899
|
+
this.searchPlayer.removePlugin(name);
|
|
900
|
+
}
|
|
860
901
|
|
|
861
902
|
this.debug(`Unregistered plugin: ${name}`);
|
|
862
903
|
|
|
@@ -24,6 +24,7 @@ export class PreloadManager {
|
|
|
24
24
|
resource: null,
|
|
25
25
|
track: null,
|
|
26
26
|
streamId: null,
|
|
27
|
+
processedStreamId: null,
|
|
27
28
|
abortController: null,
|
|
28
29
|
isValid: false,
|
|
29
30
|
isLoading: false,
|
|
@@ -239,17 +240,20 @@ export class PreloadManager {
|
|
|
239
240
|
this.debugLog(`[Preload] Track changed after stream fetch`);
|
|
240
241
|
throw new Error("PRELOAD_CANCELLED");
|
|
241
242
|
}
|
|
242
|
-
if (!streamInfo?.stream) {
|
|
243
|
+
if (!streamInfo?.stream && !streamInfo?.url) {
|
|
243
244
|
throw new Error(`No stream available`);
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
const streamId =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
const streamId =
|
|
248
|
+
streamInfo.stream ?
|
|
249
|
+
this.streamManager.registerStream(streamInfo.stream, track, {
|
|
250
|
+
source: track.source || "preload",
|
|
251
|
+
isPreload: true,
|
|
252
|
+
priority: 5,
|
|
253
|
+
})
|
|
254
|
+
: null;
|
|
251
255
|
|
|
252
|
-
const resource = createAudioResource(streamInfo.stream
|
|
256
|
+
const resource = createAudioResource(streamInfo.stream || streamInfo.url!, {
|
|
253
257
|
inlineVolume: true,
|
|
254
258
|
metadata: { ...track, preloaded: true },
|
|
255
259
|
});
|
|
@@ -277,7 +281,7 @@ export class PreloadManager {
|
|
|
277
281
|
signal.removeEventListener("abort", handler);
|
|
278
282
|
reject(new Error("PRELOAD_CANCELLED"));
|
|
279
283
|
};
|
|
280
|
-
signal.addEventListener("abort", handler);
|
|
284
|
+
signal.addEventListener("abort", handler, { once: true });
|
|
281
285
|
});
|
|
282
286
|
|
|
283
287
|
const existingStream = this.streamManager.getStreamByTrack(track.id || track.title);
|
package/src/structures/Queue.ts
CHANGED
|
@@ -231,9 +231,9 @@ export class Queue {
|
|
|
231
231
|
this.current = this.tracks.shift() || null;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// Skip bypassed track loop but no other track exists →
|
|
234
|
+
// Skip bypassed track loop but no other track exists → trigger queue end
|
|
235
235
|
if (!this.current && this._loop === "track" && ignoreLoop && this.history.length > 0) {
|
|
236
|
-
|
|
236
|
+
return null;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
return this.current;
|
package/src/types/index.ts
CHANGED
|
@@ -420,6 +420,7 @@ export interface SaveOptions {
|
|
|
420
420
|
/** Seek position in milliseconds to start saving from */
|
|
421
421
|
seek?: number;
|
|
422
422
|
}
|
|
423
|
+
|
|
423
424
|
export interface PlayerSession {
|
|
424
425
|
guildId: string;
|
|
425
426
|
queue: Track[];
|
|
@@ -471,6 +472,7 @@ export interface StreamSlot {
|
|
|
471
472
|
resource: AudioResource | null;
|
|
472
473
|
track: Track | null;
|
|
473
474
|
streamId: string | null;
|
|
475
|
+
processedStreamId: string | null;
|
|
474
476
|
abortController: AbortController | null;
|
|
475
477
|
isValid: boolean;
|
|
476
478
|
isLoading: boolean;
|
package/src/utils/timeout.ts
CHANGED
|
@@ -6,5 +6,12 @@
|
|
|
6
6
|
* @returns Promise that rejects if timeout is reached
|
|
7
7
|
*/
|
|
8
8
|
export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
|
9
|
-
|
|
9
|
+
let timeoutId: NodeJS.Timeout;
|
|
10
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
11
|
+
timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
15
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
16
|
+
});
|
|
10
17
|
}
|