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