ziplayer 0.3.2 → 0.3.3
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/{AI-Guide.md → AGENTS.md} +36 -7
- package/README.md +113 -0
- package/dist/plugins/index.js +1 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/Player.d.ts +45 -1
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +80 -0
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +35 -6
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +215 -19
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +19 -0
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +21 -0
- package/dist/structures/Queue.js.map +1 -1
- package/dist/types/index.d.ts +40 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/index.ts +1 -1
- package/src/structures/Player.ts +103 -16
- package/src/structures/PlayerManager.ts +253 -23
- package/src/structures/Queue.ts +22 -0
- package/src/types/index.ts +46 -0
package/dist/types/index.js
CHANGED
|
@@ -14,6 +14,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.normalizeTrackMiddleware = normalizeTrackMiddleware;
|
|
18
|
+
function normalizeTrackMiddleware(input) {
|
|
19
|
+
if (!input)
|
|
20
|
+
return [];
|
|
21
|
+
return Array.isArray(input) ? input : [input];
|
|
22
|
+
}
|
|
17
23
|
__exportStar(require("./fillter"), exports);
|
|
18
24
|
__exportStar(require("./plugin"), exports);
|
|
19
25
|
__exportStar(require("./extension"), exports);
|
package/dist/types/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AA4JA,4DAGC;AAHD,SAAgB,wBAAwB,CAAC,KAA2C;IACnF,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAC/C,CAAC;AAgbD,4CAA0B;AAC1B,2CAAyB;AACzB,8CAA4B"}
|
package/package.json
CHANGED
package/src/plugins/index.ts
CHANGED
|
@@ -848,7 +848,7 @@ export class PluginManager {
|
|
|
848
848
|
return [];
|
|
849
849
|
}
|
|
850
850
|
|
|
851
|
-
const history = this.player
|
|
851
|
+
const history = this.player?.queue?.previousTracks || [];
|
|
852
852
|
const historyUrls = new Set(history.map((t) => t.url));
|
|
853
853
|
const currentTrackUrl = track.url;
|
|
854
854
|
|
package/src/structures/Player.ts
CHANGED
|
@@ -16,22 +16,24 @@ import {
|
|
|
16
16
|
import { Readable } from "stream";
|
|
17
17
|
import { LRUCache } from "lru-cache";
|
|
18
18
|
import type { BaseExtension } from "../extensions";
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
import {
|
|
20
|
+
normalizeTrackMiddleware,
|
|
21
|
+
type Track,
|
|
22
|
+
type PlayerOptions,
|
|
23
|
+
type PlayerEvents,
|
|
24
|
+
type SourcePlugin,
|
|
25
|
+
type SearchResult,
|
|
26
|
+
type ProgressBarOptions,
|
|
27
|
+
type LoopMode,
|
|
28
|
+
type StreamInfo,
|
|
29
|
+
type SaveOptions,
|
|
30
|
+
type VoiceChannel,
|
|
31
|
+
type PlayerSession,
|
|
32
|
+
type ExtensionPlayRequest,
|
|
33
|
+
type ExtensionPlayResponse,
|
|
34
|
+
type ExtensionAfterPlayPayload,
|
|
35
|
+
type StreamSlot,
|
|
36
|
+
type TrackMiddleware,
|
|
35
37
|
} from "../types";
|
|
36
38
|
import type { PlayerManager } from "./PlayerManager";
|
|
37
39
|
|
|
@@ -96,6 +98,7 @@ export class Player extends EventEmitter {
|
|
|
96
98
|
public extensionManager: ExtensionManager;
|
|
97
99
|
public streamManager: StreamManager;
|
|
98
100
|
public preloadManager: PreloadManager;
|
|
101
|
+
public forwardMode: Boolean = false;
|
|
99
102
|
|
|
100
103
|
public userdata?: Record<string, any>;
|
|
101
104
|
public _lastActivity: number = Date.now();
|
|
@@ -154,6 +157,7 @@ export class Player extends EventEmitter {
|
|
|
154
157
|
private loudnessMaxCutDb = 10;
|
|
155
158
|
private loudnessLimiterCeiling = 0.95;
|
|
156
159
|
private destroyed = false;
|
|
160
|
+
private readonly trackMiddlewareChain: TrackMiddleware[];
|
|
157
161
|
|
|
158
162
|
// Cache for search results to avoid duplicate calls
|
|
159
163
|
private searchCache: LRUCache<string, SearchResult>;
|
|
@@ -247,6 +251,9 @@ export class Player extends EventEmitter {
|
|
|
247
251
|
this.debug(
|
|
248
252
|
`[Player] Runtime options resolved: lowPerformance=${this.lowPerformanceMode}, preload=${this.preloadEnabled}, crossfade=${this.crossfadeEnabled} (${this.crossfadeDurationMs}ms), smartTransition=${this.smartTransitionEnabled}, antiStuck=${this.antiStuckEnabled}, loudnessNormalization=${this.loudnessNormalizationEnabled}`,
|
|
249
253
|
);
|
|
254
|
+
|
|
255
|
+
this.trackMiddlewareChain = [...this.manager.getTrackMiddlewareChain(), ...normalizeTrackMiddleware(options.trackMiddleware)];
|
|
256
|
+
|
|
250
257
|
this.filter = new FilterManager(this, this.manager);
|
|
251
258
|
this.extensionManager = new ExtensionManager(this, this.manager);
|
|
252
259
|
this.pluginManager = new PluginManager(this, this.manager, {
|
|
@@ -906,10 +913,36 @@ export class Player extends EventEmitter {
|
|
|
906
913
|
}
|
|
907
914
|
}
|
|
908
915
|
|
|
916
|
+
private mergeTrackPreserveRef(target: Track, source: Track): void {
|
|
917
|
+
if (source === target) return;
|
|
918
|
+
const mergedMeta = {
|
|
919
|
+
...(target.metadata || {}),
|
|
920
|
+
...(source.metadata || {}),
|
|
921
|
+
};
|
|
922
|
+
Object.assign(target, source);
|
|
923
|
+
target.metadata = mergedMeta;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private async applyTrackMiddleware(track: Track): Promise<void> {
|
|
927
|
+
if (this.trackMiddlewareChain.length === 0) return;
|
|
928
|
+
const ctx = { player: this, manager: this.manager };
|
|
929
|
+
for (const mw of this.trackMiddlewareChain) {
|
|
930
|
+
try {
|
|
931
|
+
const out = await mw(track, ctx);
|
|
932
|
+
if (out != null && out !== track) {
|
|
933
|
+
this.mergeTrackPreserveRef(track, out);
|
|
934
|
+
}
|
|
935
|
+
} catch (err) {
|
|
936
|
+
this.debug(`[TrackMiddleware] Error:`, err);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
909
941
|
private async getStream(track: Track): Promise<StreamInfo | null> {
|
|
910
942
|
if (this.destroyed) {
|
|
911
943
|
throw new Error("PLAYER_DESTROYED");
|
|
912
944
|
}
|
|
945
|
+
await this.applyTrackMiddleware(track);
|
|
913
946
|
const trackId = track.id || track.url || track.title;
|
|
914
947
|
const existingStream = this.streamManager.getStreamByTrack(trackId);
|
|
915
948
|
|
|
@@ -1263,6 +1296,8 @@ export class Player extends EventEmitter {
|
|
|
1263
1296
|
if (!this.connection) throw new Error("No voice connection for TTS");
|
|
1264
1297
|
const ttsPlayer = this.ensureTTSPlayer();
|
|
1265
1298
|
|
|
1299
|
+
await this.applyTrackMiddleware(track);
|
|
1300
|
+
|
|
1266
1301
|
// Build resource from plugin stream
|
|
1267
1302
|
const streamInfo = await this.pluginManager.getStream(track);
|
|
1268
1303
|
if (!streamInfo) {
|
|
@@ -1387,6 +1422,56 @@ export class Player extends EventEmitter {
|
|
|
1387
1422
|
}
|
|
1388
1423
|
}
|
|
1389
1424
|
|
|
1425
|
+
/**
|
|
1426
|
+
* Subscribe this player's voice connection
|
|
1427
|
+
* to another player's audio stream.
|
|
1428
|
+
*
|
|
1429
|
+
* This is primarily used for:
|
|
1430
|
+
* - playback mirroring
|
|
1431
|
+
* - radio/broadcast systems
|
|
1432
|
+
* - multi-guild synchronized playback
|
|
1433
|
+
* - forwardMode shared streaming
|
|
1434
|
+
*
|
|
1435
|
+
* Instead of creating a separate audio stream,
|
|
1436
|
+
* this player will directly receive audio packets
|
|
1437
|
+
* from the target player's AudioPlayer instance.
|
|
1438
|
+
*
|
|
1439
|
+
* Benefits:
|
|
1440
|
+
* - drastically lower CPU usage
|
|
1441
|
+
* - only one ffmpeg/extractor stream
|
|
1442
|
+
* - lower bandwidth and memory usage
|
|
1443
|
+
* - perfect sync across guilds
|
|
1444
|
+
*
|
|
1445
|
+
* Important:
|
|
1446
|
+
* - both players must already have active voice connections
|
|
1447
|
+
* - this does NOT transfer queue ownership
|
|
1448
|
+
* - this does NOT clone playback state automatically
|
|
1449
|
+
*
|
|
1450
|
+
* @param {Player} player - Source player to subscribe to
|
|
1451
|
+
*
|
|
1452
|
+
* @returns {boolean}
|
|
1453
|
+
* Returns true if subscription succeeded,
|
|
1454
|
+
* otherwise false.
|
|
1455
|
+
*
|
|
1456
|
+
* @example
|
|
1457
|
+
* follower.subscribeTo(leader);
|
|
1458
|
+
*
|
|
1459
|
+
* @example
|
|
1460
|
+
* if (!player.subscribeTo(leader)) {
|
|
1461
|
+
* console.log("Failed to subscribe");
|
|
1462
|
+
* }
|
|
1463
|
+
*/
|
|
1464
|
+
public subscribeTo(player: Player): boolean {
|
|
1465
|
+
if (!this.connection) return false;
|
|
1466
|
+
|
|
1467
|
+
this.connection.subscribe(player.audioPlayer);
|
|
1468
|
+
|
|
1469
|
+
this.isPlaying = player.isPlaying;
|
|
1470
|
+
this.isPaused = player.isPaused;
|
|
1471
|
+
this.forwardMode = true;
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1390
1475
|
/**
|
|
1391
1476
|
* Pause the current track
|
|
1392
1477
|
*
|
|
@@ -1590,6 +1675,8 @@ export class Player extends EventEmitter {
|
|
|
1590
1675
|
}
|
|
1591
1676
|
|
|
1592
1677
|
try {
|
|
1678
|
+
await this.applyTrackMiddleware(track);
|
|
1679
|
+
|
|
1593
1680
|
// Skip extension manager for saving - we want the raw stream without filters/seek applied, and extensions may not support this
|
|
1594
1681
|
let streamInfo: StreamInfo | null = await this.pluginManager.getStream(track);
|
|
1595
1682
|
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
import { Player } from "./Player";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
PlayerManagerOptions,
|
|
5
|
+
PlayerOptions,
|
|
6
|
+
type Track,
|
|
7
|
+
SourcePlugin,
|
|
8
|
+
SearchResult,
|
|
9
|
+
ManagerEvents,
|
|
10
|
+
PlayerStats,
|
|
11
|
+
type PlaybackMirrorOptions,
|
|
12
|
+
type TrackMiddleware,
|
|
13
|
+
normalizeTrackMiddleware,
|
|
14
|
+
} from "../types";
|
|
4
15
|
import type { BaseExtension } from "../extensions";
|
|
5
16
|
import { withTimeout } from "../utils/timeout";
|
|
17
|
+
import { PluginManager } from "../plugins";
|
|
6
18
|
|
|
7
19
|
const GLOBAL_MANAGER_KEY: symbol = Symbol.for("ziplayer.PlayerManager.instance");
|
|
8
20
|
|
|
@@ -92,12 +104,15 @@ export class PlayerManager extends EventEmitter {
|
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
private plugins: SourcePlugin[];
|
|
107
|
+
private pluginManager: PluginManager;
|
|
95
108
|
private extensions: any[];
|
|
96
109
|
private B_debug: boolean = false;
|
|
97
110
|
private extractorTimeout: number = 10000;
|
|
98
111
|
private autoCleanup: boolean = true;
|
|
99
112
|
private cleanupTimeout: number = 60000; // 1 minute
|
|
100
113
|
private enableSearchCache: boolean = true;
|
|
114
|
+
private trackMiddlewareFromOptions: TrackMiddleware[] = [];
|
|
115
|
+
private playbackMirrorUnsubscribes = new Map<string, () => void>();
|
|
101
116
|
|
|
102
117
|
private debug(message?: any, ...optionalParams: any[]): void {
|
|
103
118
|
if (this.listenerCount("debug") > 0) {
|
|
@@ -111,17 +126,26 @@ export class PlayerManager extends EventEmitter {
|
|
|
111
126
|
constructor(options: PlayerManagerOptions = {}) {
|
|
112
127
|
super();
|
|
113
128
|
this.plugins = [];
|
|
129
|
+
this.pluginManager = new PluginManager(null as any, this, {
|
|
130
|
+
extractorTimeout: this.extractorTimeout,
|
|
131
|
+
});
|
|
114
132
|
this.searchCache = new Map();
|
|
115
133
|
|
|
116
134
|
// Initialize plugins
|
|
117
135
|
const provided = options.plugins || [];
|
|
118
136
|
for (const p of provided as any[]) {
|
|
119
137
|
try {
|
|
138
|
+
let instance: SourcePlugin | null = null;
|
|
139
|
+
|
|
120
140
|
if (p && typeof p === "object") {
|
|
121
|
-
|
|
141
|
+
instance = p as SourcePlugin;
|
|
122
142
|
} else if (typeof p === "function") {
|
|
123
|
-
|
|
124
|
-
|
|
143
|
+
instance = new (p as any)();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (instance) {
|
|
147
|
+
this.plugins.push(instance);
|
|
148
|
+
this.pluginManager.register(instance);
|
|
125
149
|
}
|
|
126
150
|
this.debug(`Registered plugin: ${p.name || "unnamed"}`);
|
|
127
151
|
} catch (e) {
|
|
@@ -134,6 +158,7 @@ export class PlayerManager extends EventEmitter {
|
|
|
134
158
|
this.autoCleanup = options.autoCleanup ?? true;
|
|
135
159
|
this.cleanupTimeout = options.cleanupInterval ?? 60000;
|
|
136
160
|
this.enableSearchCache = options.enableSearchCache ?? true;
|
|
161
|
+
this.trackMiddlewareFromOptions = normalizeTrackMiddleware(options.trackMiddleware);
|
|
137
162
|
|
|
138
163
|
// Setup auto cleanup
|
|
139
164
|
if (this.autoCleanup) {
|
|
@@ -149,11 +174,6 @@ export class PlayerManager extends EventEmitter {
|
|
|
149
174
|
this.debug(`Initialized with ${this.plugins.length} plugins, ${this.extensions.length} extensions`);
|
|
150
175
|
}
|
|
151
176
|
|
|
152
|
-
private withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
|
|
153
|
-
const timeout = this.extractorTimeout;
|
|
154
|
-
return Promise.race([promise, new Promise<never>((_, reject) => setTimeout(() => reject(new Error(message)), timeout))]);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
177
|
private resolveGuildId(guildOrId: string | { id: string }): string {
|
|
158
178
|
if (typeof guildOrId === "string") return guildOrId;
|
|
159
179
|
if (guildOrId && typeof guildOrId === "object" && "id" in guildOrId) return guildOrId.id;
|
|
@@ -571,12 +591,208 @@ export class PlayerManager extends EventEmitter {
|
|
|
571
591
|
}
|
|
572
592
|
}
|
|
573
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Like {@link broadcast} but awaits every return value (for async methods such as `play`).
|
|
596
|
+
* Uses `Promise.allSettled` — failures are captured per guild, not thrown as a whole.
|
|
597
|
+
*/
|
|
598
|
+
async broadcastAsync(action: string, ...args: any[]): Promise<PromiseSettledResult<unknown>[]> {
|
|
599
|
+
const pending: Promise<unknown>[] = [];
|
|
600
|
+
for (const player of this.players.values()) {
|
|
601
|
+
const fn = (player as any)[action];
|
|
602
|
+
if (typeof fn !== "function") continue;
|
|
603
|
+
try {
|
|
604
|
+
pending.push(Promise.resolve(fn.apply(player, args)));
|
|
605
|
+
} catch (error) {
|
|
606
|
+
pending.push(Promise.reject(error));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return Promise.allSettled(pending);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Broadcast a player method only to the given guild ids (players must already exist).
|
|
614
|
+
*/
|
|
615
|
+
broadcastGuilds(guildIds: readonly string[], action: string, ...args: any[]): void {
|
|
616
|
+
const wanted = new Set(guildIds);
|
|
617
|
+
for (const player of this.players.values()) {
|
|
618
|
+
if (!wanted.has(player.guildId)) continue;
|
|
619
|
+
if (typeof (player as any)[action] === "function") {
|
|
620
|
+
try {
|
|
621
|
+
(player as any)[action](...args);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
this.debug(`Error broadcasting ${action} to ${player.guildId}:`, error);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Global {@link TrackMiddleware} configured on this manager (applied before per-player middleware).
|
|
631
|
+
*/
|
|
632
|
+
getTrackMiddlewareChain(): TrackMiddleware[] {
|
|
633
|
+
return [...this.trackMiddlewareFromOptions];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Mirror playback controls from a leader guild to follower guilds (each guild must already have a {@link Player} via
|
|
638
|
+
* {@link create}). Followers receive the same track on `trackStart`, and pause/resume/stop/volume from the leader when
|
|
639
|
+
* applicable.
|
|
640
|
+
*
|
|
641
|
+
* @returns Unsubscribe function — also runs when the leader player is destroyed.
|
|
642
|
+
*/
|
|
643
|
+
subscribePlaybackMirror(options: PlaybackMirrorOptions): () => void {
|
|
644
|
+
const leader = this.get(options.leaderGuildId);
|
|
645
|
+
if (!leader) {
|
|
646
|
+
throw new Error(`subscribePlaybackMirror: no player for leader guild ${options.leaderGuildId}`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const followers = [...new Set(options.followerGuildIds)].filter((id) => id !== options.leaderGuildId);
|
|
650
|
+
const mirrorUserId = options.mirrorUserId;
|
|
651
|
+
const syncVolume = options.syncVolume ?? true;
|
|
652
|
+
const forwardMode = options.forwardMode ?? true;
|
|
653
|
+
|
|
654
|
+
const existing = this.playbackMirrorUnsubscribes.get(options.leaderGuildId);
|
|
655
|
+
existing?.();
|
|
656
|
+
|
|
657
|
+
const runFollowers = (fn: (p: Player) => void | Promise<void>) => {
|
|
658
|
+
for (const gid of followers) {
|
|
659
|
+
const fp = this.get(gid);
|
|
660
|
+
if (!fp) {
|
|
661
|
+
this.debug(`Playback mirror: no player for follower guild ${gid}`);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
Promise.resolve(fn(fp)).catch((e) => this.debug(`Playback mirror follower error (${gid}):`, e));
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const onTrackStart = async (track: Track) => {
|
|
669
|
+
if (!forwardMode) {
|
|
670
|
+
runFollowers(async (fp) => {
|
|
671
|
+
fp.stop();
|
|
672
|
+
await fp.play(track, mirrorUserId);
|
|
673
|
+
});
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
runFollowers((fp) => {
|
|
678
|
+
if (!fp.connection || !leader.connection) {
|
|
679
|
+
this.debug(`Playback mirror forwardMode: missing connection for follower ${fp.guildId}`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
// sync state
|
|
685
|
+
fp.queue.clear();
|
|
686
|
+
|
|
687
|
+
// optional fake current track sync
|
|
688
|
+
if (track) {
|
|
689
|
+
fp.queue.setCurrentTrack(track);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// subscribe directly to leader player
|
|
693
|
+
fp.subscribeTo(leader);
|
|
694
|
+
fp.isPlaying = leader.isPlaying;
|
|
695
|
+
fp.isPaused = leader.isPaused;
|
|
696
|
+
|
|
697
|
+
fp.emit("trackStart", track);
|
|
698
|
+
this.debug(`Playback mirror forwardMode subscribed ${fp.guildId} -> ${leader.guildId}`);
|
|
699
|
+
} catch (e) {
|
|
700
|
+
this.debug(`Playback mirror forwardMode error (${fp.guildId}):`, e);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const onPause = () => {
|
|
706
|
+
runFollowers((fp) => {
|
|
707
|
+
if (forwardMode) {
|
|
708
|
+
fp.isPaused = true;
|
|
709
|
+
fp.isPlaying = false;
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
fp.pause();
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const onResume = () => {
|
|
718
|
+
runFollowers((fp) => {
|
|
719
|
+
if (forwardMode) {
|
|
720
|
+
fp.isPaused = false;
|
|
721
|
+
fp.isPlaying = true;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
fp.resume();
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const onStop = () => {
|
|
730
|
+
runFollowers((fp) => {
|
|
731
|
+
if (forwardMode) {
|
|
732
|
+
try {
|
|
733
|
+
fp.connection?.subscribe(fp.audioPlayer);
|
|
734
|
+
|
|
735
|
+
fp.isPlaying = false;
|
|
736
|
+
fp.isPaused = false;
|
|
737
|
+
|
|
738
|
+
fp.audioPlayer.stop(true);
|
|
739
|
+
} catch {}
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
fp.stop();
|
|
744
|
+
});
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const onVolume = (_oldVol: number, newVol: number) => {
|
|
748
|
+
if (!syncVolume) return;
|
|
749
|
+
runFollowers((fp) => {
|
|
750
|
+
fp.setVolume(newVol);
|
|
751
|
+
});
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
let closed = false;
|
|
755
|
+
const unsubscribe = () => {
|
|
756
|
+
if (closed) return;
|
|
757
|
+
closed = true;
|
|
758
|
+
leader.off("trackStart", onTrackStart);
|
|
759
|
+
leader.off("playerPause", onPause);
|
|
760
|
+
leader.off("playerResume", onResume);
|
|
761
|
+
leader.off("playerStop", onStop);
|
|
762
|
+
leader.off("volumeChange", onVolume);
|
|
763
|
+
leader.off("playerDestroy", onLeaderDestroy);
|
|
764
|
+
onStop();
|
|
765
|
+
this.playbackMirrorUnsubscribes.delete(options.leaderGuildId);
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
const onLeaderDestroy = () => {
|
|
769
|
+
unsubscribe();
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
leader.on("trackStart", onTrackStart);
|
|
773
|
+
leader.on("playerPause", onPause);
|
|
774
|
+
leader.on("playerResume", onResume);
|
|
775
|
+
leader.on("playerStop", onStop);
|
|
776
|
+
leader.on("volumeChange", onVolume);
|
|
777
|
+
leader.on("playerDestroy", onLeaderDestroy);
|
|
778
|
+
if (leader?.currentTrack) onTrackStart(leader.currentTrack);
|
|
779
|
+
this.playbackMirrorUnsubscribes.set(options.leaderGuildId, unsubscribe);
|
|
780
|
+
return unsubscribe;
|
|
781
|
+
}
|
|
782
|
+
|
|
574
783
|
/**
|
|
575
784
|
* Destroy all players and clean up
|
|
576
785
|
*/
|
|
577
786
|
destroy(): void {
|
|
578
787
|
this.debug(`Destroying all players`);
|
|
579
788
|
|
|
789
|
+
for (const unsub of this.playbackMirrorUnsubscribes.values()) {
|
|
790
|
+
try {
|
|
791
|
+
unsub();
|
|
792
|
+
} catch {}
|
|
793
|
+
}
|
|
794
|
+
this.playbackMirrorUnsubscribes.clear();
|
|
795
|
+
|
|
580
796
|
// Stop cleanup intervals
|
|
581
797
|
if (this.cleanupInterval) {
|
|
582
798
|
clearInterval(this.cleanupInterval);
|
|
@@ -600,30 +816,42 @@ export class PlayerManager extends EventEmitter {
|
|
|
600
816
|
}
|
|
601
817
|
|
|
602
818
|
/**
|
|
603
|
-
* Search using
|
|
819
|
+
* Search using PluginManager without creating a Player.
|
|
604
820
|
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
821
|
+
* Uses the same search pipeline as Player.search():
|
|
822
|
+
* - cache
|
|
823
|
+
* - plugin deduplication
|
|
824
|
+
* - plugin scoring/evaluation
|
|
825
|
+
* - fallback handling
|
|
826
|
+
*
|
|
827
|
+
* @param {string} query
|
|
828
|
+
* @param {string} requestedBy
|
|
829
|
+
* @returns {Promise<SearchResult>}
|
|
608
830
|
*/
|
|
609
831
|
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
610
832
|
this.debug(`Search called with query: ${query}, requestedBy: ${requestedBy}`);
|
|
611
833
|
|
|
612
|
-
//
|
|
834
|
+
// Cache
|
|
613
835
|
const cached = this.getCachedSearch(query);
|
|
614
836
|
if (cached) {
|
|
615
837
|
return cached;
|
|
616
838
|
}
|
|
617
839
|
|
|
618
|
-
const plugin = this.plugins.find((p) => p.canHandle(query));
|
|
619
|
-
if (!plugin) {
|
|
620
|
-
this.debug(`No plugin found to handle: ${query}`);
|
|
621
|
-
throw new Error(`No plugin found to handle: ${query}`);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
840
|
try {
|
|
625
|
-
const result = await this.
|
|
841
|
+
const result = await this.pluginManager.search(query, requestedBy);
|
|
842
|
+
|
|
843
|
+
if (!result || !Array.isArray(result.tracks) || result.tracks.length === 0) {
|
|
844
|
+
throw new Error(`No results found for: ${query}`);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
this.debug(`Plugin search returned ${result.tracks.length} tracks (score: ${result.score?.score ?? "unknown"}%)`);
|
|
848
|
+
|
|
849
|
+
if (result.score) {
|
|
850
|
+
this.debug(`Search evaluation - ${result.score.reason}`);
|
|
851
|
+
}
|
|
852
|
+
|
|
626
853
|
this.setCachedSearch(query, result);
|
|
854
|
+
|
|
627
855
|
return result;
|
|
628
856
|
} catch (error) {
|
|
629
857
|
this.debug(`Search error:`, error);
|
|
@@ -647,9 +875,10 @@ export class PlayerManager extends EventEmitter {
|
|
|
647
875
|
*/
|
|
648
876
|
registerPlugin(plugin: SourcePlugin): void {
|
|
649
877
|
this.plugins.push(plugin);
|
|
878
|
+
this.pluginManager.register(plugin);
|
|
879
|
+
|
|
650
880
|
this.debug(`Registered plugin: ${plugin.name}`);
|
|
651
881
|
|
|
652
|
-
// Register plugin with all existing players
|
|
653
882
|
for (const player of this.players.values()) {
|
|
654
883
|
player.addPlugin(plugin);
|
|
655
884
|
}
|
|
@@ -666,9 +895,10 @@ export class PlayerManager extends EventEmitter {
|
|
|
666
895
|
if (index === -1) return false;
|
|
667
896
|
|
|
668
897
|
this.plugins.splice(index, 1);
|
|
898
|
+
this.pluginManager.unregister(name);
|
|
899
|
+
|
|
669
900
|
this.debug(`Unregistered plugin: ${name}`);
|
|
670
901
|
|
|
671
|
-
// Note: Cannot easily remove plugins from existing players
|
|
672
902
|
return true;
|
|
673
903
|
}
|
|
674
904
|
|
package/src/structures/Queue.ts
CHANGED
|
@@ -400,6 +400,28 @@ export class Queue {
|
|
|
400
400
|
return this.current;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Force set current playing track.
|
|
405
|
+
*
|
|
406
|
+
* Mainly used internally for playback synchronization,
|
|
407
|
+
* playback mirroring, restoring player state,
|
|
408
|
+
* or forward-mode shared audio sessions.
|
|
409
|
+
*
|
|
410
|
+
* This does NOT modify queue order/history automatically.
|
|
411
|
+
* It only updates the current active track reference.
|
|
412
|
+
*
|
|
413
|
+
* @param {Track | null} track - Track to set as current
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* queue.setCurrentTrack(track);
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* queue.setCurrentTrack(null);
|
|
420
|
+
*/
|
|
421
|
+
setCurrentTrack(track: Track | null): void {
|
|
422
|
+
this.current = track;
|
|
423
|
+
}
|
|
424
|
+
|
|
403
425
|
/**
|
|
404
426
|
* Get the previous tracks
|
|
405
427
|
*/
|
package/src/types/index.ts
CHANGED
|
@@ -122,6 +122,43 @@ export interface StreamInfo {
|
|
|
122
122
|
metadata?: Record<string, any>;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** Passed to each {@link TrackMiddleware} run (before stream resolution). */
|
|
126
|
+
export interface TrackMiddlewareContext {
|
|
127
|
+
player: Player;
|
|
128
|
+
manager: PlayerManager;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Runs immediately before stream extraction (`Player.getStream`): after enqueue, before extension `provideStream` and plugins.
|
|
133
|
+
* Prefer mutating `track` in place (especially `metadata`). If you return another object, its fields are merged into the original
|
|
134
|
+
* `track` reference so queue/current-track pointers stay valid.
|
|
135
|
+
*/
|
|
136
|
+
export type TrackMiddleware = (track: Track, context: TrackMiddlewareContext) => void | Track | Promise<void | Track>;
|
|
137
|
+
|
|
138
|
+
/** Options for {@link PlayerManager.subscribePlaybackMirror}. */
|
|
139
|
+
export interface PlaybackMirrorOptions {
|
|
140
|
+
leaderGuildId: string;
|
|
141
|
+
followerGuildIds: string[];
|
|
142
|
+
/** User id passed to follower `play()` (often the bot application id). */
|
|
143
|
+
mirrorUserId: string;
|
|
144
|
+
/** When true (default), follower `setVolume` tracks the leader. */
|
|
145
|
+
syncVolume?: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* When enabled, follower connections subscribe directly
|
|
148
|
+
* to leader.audioPlayer instead of creating their own streams.
|
|
149
|
+
*
|
|
150
|
+
* Greatly reduces bandwidth/CPU usage.
|
|
151
|
+
*
|
|
152
|
+
* Default: true
|
|
153
|
+
*/
|
|
154
|
+
forwardMode?: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function normalizeTrackMiddleware(input?: TrackMiddleware | TrackMiddleware[]): TrackMiddleware[] {
|
|
158
|
+
if (!input) return [];
|
|
159
|
+
return Array.isArray(input) ? input : [input];
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
/**
|
|
126
163
|
* Configuration options for creating a new player instance.
|
|
127
164
|
*
|
|
@@ -302,6 +339,11 @@ export interface PlayerOptions {
|
|
|
302
339
|
*/
|
|
303
340
|
limiterCeiling?: number;
|
|
304
341
|
};
|
|
342
|
+
/**
|
|
343
|
+
* Chain of middleware applied to every track immediately before stream extraction (after queueing).
|
|
344
|
+
* Merged after {@link PlayerManagerOptions.trackMiddleware} from the manager.
|
|
345
|
+
*/
|
|
346
|
+
trackMiddleware?: TrackMiddleware | TrackMiddleware[];
|
|
305
347
|
}
|
|
306
348
|
|
|
307
349
|
export interface PlayerManagerOptions {
|
|
@@ -312,6 +354,10 @@ export interface PlayerManagerOptions {
|
|
|
312
354
|
cleanupInterval?: number; // Cleanup interval in ms
|
|
313
355
|
enableSearchCache?: boolean; // Enable search result caching
|
|
314
356
|
enableStatsCollection?: boolean; // Enable stats collection events
|
|
357
|
+
/**
|
|
358
|
+
* Global track middleware for every {@link Player} created from this manager (before per-player middleware).
|
|
359
|
+
*/
|
|
360
|
+
trackMiddleware?: TrackMiddleware | TrackMiddleware[];
|
|
315
361
|
}
|
|
316
362
|
|
|
317
363
|
/**
|