ziplayer 0.3.2 → 0.3.4

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.
Files changed (39) hide show
  1. package/{AI-Guide.md → AGENTS.md} +717 -624
  2. package/README.md +658 -526
  3. package/dist/extensions/BaseExtension.d.ts +10 -1
  4. package/dist/extensions/BaseExtension.d.ts.map +1 -1
  5. package/dist/extensions/BaseExtension.js +27 -1
  6. package/dist/extensions/BaseExtension.js.map +1 -1
  7. package/dist/extensions/index.d.ts.map +1 -1
  8. package/dist/extensions/index.js +24 -6
  9. package/dist/extensions/index.js.map +1 -1
  10. package/dist/plugins/index.d.ts.map +1 -1
  11. package/dist/plugins/index.js +105 -51
  12. package/dist/plugins/index.js.map +1 -1
  13. package/dist/structures/Player.d.ts +90 -15
  14. package/dist/structures/Player.d.ts.map +1 -1
  15. package/dist/structures/Player.js +487 -81
  16. package/dist/structures/Player.js.map +1 -1
  17. package/dist/structures/PlayerManager.d.ts +70 -6
  18. package/dist/structures/PlayerManager.d.ts.map +1 -1
  19. package/dist/structures/PlayerManager.js +184 -19
  20. package/dist/structures/PlayerManager.js.map +1 -1
  21. package/dist/structures/Queue.d.ts +19 -0
  22. package/dist/structures/Queue.d.ts.map +1 -1
  23. package/dist/structures/Queue.js +21 -0
  24. package/dist/structures/Queue.js.map +1 -1
  25. package/dist/types/extension.d.ts +3 -0
  26. package/dist/types/extension.d.ts.map +1 -1
  27. package/dist/types/index.d.ts +69 -2
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/types/index.js +13 -0
  30. package/dist/types/index.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/extensions/BaseExtension.ts +31 -1
  33. package/src/extensions/index.ts +30 -7
  34. package/src/plugins/index.ts +137 -54
  35. package/src/structures/Player.ts +2937 -2457
  36. package/src/structures/PlayerManager.ts +916 -725
  37. package/src/structures/Queue.ts +621 -599
  38. package/src/types/extension.ts +3 -0
  39. package/src/types/index.ts +80 -2
package/README.md CHANGED
@@ -1,526 +1,658 @@
1
- <img width="1175" height="305" alt="logo" src="https://raw.githubusercontent.com/ZiProject/ZiPlayer/refs/heads/main/publish/logo.png" />
2
-
3
- # ZiPlayer
4
-
5
- A powerful, extensible Discord music engine built on top of `@discordjs/voice`, designed for scalability, flexibility, and
6
- developer experience.
7
-
8
- ZiPlayer is not just a player — it's a **full ecosystem** with plugins, extensions, and a modular architecture that lets you build
9
- advanced music bots quickly.
10
-
11
- ---
12
-
13
- ## ✨ Highlights
14
-
15
- - 🔌 **Plugin-driven architecture** — Easily support new audio sources
16
- - 🌐 **Multi-source playback** — YouTube, SoundCloud, Spotify (with fallback), TTS, and more
17
- - 🧠 **Smart fallback system** — Automatically resolves streams across plugins
18
- - 🎛️ **Advanced audio filters** — Real-time FFmpeg effects (bassboost, nightcore, etc.)
19
- - 🔁 **Autoplay & looping** — Seamless listening experience
20
- - 🧩 **Extension system** — Add STT, lyrics, Lavalink, and custom logic
21
- - 🗂️ **Per-guild player system** — Scales across multiple Discord servers
22
- - 📡 **Event-driven core** — Full lifecycle hooks for customization
23
- - 💾 **Custom userdata** — Attach context to each player
24
- - ⚡ **Smart caching** — Search and stream caching for better performance
25
- - 🎯 **Queue management** — Advanced queue operations (move, swap, batch remove)
26
- - 💹 **Preload** - Auto Preload next Track
27
- - 🔃 **Crossfade** - Suport crossfade for new/slip Track
28
- - 🧠 **Transition Engine** - BPM/genre-aware crossfade (chill → long fade, EDM → short fade) with beat-aligned entry instead of
29
- blind time-based fading
30
- - 🔄 **Anti-Stuck Recovery 2.0** - Automatic stream failure recovery: reuse preload → fallback plugin → reduce quality →
31
- controlled skip (no chaotic skipping)
32
- - 🔊 **Loudness Normalization** - LUFS-based normalization prevents sudden volume jumps between tracks, with gentle limiter to
33
- avoid distortion
34
-
35
- ---
36
-
37
- ## 📦 Installation
38
-
39
- ```bash
40
- npm install ziplayer @ziplayer/plugin @ziplayer/extension @ziplayer/infinity @discordjs/voice discord.js opusscript
41
- ```
42
-
43
- ---
44
-
45
- ## 🚀 Quick Start
46
-
47
- ```ts
48
- import { Client, GatewayIntentBits } from "discord.js";
49
- import { PlayerManager } from "ziplayer";
50
- import { YouTubePlugin, SoundCloudPlugin, SpotifyPlugin } from "@ziplayer/plugin";
51
- import { InfinityPlugin } from "@ziplayer/infinity";
52
-
53
- const client = new Client({
54
- intents: [
55
- GatewayIntentBits.Guilds,
56
- GatewayIntentBits.GuildVoiceStates,
57
- GatewayIntentBits.GuildMessages,
58
- GatewayIntentBits.MessageContent,
59
- ],
60
- });
61
-
62
- const manager = new PlayerManager({
63
- plugins: [new YouTubePlugin(), new SoundCloudPlugin(), new SpotifyPlugin(), new InfinityPlugin()],
64
- });
65
-
66
- client.on("messageCreate", async (msg) => {
67
- if (!msg.content.startsWith("!play ") || !msg.guildId) return;
68
-
69
- const voiceChannel = msg.member?.voice?.channel;
70
- if (!voiceChannel) return msg.reply("Join a voice channel first!");
71
-
72
- const player = await manager.create(msg.guildId, {
73
- leaveOnEnd: true,
74
- userdata: { channel: msg.channel },
75
- });
76
-
77
- if (!player.connection) await player.connect(voiceChannel);
78
- await player.play(msg.content.slice(6), msg.author.id);
79
- });
80
-
81
- client.login(process.env.DISCORD_TOKEN);
82
- ```
83
-
84
- ---
85
-
86
- ## 🧱 Architecture Overview
87
-
88
- ```
89
- PlayerManager (global)
90
- └── Player (per guild)
91
- ├── Queue (advanced operations)
92
- ├── PluginManager (with caching & fallback)
93
- ├── ExtensionManager (with priority & caching)
94
- └── FilterManager (FFmpeg filters)
95
- ```
96
-
97
- ### Flow
98
-
99
- ```
100
- create connect play → stream → events → destroy
101
-
102
- auto-save (periodic)
103
-
104
- restore on restart
105
- ```
106
-
107
- ---
108
-
109
- ## 🎵 Core Usage
110
-
111
- ### Play music
112
-
113
- ```ts
114
- await player.play("Never Gonna Give You Up", userId);
115
- await player.play("https://youtube.com/watch?v=...", userId);
116
- await player.play("tts: Hello world", userId);
117
- await player.play(searchResult, userId); // Play from SearchResult
118
- await player.play(null); // Resume from queue
119
- ```
120
-
121
- ### Controls
122
-
123
- ```ts
124
- player.pause();
125
- player.resume();
126
- player.skip();
127
- player.skip(2); // Skip to track at index 2
128
- player.stop();
129
- player.setVolume(100);
130
- player.loop("track"); // Loop current track
131
- player.loop("queue"); // Loop entire queue
132
- player.loop(1); // Number mode: 0=off, 1=track, 2=queue
133
- player.shuffle();
134
- player.seek(30000); // Seek to 30 seconds
135
- player.previous(); // Go back to previous track
136
- ```
137
-
138
- ### Queue Management
139
-
140
- ```ts
141
- // Basic operations
142
- player.queue.add(track);
143
- player.queue.addMultiple([track1, track2]);
144
- player.queue.remove(0);
145
- player.queue.removeMultiple([0, 2, 5]); // Remove multiple indices
146
- player.queue.removeWhere((t) => t.source === "youtube"); // Remove by condition
147
- player.queue.clear();
148
-
149
- // Queue manipulation
150
- player.queue.move(3, 0); // Move track at index 3 to front
151
- player.queue.swap(1, 3); // Swap positions 1 and 3
152
- player.queue.shuffle();
153
-
154
- // Queue inspection
155
- player.queue.size;
156
- player.queue.isEmpty;
157
- player.queue.currentTrack;
158
- player.queue.nextTrack;
159
- player.queue.lastTrack;
160
- player.queue.previousTracks;
161
- player.queue.getTrack(5);
162
- player.queue.findTracks((t) => t.duration > 300000);
163
- player.queue.indexOf(track);
164
- player.queue.has(track);
165
-
166
- // History navigation
167
- player.queue.jumpToHistory(2); // Go back 2 tracks
168
- ```
169
-
170
- ---
171
-
172
- ## 🔌 Plugins
173
-
174
- Install via `@ziplayer/plugin`:
175
-
176
- - **YouTubePlugin** — YouTube + search
177
- - **SoundCloudPlugin** — SoundCloud streaming
178
- - **SpotifyPlugin** — Metadata (uses fallback)
179
- - **TTSPlugin** — Text-to-speech
180
- - **AttachmentsPlugin** — Local/URL audio files
181
-
182
- ### Example
183
-
184
- ```ts
185
- import { TTSPlugin } from "@ziplayer/plugin";
186
-
187
- new PlayerManager({
188
- plugins: [new TTSPlugin({ defaultLang: "en" })],
189
- });
190
- ```
191
-
192
- ### Dynamic Plugin Registration
193
-
194
- ```ts
195
- // Register plugin after initialization
196
- manager.registerPlugin(new YouTubePlugin());
197
-
198
- // Get all registered plugins
199
- const plugins = manager.getPlugins();
200
- ```
201
-
202
- ---
203
-
204
- ## 🧩 Extensions
205
-
206
- Enhance player behavior:
207
-
208
- - 🎤 `voiceExt` — Speech-to-text commands
209
- - 🎤 `lyricsExt` — Auto lyrics (synced support)
210
- - ⚡ `lavalinkExt` — External Lavalink node
211
-
212
- ### Example
213
-
214
- ```ts
215
- import { voiceExt, lyricsExt } from "@ziplayer/extension";
216
-
217
- const manager = new PlayerManager({
218
- extensions: [new voiceExt(null, { lang: "en-US" }), new lyricsExt(null, { provider: "lrclib" })],
219
- });
220
- ```
221
-
222
- ### Extension Capabilities
223
-
224
- Extensions can now provide:
225
-
226
- - **Search** — Custom search handling
227
- - **Stream** — Custom stream sources (Lavalink, etc.)
228
- - **Before/After play hooks** — Modify playback behavior
229
-
230
- ---
231
-
232
- ## 🎛️ Audio Filters
233
-
234
- Apply FFmpeg filters in real-time:
235
-
236
- ```ts
237
- await player.filter.applyFilter("bassboost");
238
- await player.filter.applyFilter("nightcore");
239
- await player.filter.applyFilters(["bassboost", "trebleboost"]); // Multiple filters
240
- await player.filter.getFilterString(); // "bassboost,trebleboost"
241
- await player.filter.clearAll();
242
- ```
243
-
244
- ### Available filters
245
-
246
- - bassboost, trebleboost
247
- - nightcore, lofi, vaporwave
248
- - echo, reverb, chorus
249
- - karaoke
250
- - normalize, compressor, limiter
251
-
252
- ---
253
-
254
- ## 🔊 TTS (Interrupt Mode)
255
-
256
- ```ts
257
- const player = await manager.create(guildId, {
258
- tts: {
259
- createPlayer: true,
260
- interrupt: true,
261
- volume: 100,
262
- maxTimeTts: 60000,
263
- },
264
- });
265
-
266
- await player.play("tts: Hello everyone", userId);
267
- ```
268
-
269
- ---
270
-
271
- ## 📡 Events
272
-
273
- Listen globally via manager:
274
-
275
- ```ts
276
- manager.on("trackStart", (player, track) => {});
277
- manager.on("trackEnd", (player, track) => {});
278
- manager.on("queueEnd", (player) => {});
279
- manager.on("playerError", (player, error, track) => {});
280
- manager.on("playerPause", (player, track) => {});
281
- manager.on("playerResume", (player, track) => {});
282
- manager.on("volumeChange", (player, oldVolume, newVolume) => {});
283
- manager.on("queueAdd", (player, track) => {});
284
- manager.on("queueAddList", (player, tracks) => {});
285
- manager.on("queueRemove", (player, track, index) => {});
286
- manager.on("playerDestroy", (player) => {});
287
- manager.on("ttsStart", (player, payload) => {});
288
- manager.on("ttsEnd", (player) => {});
289
- manager.on("stats", (PlayerStats) => {});
290
- ```
291
-
292
- ---
293
-
294
- ## 🧠 Advanced Features
295
-
296
- ### Autoplay
297
-
298
- ```ts
299
- player.queue.autoPlay(true);
300
- ```
301
-
302
- ### Insert next track
303
-
304
- ```ts
305
- await player.insert("song", 0); // Insert at position 0 (play next)
306
- await player.insert([track1, track2], 2); // Insert multiple at index 2
307
- ```
308
-
309
- ### Save stream to file
310
-
311
- ```ts
312
- const stream = await player.save(track);
313
- stream.pipe(fs.createWriteStream("song.mp3"));
314
-
315
- // Save with filters
316
- const filteredStream = await player.save(track, {
317
- filter: ["bassboost"],
318
- seek: 30000, // Start from 30 seconds
319
- });
320
- ```
321
-
322
- ### Progress Bar
323
-
324
- ```ts
325
- // Default (compact time format)
326
- console.log(player.getProgressBar());
327
- // Output: "1:22:12 ▬▬▬▬▬▬▬▬▬▬🔘▬▬▬▬▬▬▬▬ 1:45:30"
328
-
329
- // Custom options
330
- console.log(
331
- player.getProgressBar({
332
- size: 30,
333
- barChar: "",
334
- progressChar: "",
335
- timeFormat: "full", // "full" or "compact"
336
- showPercentage: true,
337
- }),
338
- );
339
- // Output: "01:22:12 ───────●───────────────────── 01:45:30 (47%)"
340
- ```
341
-
342
- ### Time Formatting
343
-
344
- ```ts
345
- const time = player.getTime();
346
- console.log(time.formatted.current); // "1:22:12" (compact)
347
- console.log(time.format); // "01:22:12" (full with leading zeros)
348
- ```
349
-
350
- ### Batch Operations
351
-
352
- ```ts
353
- // Broadcast action to all players
354
- manager.broadcast("setVolume", 50);
355
- manager.broadcast("pause");
356
-
357
- // Get players by filter
358
- const activePlayers = manager.getPlayersByFilter((p) => p.isPlaying);
359
-
360
- // Delete multiple players
361
- manager.deleteWhere((p) => p.queue.isEmpty && !p.isPlaying);
362
- ```
363
-
364
- ---
365
-
366
- ## ⚙️ Advanced Configuration
367
-
368
- ### PlayerManager Options
369
-
370
- ```ts
371
- const manager = new PlayerManager({
372
- plugins: [...],
373
- extensions: [...],
374
- extractorTimeout: 30000, // Timeout for stream extraction
375
- autoCleanup: true, // Auto cleanup inactive players
376
- cleanupInterval: 120000, // Cleanup interval (ms)
377
- enableSearchCache: true, // Cache search results
378
- enableStatsCollection: true, // Enable stats events
379
- persistence: {...} // Persistence configuration
380
- });
381
- ```
382
-
383
- ### Player Options
384
-
385
- ```ts
386
- const player = await manager.create(guildId, {
387
- volume: 100,
388
- quality: "high",
389
- leaveOnEnd: true,
390
- leaveOnEmpty: true,
391
- leaveTimeout: 100000,
392
- selfDeaf: true,
393
- selfMute: false,
394
- extractorTimeout: 50000,
395
- filters: ["bassboost", "nightcore"],
396
- tts: {
397
- createPlayer: false,
398
- interrupt: true,
399
- volume: 100,
400
- maxTimeTts: 60000,
401
- },
402
- // Runtime profile
403
- lowPerformance: false,
404
- preload: {
405
- enabled: true,
406
- autoDisableInLowPerformance: true,
407
- },
408
- crossfade: {
409
- enabled: undefined, // omit to let autoEnable decide
410
- autoEnable: true,
411
- autoDisableInLowPerformance: true,
412
- durationMs: 5000,
413
- },
414
- smartTransition: {
415
- enabled: true,
416
- genreAware: true,
417
- beatAlign: true,
418
- baseDurationMs: 5000,
419
- minDurationMs: 1200,
420
- maxDurationMs: 8000,
421
- genreDurations: { chill: 7000, edm: 2200 },
422
- beatAlignMaxWaitMs: 1200,
423
- },
424
- antiStuck: {
425
- enabled: true,
426
- maxRetries: 2,
427
- retryDelayMs: 900,
428
- reusePreloadFirst: true,
429
- reduceQualityOnRetry: true,
430
- controlledSkipThreshold: 3,
431
- },
432
- loudnessNormalization: {
433
- enabled: true,
434
- targetLUFS: -14,
435
- maxBoostDb: 8,
436
- maxCutDb: 10,
437
- limiterCeiling: 0.95,
438
- },
439
- userdata: { customField: "value" },
440
- });
441
- ```
442
-
443
- ### Crossfade + Low Performance
444
-
445
- ```ts
446
- // Auto mode: crossfade/preload enabled unless lowPerformance is on
447
- const player = await manager.create(guildId, {
448
- lowPerformance: false,
449
- preload: { enabled: true, autoDisableInLowPerformance: true },
450
- crossfade: { autoEnable: true, autoDisableInLowPerformance: true, durationMs: 4000 },
451
- });
452
-
453
- // Low performance mode: auto disable preload and crossfade
454
- const litePlayer = await manager.create(guildId, {
455
- lowPerformance: true,
456
- preload: { enabled: true, autoDisableInLowPerformance: true }, // resolved: disabled
457
- crossfade: { autoEnable: true, autoDisableInLowPerformance: true }, // resolved: disabled
458
- });
459
- ```
460
-
461
- > Crossfade is applied when switching to the next track and when calling `player.skip()`. Smart transition adapts fade by
462
- > `metadata.genre` and can align to beat using `metadata.bpm`. Loudness normalization uses `metadata.lufs` when available and
463
- > applies a limiter ceiling.
464
-
465
- ---
466
-
467
- ## 📊 Monitoring & Stats
468
-
469
- ```ts
470
- // Get manager statistics
471
- const stats = manager.getStats();
472
- console.log({
473
- totalPlayers: stats.totalPlayers,
474
- activePlayers: stats.activePlayers,
475
- pausedPlayers: stats.pausedPlayers,
476
- connectedPlayers: stats.connectedPlayers,
477
- totalTracksInQueue: stats.totalTracksInQueue,
478
- });
479
-
480
- // Get plugin/extension stats
481
- console.log(manager.getConfig());
482
- console.log(player.pluginManager.getStats());
483
- console.log(player.extensionManager.getStats());
484
-
485
- // Clear caches
486
- player.clearSearchCache();
487
- player.extensionManager.clearCache("search");
488
- ```
489
-
490
- ---
491
-
492
- ## ⚠️ Best Practices
493
-
494
- - Use **one PlayerManager** per bot
495
- - Always `await player.connect()` before playing
496
- - Handle `playerError` events
497
- - Do not reuse a destroyed player
498
- - Enable **persistence** for production bots to survive restarts
499
- - Use **autoCleanup** to prevent memory leaks
500
- - Set appropriate **extractorTimeout** based on your network (default: 10-50 seconds)
501
-
502
- ---
503
-
504
- ## 🌟 Migration Guide
505
-
506
- ### From v1.x to v2.x
507
-
508
- - `player.getTime()` now returns `{ current, total, format, formatted }`
509
- - `player.getProgressBar()` supports new options
510
- - `player.queue.remove(index)` removed track is now returned
511
- - New `queue.removeMultiple()`, `queue.move()`, `queue.swap()` methods
512
- - Extension hooks now support async properly
513
-
514
- ---
515
-
516
- ## 📚 Resources
517
-
518
- - Examples: [https://github.com/ZiProject/ZiPlayer/tree/main/examples](https://github.com/ZiProject/ZiPlayer/tree/main/examples)
519
- - GitHub: [https://github.com/ZiProject/ZiPlayer](https://github.com/ZiProject/ZiPlayer)
520
- - npm: [https://www.npmjs.com/package/ziplayer](https://www.npmjs.com/package/ziplayer)
521
-
522
- ---
523
-
524
- ## 📄 License
525
-
526
- MIT License
1
+ <img width="1175" height="305" alt="logo" src="https://raw.githubusercontent.com/ZiProject/ZiPlayer/refs/heads/main/publish/logo.png" />
2
+
3
+ # ZiPlayer
4
+
5
+ A powerful, extensible Discord music engine built on top of `@discordjs/voice`, designed for scalability, flexibility, and
6
+ developer experience.
7
+
8
+ ZiPlayer is not just a player — it's a **full ecosystem** with plugins, extensions, and a modular architecture that lets you build
9
+ advanced music bots quickly.
10
+
11
+ ---
12
+
13
+ ## ✨ Highlights
14
+
15
+ - 🔌 **Plugin-driven architecture** — Easily support new audio sources
16
+ - 🌐 **Multi-source playback** — YouTube, SoundCloud, Spotify (with fallback), TTS, and more
17
+ - 🧠 **Smart fallback system** — Automatically resolves streams across plugins
18
+ - 🎛️ **Advanced audio filters** — Real-time FFmpeg effects (bassboost, nightcore, etc.)
19
+ - 🔁 **Autoplay & looping** — Seamless listening experience
20
+ - 🧩 **Extension system** — Add STT, lyrics, Lavalink, and custom logic
21
+ - 🗂️ **Per-guild player system** — Scales across multiple Discord servers
22
+ - 📡 **Event-driven core** — Full lifecycle hooks for customization
23
+ - 💾 **Custom userdata** — Attach context to each player
24
+ - ⚡ **Smart caching** — Search and stream caching for better performance
25
+ - 🎯 **Queue management** — Advanced queue operations (move, swap, batch remove)
26
+ - 💹 **Preload** - Auto Preload next Track
27
+ - 🔃 **Crossfade** - Suport crossfade for new/slip Track
28
+ - 🧠 **Transition Engine** - BPM/genre-aware crossfade (chill → long fade, EDM → short fade) with beat-aligned entry instead of
29
+ blind time-based fading
30
+ - 🔄 **Anti-Stuck Recovery 2.0** - Automatic stream failure recovery: reuse preload → fallback plugin → reduce quality →
31
+ controlled skip (no chaotic skipping)
32
+ - 🔊 **Loudness Normalization** - LUFS-based normalization prevents sudden volume jumps between tracks, with gentle limiter to
33
+ avoid distortion
34
+ - 🧪 **Track middleware (extensions)** — Transform or enrich tracks before streaming (for example fill `metadata.bpm`,
35
+ `metadata.lufs`, `metadata.genre` from an audio-analysis HTTP API instead of manual entry)
36
+ - 📻 **Multi-guild broadcast** — Fan out the same Player API calls to every active guild with `manager.broadcast()` (shared
37
+ controls / mirrored sessions across servers)
38
+ - 🎛️ **Playback Mirror / Forward Mode** - "forward mode", where the follower player directly subscribes to the leader player's
39
+ instead of creating its own stream.
40
+
41
+ ---
42
+
43
+ ## 📦 Installation
44
+
45
+ ```bash
46
+ npm install ziplayer @ziplayer/plugin @ziplayer/extension @ziplayer/infinity @discordjs/voice discord.js opusscript
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 🚀 Quick Start
52
+
53
+ ```ts
54
+ import { Client, GatewayIntentBits } from "discord.js";
55
+ import { PlayerManager } from "ziplayer";
56
+ import { YouTubePlugin, SoundCloudPlugin, SpotifyPlugin } from "@ziplayer/plugin";
57
+ import { InfinityPlugin } from "@ziplayer/infinity";
58
+
59
+ const client = new Client({
60
+ intents: [
61
+ GatewayIntentBits.Guilds,
62
+ GatewayIntentBits.GuildVoiceStates,
63
+ GatewayIntentBits.GuildMessages,
64
+ GatewayIntentBits.MessageContent,
65
+ ],
66
+ });
67
+
68
+ const manager = new PlayerManager({
69
+ plugins: [new YouTubePlugin(), new SoundCloudPlugin(), new SpotifyPlugin(), new InfinityPlugin()],
70
+ });
71
+
72
+ client.on("messageCreate", async (msg) => {
73
+ if (!msg.content.startsWith("!play ") || !msg.guildId) return;
74
+
75
+ const voiceChannel = msg.member?.voice?.channel;
76
+ if (!voiceChannel) return msg.reply("Join a voice channel first!");
77
+
78
+ const player = await manager.create(msg.guildId, {
79
+ leaveOnEnd: true,
80
+ userdata: { channel: msg.channel },
81
+ });
82
+
83
+ if (!player.connection) await player.connect(voiceChannel);
84
+ await player.play(msg.content.slice(6), msg.author.id);
85
+ });
86
+
87
+ client.login(process.env.DISCORD_TOKEN);
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 🧱 Architecture Overview
93
+
94
+ ```
95
+ PlayerManager (global)
96
+ └── Player (per guild)
97
+ ├── Queue (advanced operations)
98
+ ├── PluginManager (with caching & fallback)
99
+ ├── ExtensionManager (with priority & caching)
100
+ ├── StreamManager (Store & Manage streams)
101
+ ├── PreloadManager (Preload next tracks)
102
+ └── FilterManager (FFmpeg filters)
103
+
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 🎵 Core Usage
109
+
110
+ ### Play music
111
+
112
+ ```ts
113
+ await player.play("Never Gonna Give You Up", userId);
114
+ await player.play("https://youtube.com/watch?v=...", userId);
115
+ await player.play("tts: Hello world", userId);
116
+ await player.play(searchResult, userId); // Play from SearchResult
117
+ await player.play(null); // Resume from queue
118
+ ```
119
+
120
+ ### Controls
121
+
122
+ ```ts
123
+ player.pause();
124
+ player.resume();
125
+ player.skip();
126
+ player.skip(2); // Skip to track at index 2
127
+ player.stop();
128
+ player.setVolume(100);
129
+ player.loop("track"); // Loop current track
130
+ player.loop("queue"); // Loop entire queue
131
+ player.loop(1); // Number mode: 0=off, 1=track, 2=queue
132
+ player.shuffle();
133
+ player.seek(30000); // Seek to 30 seconds
134
+ player.previous(); // Go back to previous track
135
+ ```
136
+
137
+ ### Queue Management
138
+
139
+ ```ts
140
+ // Basic operations
141
+ player.queue.add(track);
142
+ player.queue.addMultiple([track1, track2]);
143
+ player.queue.remove(0);
144
+ player.queue.removeMultiple([0, 2, 5]); // Remove multiple indices
145
+ player.queue.removeWhere((t) => t.source === "youtube"); // Remove by condition
146
+ player.queue.clear();
147
+
148
+ // Queue manipulation
149
+ player.queue.move(3, 0); // Move track at index 3 to front
150
+ player.queue.swap(1, 3); // Swap positions 1 and 3
151
+ player.queue.shuffle();
152
+
153
+ // Queue inspection
154
+ player.queue.size;
155
+ player.queue.isEmpty;
156
+ player.queue.currentTrack;
157
+ player.queue.nextTrack;
158
+ player.queue.lastTrack;
159
+ player.queue.previousTracks;
160
+ player.queue.getTrack(5);
161
+ player.queue.findTracks((t) => t.duration > 300000);
162
+ player.queue.indexOf(track);
163
+ player.queue.has(track);
164
+
165
+ // History navigation
166
+ player.queue.jumpToHistory(2); // Go back 2 tracks
167
+ ```
168
+
169
+ ---
170
+
171
+ ## 🔌 Plugins
172
+
173
+ Install via `@ziplayer/plugin`:
174
+
175
+ - **YouTubePlugin** — YouTube + search
176
+ - **SoundCloudPlugin** — SoundCloud streaming
177
+ - **SpotifyPlugin** — Metadata (uses fallback)
178
+ - **TTSPlugin** — Text-to-speech
179
+ - **AttachmentsPlugin** — Local/URL audio files
180
+
181
+ ### Example
182
+
183
+ ```ts
184
+ import { TTSPlugin } from "@ziplayer/plugin";
185
+
186
+ new PlayerManager({
187
+ plugins: [new TTSPlugin({ defaultLang: "en" })],
188
+ });
189
+ ```
190
+
191
+ ### Dynamic Plugin Registration
192
+
193
+ ```ts
194
+ // Register plugin after initialization
195
+ manager.registerPlugin(new YouTubePlugin());
196
+
197
+ // Get all registered plugins
198
+ const plugins = manager.getPlugins();
199
+ ```
200
+
201
+ ---
202
+
203
+ ## 🧩 Extensions
204
+
205
+ Enhance player behavior:
206
+
207
+ - 🎤 `voiceExt` — Speech-to-text commands
208
+ - 🎤 `lyricsExt` — Auto lyrics (synced support)
209
+ - `lavalinkExt` — External Lavalink node
210
+
211
+ ### Example
212
+
213
+ ```ts
214
+ import { voiceExt, lyricsExt } from "@ziplayer/extension";
215
+
216
+ const manager = new PlayerManager({
217
+ extensions: [new voiceExt(null, { lang: "en-US" }), new lyricsExt(null, { provider: "lrclib" })],
218
+ });
219
+ ```
220
+
221
+ ### Extension Capabilities
222
+
223
+ Extensions can now provide:
224
+
225
+ - **Search** — Custom search handling
226
+ - **Stream** — Custom stream sources (Lavalink, etc.)
227
+ - **Before/After play hooks** — Modify playback behavior
228
+
229
+ ### Track middleware (metadata before stream)
230
+
231
+ Core exposes **`trackMiddleware`** on **`PlayerManager`** options and **`Player`** options: an ordered chain of async/sync
232
+ functions `(track, { player, manager }) => void | Track`. They run **once per stream resolution**, immediately before extension
233
+ `provideStream` and plugins — including preload and `player.save()`.
234
+
235
+ Prefer mutating **`track.metadata`** in place. If you return a **new** object, its enumerable fields (and merged `metadata`) are
236
+ copied onto the original track reference so queue/current-track pointers stay stable.
237
+
238
+ ```ts
239
+ const manager = new PlayerManager({
240
+ plugins: [...],
241
+ trackMiddleware: async (track, { player }) => {
242
+ const analysis = await fetchAnalysis(track.url); // your HTTP API
243
+ track.metadata = {
244
+ ...track.metadata,
245
+ bpm: analysis.bpm,
246
+ lufs: analysis.lufs,
247
+ genre: analysis.genre,
248
+ };
249
+ },
250
+ });
251
+
252
+ // Per-player middleware runs after manager-level middleware
253
+ await manager.create(guildId, {
254
+ trackMiddleware: [(track) => {
255
+ track.metadata = { ...track.metadata, sourcePreset: "guild-radio" };
256
+ }],
257
+ });
258
+ ```
259
+
260
+ Extensions remain useful for **`beforePlay`** (rewrite query / inject tracks before search) and **`provideStream`** (custom
261
+ backends):
262
+
263
+ 1. **`beforePlay`** (capability `beforePlay`) runs inside `player.play()` before search resolution. You can:
264
+ - Adjust `payload.query` when it is a string (rewrite query) or a **`Track`** (mutate the object, including `track.metadata`).
265
+ - Return **`tracks`** to inject or replace the list of tracks (with enriched metadata).
266
+ - Set **`handled: true`** to short-circuit normal handling when you fully control the outcome.
267
+
268
+ 2. **`provideStream`** (capability `stream`) runs **after** track middleware and **before** plugin extraction in
269
+ `Player.getStream()`. Use it to supply a stream from Lavalink or another backend while still using plugins for search.
270
+
271
+ Core features read optional **`Track.metadata`** fields:
272
+
273
+ | Key (in `track.metadata`) | Used by |
274
+ | ------------------------- | -------------------------------------------------------------------------- |
275
+ | `bpm` | Smart transition beat alignment (`smartTransition.beatAlign`) |
276
+ | `genre` | Genre-aware fade duration (`smartTransition.genreAware`, `genreDurations`) |
277
+ | `lufs` | Loudness normalization (`loudnessNormalization`) |
278
+
279
+ Example sketch (extension path): in `beforePlay`, if `payload.query` is a `Track`, call your analysis service (or cache), then
280
+ assign `track.metadata = { ...track.metadata, bpm, lufs, genre }` before returning.
281
+
282
+ ---
283
+
284
+ ## 🎛️ Audio Filters
285
+
286
+ Apply FFmpeg filters in real-time:
287
+
288
+ ```ts
289
+ await player.filter.applyFilter("bassboost");
290
+ await player.filter.applyFilter("nightcore");
291
+ await player.filter.applyFilters(["bassboost", "trebleboost"]); // Multiple filters
292
+ await player.filter.getFilterString(); // "bassboost,trebleboost"
293
+ await player.filter.clearAll();
294
+ ```
295
+
296
+ ### Available filters
297
+
298
+ - bassboost, trebleboost
299
+ - nightcore, lofi, vaporwave
300
+ - echo, reverb, chorus
301
+ - karaoke
302
+ - normalize, compressor, limiter
303
+
304
+ ---
305
+
306
+ ## 🔊 TTS (Interrupt Mode)
307
+
308
+ ```ts
309
+ const player = await manager.create(guildId, {
310
+ tts: {
311
+ createPlayer: true,
312
+ interrupt: true,
313
+ volume: 100,
314
+ maxTimeTts: 60000,
315
+ },
316
+ });
317
+
318
+ await player.play("tts: Hello everyone", userId);
319
+ ```
320
+
321
+ ---
322
+
323
+ ## 📡 Events
324
+
325
+ Listen globally via manager:
326
+
327
+ ```ts
328
+ manager.on("trackStart", (player, track) => {});
329
+ manager.on("trackEnd", (player, track) => {});
330
+ manager.on("queueEnd", (player) => {});
331
+ manager.on("playerError", (player, error, track) => {});
332
+ manager.on("playerPause", (player, track) => {});
333
+ manager.on("playerResume", (player, track) => {});
334
+ manager.on("volumeChange", (player, oldVolume, newVolume) => {});
335
+ manager.on("queueAdd", (player, track) => {});
336
+ manager.on("queueAddList", (player, tracks) => {});
337
+ manager.on("queueRemove", (player, track, index) => {});
338
+ manager.on("playerDestroy", (player) => {});
339
+ manager.on("ttsStart", (player, payload) => {});
340
+ manager.on("ttsEnd", (player) => {});
341
+ manager.on("stats", (PlayerStats) => {});
342
+ manager.on("forwardModeStart", (player, leader) => {});
343
+ manager.on("forwardModeEnd", (player, leader) => {});
344
+ ```
345
+
346
+ ---
347
+
348
+ ## 🧠 Advanced Features
349
+
350
+ ### Autoplay
351
+
352
+ ```ts
353
+ player.queue.autoPlay(true);
354
+ ```
355
+
356
+ ### Insert next track
357
+
358
+ ```ts
359
+ await player.insert("song", 0); // Insert at position 0 (play next)
360
+ await player.insert([track1, track2], 2); // Insert multiple at index 2
361
+ ```
362
+
363
+ ### Save stream to file
364
+
365
+ ```ts
366
+ const stream = await player.save(track);
367
+ stream.pipe(fs.createWriteStream("song.mp3"));
368
+
369
+ // Save with filters
370
+ const filteredStream = await player.save(track, {
371
+ filter: ["bassboost"],
372
+ seek: 30000, // Start from 30 seconds
373
+ });
374
+ ```
375
+
376
+ ### Progress Bar
377
+
378
+ ```ts
379
+ // Default (compact time format)
380
+ console.log(player.getProgressBar());
381
+ // Output: "1:22:12 ▬▬▬▬▬▬▬▬▬▬🔘▬▬▬▬▬▬▬▬ 1:45:30"
382
+
383
+ // Custom options
384
+ console.log(
385
+ player.getProgressBar({
386
+ size: 30,
387
+ barChar: "─",
388
+ progressChar: "",
389
+ timeFormat: "full", // "full" or "compact"
390
+ showPercentage: true,
391
+ }),
392
+ );
393
+ // Output: "01:22:12 ───────●───────────────────── 01:45:30 (47%)"
394
+ ```
395
+
396
+ ### Time Formatting
397
+
398
+ ```ts
399
+ const time = player.getTime();
400
+ console.log(time.formatted.current); // "1:22:12" (compact)
401
+ console.log(time.format); // "01:22:12" (full with leading zeros)
402
+ ```
403
+
404
+ ### Batch Operations
405
+
406
+ ```ts
407
+ // Broadcast action to all players
408
+ manager.broadcast("setVolume", 50);
409
+ manager.broadcast("pause");
410
+
411
+ // Get players by filter
412
+ const activePlayers = manager.getPlayersByFilter((p) => p.isPlaying);
413
+
414
+ // Delete multiple players
415
+ manager.deleteWhere((p) => p.queue.isEmpty && !p.isPlaying);
416
+ ```
417
+
418
+ ### Multi-room / multi-guild broadcast
419
+
420
+ `PlayerManager.broadcast(action, ...args)` loops every registered **`Player`** and, if `player[action]` is a function, calls
421
+ `player[action](...args)`. It is a **control fan-out**: the same method name runs on all guild players (pause, volume, skip,etc.).
422
+ It does **not** multiplex one Discord voice stream to many guilds—each guild still has its own voice connection and decoder.
423
+
424
+ Use **`broadcastAsync`** when you need to await async methods (for example `play`):
425
+
426
+ ```ts
427
+ const results = await manager.broadcastAsync("play", "https://youtu.be/...", botUserId);
428
+ ```
429
+
430
+ Use **`broadcastGuilds`** to target a subset of guild ids:
431
+
432
+ ```ts
433
+ manager.broadcastGuilds(["guildA", "guildB"], "pause");
434
+ ```
435
+
436
+ **“Subscribe” pattern (manual):**
437
+
438
+ 1. Call `await manager.create(guildId, options)` (and `player.connect(voiceChannel)`) for **each** guild that should participate
439
+ so each server has a player instance.
440
+ 2. Drive playback from your bot logic: mirror API above, or issue the same `play` / queue commands per guild, or use `broadcast`
441
+ for **synchronized controls** only.
442
+ 3. Plain `broadcast` is **synchronous** and does not `await` async methods. Prefer `broadcastAsync` or a `for` loop with `await`
443
+ when order/errors matter.
444
+
445
+ ```ts
446
+ // Same control on every guild that already has a player
447
+ manager.broadcast("pause");
448
+ manager.broadcast("setVolume", 75);
449
+
450
+ // Prefer explicit awaits if you need ordered or error-handled play on many guilds
451
+ for (const player of manager.getAll()) {
452
+ await player.play(sharedQueueUrl, botUserId).catch(console.error);
453
+ }
454
+ ```
455
+
456
+ ### Playback Mirror / Forward Mode
457
+
458
+ Ziplayer supports built-in multi-guild playback mirroring using shared audio forwarding. A leader player streams audio normally,
459
+ while followers directly subscribe to the leader's internal audioPlayer.
460
+
461
+ This allows multiple guilds to hear the exact same playback while using only:
462
+
463
+ - one stream
464
+ - one decoder
465
+ - one extractor pipeline
466
+
467
+ Resulting in extremely low CPU and bandwidth usage.
468
+
469
+ ```ts
470
+ const stopMirror = manager.subscribeForwardMirror({
471
+ leaderGuildId: "123",
472
+ followerGuildIds: ["456", "789"],
473
+ syncVolume: true,
474
+ });
475
+
476
+ // later
477
+ stopMirror();
478
+ ```
479
+
480
+ **Direct Player Subscription:**
481
+
482
+ Followers may also subscribe manually:
483
+
484
+ ````ts
485
+ const leader = manager.get("123");
486
+ const follower = manager.get("456");
487
+
488
+ follower.subscribeTo(leader);
489
+ //Unsubscribe:
490
+ //follower.unsubscribeForward();
491
+ ```
492
+
493
+ ---
494
+
495
+ ## ⚙️ Advanced Configuration
496
+
497
+ ### PlayerManager Options
498
+
499
+ ```ts
500
+ const manager = new PlayerManager({
501
+ plugins: [...],
502
+ extensions: [...],
503
+ extractorTimeout: 30000, // Timeout for stream extraction
504
+ autoCleanup: true, // Auto cleanup inactive players
505
+ cleanupInterval: 120000, // Cleanup interval (ms)
506
+ enableSearchCache: true, // Cache search results
507
+ enableStatsCollection: true, // Enable stats events
508
+ trackMiddleware: [...], // Global pre-stream track transforms (before per-player middleware)
509
+ persistence: {...} // Persistence configuration
510
+ });
511
+ ````
512
+
513
+ ### Player Options
514
+
515
+ ```ts
516
+ const player = await manager.create(guildId, {
517
+ volume: 100,
518
+ quality: "high",
519
+ leaveOnEnd: true,
520
+ leaveOnEmpty: true,
521
+ leaveTimeout: 100000,
522
+ selfDeaf: true,
523
+ selfMute: false,
524
+ extractorTimeout: 50000,
525
+ filters: ["bassboost", "nightcore"],
526
+ tts: {
527
+ createPlayer: false,
528
+ interrupt: true,
529
+ volume: 100,
530
+ maxTimeTts: 60000,
531
+ },
532
+ // Runtime profile
533
+ lowPerformance: false,
534
+ preload: {
535
+ enabled: true,
536
+ autoDisableInLowPerformance: true,
537
+ },
538
+ crossfade: {
539
+ enabled: undefined, // omit to let autoEnable decide
540
+ autoEnable: true,
541
+ autoDisableInLowPerformance: true,
542
+ durationMs: 5000,
543
+ },
544
+ smartTransition: {
545
+ enabled: true,
546
+ genreAware: true,
547
+ beatAlign: true,
548
+ baseDurationMs: 5000,
549
+ minDurationMs: 1200,
550
+ maxDurationMs: 8000,
551
+ genreDurations: { chill: 7000, edm: 2200 },
552
+ beatAlignMaxWaitMs: 1200,
553
+ },
554
+ antiStuck: {
555
+ enabled: true,
556
+ maxRetries: 2,
557
+ retryDelayMs: 900,
558
+ reusePreloadFirst: true,
559
+ reduceQualityOnRetry: true,
560
+ controlledSkipThreshold: 3,
561
+ },
562
+ loudnessNormalization: {
563
+ enabled: true,
564
+ targetLUFS: -14,
565
+ maxBoostDb: 8,
566
+ maxCutDb: 10,
567
+ limiterCeiling: 0.95,
568
+ },
569
+ trackMiddleware: [], // Optional per-player chain (after manager trackMiddleware)
570
+ userdata: { customField: "value" },
571
+ });
572
+ ```
573
+
574
+ ### Crossfade + Low Performance
575
+
576
+ ```ts
577
+ // Auto mode: crossfade/preload enabled unless lowPerformance is on
578
+ const player = await manager.create(guildId, {
579
+ lowPerformance: false,
580
+ preload: { enabled: true, autoDisableInLowPerformance: true },
581
+ crossfade: { autoEnable: true, autoDisableInLowPerformance: true, durationMs: 4000 },
582
+ });
583
+
584
+ // Low performance mode: auto disable preload and crossfade
585
+ const litePlayer = await manager.create(guildId, {
586
+ lowPerformance: true,
587
+ preload: { enabled: true, autoDisableInLowPerformance: true }, // resolved: disabled
588
+ crossfade: { autoEnable: true, autoDisableInLowPerformance: true }, // resolved: disabled
589
+ });
590
+ ```
591
+
592
+ > Crossfade is applied when switching to the next track and when calling `player.skip()`. Smart transition adapts fade by
593
+ > `metadata.genre` and can align to beat using `metadata.bpm`. Loudness normalization uses `metadata.lufs` when available and
594
+ > applies a limiter ceiling.
595
+
596
+ ---
597
+
598
+ ## 📊 Monitoring & Stats
599
+
600
+ ```ts
601
+ // Get manager statistics
602
+ const stats = manager.getStats();
603
+ console.log({
604
+ totalPlayers: stats.totalPlayers,
605
+ activePlayers: stats.activePlayers,
606
+ pausedPlayers: stats.pausedPlayers,
607
+ connectedPlayers: stats.connectedPlayers,
608
+ totalTracksInQueue: stats.totalTracksInQueue,
609
+ });
610
+
611
+ // Get plugin/extension stats
612
+ console.log(manager.getConfig());
613
+ console.log(player.pluginManager.getStats());
614
+ console.log(player.extensionManager.getStats());
615
+
616
+ // Clear caches
617
+ player.clearSearchCache();
618
+ player.extensionManager.clearCache("search");
619
+ ```
620
+
621
+ ---
622
+
623
+ ## ⚠️ Best Practices
624
+
625
+ - Use **one PlayerManager** per bot
626
+ - Always `await player.connect()` before playing
627
+ - Handle `playerError` events
628
+ - Do not reuse a destroyed player
629
+ - Enable **persistence** for production bots to survive restarts
630
+ - Use **autoCleanup** to prevent memory leaks
631
+ - Set appropriate **extractorTimeout** based on your network (default: 10-50 seconds)
632
+
633
+ ---
634
+
635
+ ## 🌟 Migration Guide
636
+
637
+ ### From v1.x to v2.x
638
+
639
+ - `player.getTime()` now returns `{ current, total, format, formatted }`
640
+ - `player.getProgressBar()` supports new options
641
+ - `player.queue.remove(index)` removed track is now returned
642
+ - New `queue.removeMultiple()`, `queue.move()`, `queue.swap()` methods
643
+ - Extension hooks now support async properly
644
+
645
+ ---
646
+
647
+ ## 📚 Resources
648
+
649
+ - Examples: [https://github.com/ZiProject/ZiPlayer/tree/main/examples](https://github.com/ZiProject/ZiPlayer/tree/main/examples)
650
+ - GitHub: [https://github.com/ZiProject/ZiPlayer](https://github.com/ZiProject/ZiPlayer)
651
+ - npm: [https://www.npmjs.com/package/ziplayer](https://www.npmjs.com/package/ziplayer)
652
+ - AI/agent-oriented notes (middleware metadata, broadcast semantics): see `AGENTS.md` in this repo
653
+
654
+ ---
655
+
656
+ ## 📄 License
657
+
658
+ MIT License