ziplayer 0.3.3 → 0.3.5

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 (40) 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 +443 -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/structures/StreamManager.d.ts +1 -0
  22. package/dist/structures/StreamManager.d.ts.map +1 -1
  23. package/dist/structures/StreamManager.js +1 -0
  24. package/dist/structures/StreamManager.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 +38 -11
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/types/index.js +7 -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 +136 -53
  35. package/src/structures/Player.ts +2940 -2544
  36. package/src/structures/PlayerManager.ts +916 -955
  37. package/src/structures/Queue.ts +621 -621
  38. package/src/structures/StreamManager.ts +2 -0
  39. package/src/types/extension.ts +3 -0
  40. package/src/types/index.ts +43 -11
package/AGENTS.md CHANGED
@@ -1,653 +1,717 @@
1
- # AI / agent notes — ZiPlayer core
2
-
3
- A comprehensive guide for AI assistants and developers working with ZiPlayer - a powerful Discord music player library.
4
-
5
- ## 📋 Table of Contents
6
-
7
- 1. [Project Overview](#project-overview)
8
- 2. [Architecture](#architecture)
9
- 3. [Core Concepts](#core-concepts)
10
- 4. [API Reference](#api-reference)
11
- 5. [Common Patterns](#common-patterns)
12
- 6. [Troubleshooting](#troubleshooting)
13
- 7. [Code Examples](#code-examples)
14
-
15
- ---
16
-
17
- ## 🎯 Project Overview
18
-
19
- **ZiPlayer** is an extensible Discord music engine built on `@discordjs/voice`.
20
-
21
- ### Key Features
22
-
23
- - Plugin-driven architecture (YouTube, SoundCloud, Spotify, TTS)
24
- - Extension system (voice commands, lyrics, Lavalink)
25
- - Audio filters (bassboost, nightcore, etc.)
26
- - Smart caching and fallback system
27
-
28
- ### Tech Stack
29
-
30
- - TypeScript
31
- - `@discordjs/voice` for audio
32
- - FFmpeg for audio processing
33
- - Node.js EventEmitter for events
34
-
35
- ---
36
-
37
- ### Component Responsibilities
38
-
39
- | Component | Responsibility |
40
- | ------------------ | -------------------------------------------------- |
41
- | `PlayerManager` | Creates/manages players, global event bus |
42
- | `Player` | Per-guild audio playback, controls, event emission |
43
- | `Queue` | Track management, loop modes, history, auto-play |
44
- | `PluginManager` | Audio source resolution, streaming, fallback logic |
45
- | `ExtensionManager` | Custom hooks (search, stream, before/after play) |
46
- | `FilterManager` | FFmpeg audio effects |
47
- | `StreamManager` | Centralized stream management/Auto cleanup |
48
-
49
- ---
50
-
51
- ## 🧠 Core Concepts
52
-
53
- ### 1. Player Lifecycle
54
-
55
- ```typescript
56
- // Create → Connect → Play → (Auto-save) → Destroy
57
- const player = await manager.create(guildId, options);
58
- await player.connect(voiceChannel);
59
- await player.play(query, userId);
60
- player.destroy();
61
- ```
62
-
63
- ### 2. Queue Loop Modes
64
-
65
- ```typescript
66
- player.loop("off"); // No loop (default)
67
- player.loop("track"); // Repeat current track
68
- player.loop("queue"); // Repeat entire queue
69
- ```
70
-
71
- ### 3. Event Flow
72
-
73
- ```
74
- trackStart playing trackEnd → playNext → (loop/autoplay)
75
-
76
- queueEnd → leave
77
- ```
78
-
79
- ### 4. Plugin Priority & Fallback
80
-
81
- ```typescript
82
- // Plugins are tried in priority order (higher = first)
83
- // If primary fails, fallback plugins are attempted sequentially
84
- // Failed plugins don't block the queue
85
-
86
- plugin.priority = 10; // Higher priority
87
- ```
88
-
89
- ### 5. Caching Strategy
90
-
91
- | Cache Type | TTL | Purpose |
92
- | --------------- | ------- | --------------------------- |
93
- | Search cache | 2 min | Avoid duplicate API calls |
94
- | Stream cache | 5 min | Cache resolved streams |
95
- | Extension cache | 1-5 min | Extension operation results |
96
-
97
- ---
98
-
99
- ## 📚 API Reference
100
-
101
- ### PlayerManager
102
-
103
- #### Constructor Options
104
-
105
- ```typescript
106
- interface PlayerManagerOptions {
107
- plugins?: SourcePlugin[]; // Audio source plugins
108
- extensions?: BaseExtension[]; // Custom extensions
109
- extractorTimeout?: number; // Default: 10000ms
110
- autoCleanup?: boolean; // Default: true
111
- cleanupInterval?: number; // Default: 60000ms
112
- enableSearchCache?: boolean; // Default: true
113
- enableStatsCollection?: boolean; // Default: false
114
- }
115
- ```
116
-
117
- #### Player Runtime Options (Performance Profile)
118
-
119
- ```typescript
120
- interface PlayerOptions {
121
- lowPerformance?: boolean; // Default: false (or true when quality === "low")
122
- preload?: {
123
- enabled?: boolean; // Default: true
124
- autoDisableInLowPerformance?: boolean; // Default: true
125
- };
126
- crossfade?: {
127
- enabled?: boolean; // Explicit on/off
128
- autoEnable?: boolean; // Default: true when enabled is undefined
129
- autoDisableInLowPerformance?: boolean; // Default: true
130
- durationMs?: number; // Default: 5000
131
- };
132
- smartTransition?: {
133
- enabled?: boolean;
134
- genreAware?: boolean;
135
- beatAlign?: boolean;
136
- baseDurationMs?: number;
137
- minDurationMs?: number;
138
- maxDurationMs?: number;
139
- genreDurations?: Record<string, number>;
140
- beatAlignMaxWaitMs?: number;
141
- };
142
- antiStuck?: {
143
- enabled?: boolean;
144
- maxRetries?: number;
145
- retryDelayMs?: number;
146
- reusePreloadFirst?: boolean;
147
- reduceQualityOnRetry?: boolean;
148
- controlledSkipThreshold?: number;
149
- };
150
- loudnessNormalization?: {
151
- enabled?: boolean;
152
- targetLUFS?: number;
153
- maxBoostDb?: number;
154
- maxCutDb?: number;
155
- limiterCeiling?: number;
156
- };
157
- }
158
- ```
159
-
160
- - If `lowPerformance=true`, preload and crossfade are auto-disabled by default.
161
- - `crossfade.autoEnable=true` allows crossfade to be enabled automatically when `crossfade.enabled` is not explicitly set.
162
- - You can still force behavior by setting `enabled` flags directly.
163
- - Runtime behavior: crossfade is used for next-track transitions and `skip()`.
164
- - Smart transition can tune fade by `metadata.genre` and beat-align by `metadata.bpm`.
165
- - Loudness normalization uses `metadata.lufs` with limiter ceiling protection.
166
- - Anti-stuck retries in-place before controlled skip to avoid skip storms.
167
-
168
- #### Key Methods
169
-
170
- | Method | Description |
171
- | ---------------------------- | -------------------------- |
172
- | `create(guildId, options)` | Create new player |
173
- | `get(guildId)` | Get existing player |
174
- | `delete(guildId)` | Destroy and remove player |
175
- | `getAll()` | Get all players |
176
- | `broadcast(action, ...args)` | Send action to all players |
177
-
178
- ### Player
179
-
180
- #### Core Methods
181
-
182
- | Method | Description | Returns |
183
- | ------------------------------ | ----------------------- | ------------------- |
184
- | `play(query, userId)` | Play track/search/queue | `Promise<boolean>` |
185
- | `pause()` | Pause current | `boolean` |
186
- | `resume()` | Resume playback | `boolean` |
187
- | `skip(index?)` | Skip to next/index | `boolean` |
188
- | `stop()` | Stop and clear queue | `boolean` |
189
- | `seek(position)` | Seek to position (ms) | `Promise<boolean>` |
190
- | `previous()` | Play previous track | `Promise<boolean>` |
191
- | `setVolume(vol)` | Set volume (0-200) | `boolean` |
192
- | `loop(mode)` | Set loop mode | `LoopMode` |
193
- | `shuffle()` | Shuffle queue | `void` |
194
- | `insert(query, index, userId)` | Insert at position | `Promise<boolean>` |
195
- | `save(track, options)` | Save track to stream | `Promise<Readable>` |
196
-
197
- #### Getters
198
-
199
- ```typescript
200
- player.currentTrack; // Track | null
201
- player.queueSize; // number
202
- player.isPlaying; // boolean
203
- player.isPaused; // boolean
204
- player.volume; // number
205
- player.upcomingTracks; // Track[]
206
- player.previousTracks; // Track[]
207
- player.relatedTracks; // Track[] | null
208
- ```
209
-
210
- ### Queue
211
-
212
- #### Methods
213
-
214
- | Method | Description |
215
- | ------------------------- | -------------------- |
216
- | `add(track)` | Add single track |
217
- | `addMultiple(tracks)` | Add multiple tracks |
218
- | `insert(track, index)` | Insert at position |
219
- | `remove(index)` | Remove at index |
220
- | `removeMultiple(indices)` | Remove multiple |
221
- | `removeWhere(predicate)` | Remove by condition |
222
- | `move(from, to)` | Move track |
223
- | `swap(a, b)` | Swap tracks |
224
- | `shuffle()` | Randomize order |
225
- | `clear()` | Clear all tracks |
226
- | `loop(mode)` | Set loop mode |
227
- | `autoPlay(enabled)` | Enable/disable |
228
- | `previous()` | Get previous track |
229
- | `jumpToHistory(steps)` | Jump back in history |
230
-
231
- #### Properties
232
-
233
- ```typescript
234
- queue.size; // number
235
- queue.isEmpty; // boolean
236
- queue.currentTrack; // Track | null
237
- queue.nextTrack; // Track | null
238
- queue.lastTrack; // Track | null
239
- queue.previousTracks; // Track[]
240
- ```
241
-
242
- ### FilterManager
243
-
244
- ```typescript
245
- // Apply filters
246
- await player.filter.applyFilter("bassboost");
247
- await player.filter.applyFilters(["bassboost", "nightcore"]);
248
-
249
- // Available filters
250
- // bassboost, trebleboost, nightcore, lofi, vaporwave,
251
- // echo, reverb, chorus, karaoke, normalize, compressor, limiter
252
-
253
- // Clear filters
254
- await player.filter.clearAll();
255
- await player.filter.clear("bassboost");
256
-
257
- // Get current filters
258
- const filterString = player.filter.getFilterString(); // "bassboost,nightcore"
259
- ```
260
-
261
- ## 🔧 Common Patterns
262
-
263
- ### 1. Basic Music Bot Setup
264
-
265
- ```typescript
266
- import { Client, GatewayIntentBits } from "discord.js";
267
- import { PlayerManager } from "ziplayer";
268
- import { YouTubePlugin, SpotifyPlugin } from "@ziplayer/plugin";
269
-
270
- const client = new Client({
271
- intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
272
- });
273
-
274
- const manager = new PlayerManager({
275
- plugins: [new YouTubePlugin(), new SpotifyPlugin()],
276
- autoCleanup: true,
277
- });
278
-
279
- client.on("messageCreate", async (msg) => {
280
- if (msg.content.startsWith("!play")) {
281
- const player = await manager.create(msg.guildId);
282
- const voiceChannel = msg.member?.voice.channel;
283
-
284
- if (!player.connection) {
285
- await player.connect(voiceChannel);
286
- }
287
-
288
- const query = msg.content.slice(6);
289
- await player.play(query, msg.author.id);
290
- }
291
- });
292
-
293
- const player = await manager.create(guildId, {
294
- lowPerformance: false,
295
- preload: { enabled: true, autoDisableInLowPerformance: true },
296
- crossfade: { autoEnable: true, autoDisableInLowPerformance: true, durationMs: 5000 },
297
- smartTransition: {
298
- enabled: true,
299
- genreAware: true,
300
- beatAlign: true,
301
- baseDurationMs: 5000,
302
- genreDurations: { chill: 7000, edm: 2200 },
303
- },
304
- antiStuck: {
305
- enabled: true,
306
- maxRetries: 2,
307
- retryDelayMs: 900,
308
- reusePreloadFirst: true,
309
- reduceQualityOnRetry: true,
310
- controlledSkipThreshold: 3,
311
- },
312
- loudnessNormalization: {
313
- enabled: true,
314
- targetLUFS: -14,
315
- limiterCeiling: 0.95,
316
- },
317
- });
318
- ```
319
-
320
- ### 2. Progress Bar with Time Format
321
-
322
- ```typescript
323
- // Get formatted time
324
- const time = player.getTime();
325
- console.log(`Current: ${time.formatted.current}`); // "1:22:12"
326
- console.log(`Total: ${time.formatted.total}`); // "3:45:30"
327
-
328
- // Progress bar
329
- const progressBar = player.getProgressBar({
330
- size: 20,
331
- barChar: "▬",
332
- progressChar: "🔘",
333
- timeFormat: "compact",
334
- showPercentage: true,
335
- });
336
- // Output: "1:22:12 ▬▬▬▬▬▬▬▬▬🔘▬▬▬▬▬▬▬▬ 3:45:30 (36%)"
337
- ```
338
-
339
- ### 3. Queue Management Commands
340
-
341
- ```typescript
342
- // Skip to specific track
343
- await player.skip(3); // Skip to index 3
344
-
345
- // Move track to front
346
- player.queue.move(5, 0);
347
-
348
- // Remove all tracks from specific source
349
- player.queue.removeWhere((t) => t.source === "soundcloud");
350
-
351
- // Jump back 2 tracks
352
- await player.queue.jumpToHistory(2);
353
-
354
- // Insert as next track
355
- await player.insert("song name", 0, userId);
356
- ```
357
-
358
- ### 4. Custom Plugin Implementation
359
-
360
- ```typescript
361
- import { BasePlugin, Track, StreamInfo } from "ziplayer";
362
-
363
- class CustomPlugin extends BasePlugin {
364
- name = "CustomPlugin";
365
- priority = 5;
366
-
367
- canHandle(query: string): boolean {
368
- return query.startsWith("custom:");
369
- }
370
-
371
- async search(query: string, requestedBy: string): Promise<SearchResult> {
372
- // Implementation
373
- return { tracks: [] };
374
- }
375
-
376
- async getStream(track: Track, signal: AbortSignal): Promise<StreamInfo> {
377
- // Return audio stream
378
- return { stream: readableStream, type: "arbitrary" };
379
- }
380
-
381
- async getRelatedTracks(track: Track): Promise<Track[]> {
382
- // Return recommendations
383
- return [];
384
- }
385
- }
386
- ```
387
-
388
- ### 5. Custom Extension Implementation
389
-
390
- ```typescript
391
- import { BaseExtension, ExtensionContext } from "ziplayer";
392
-
393
- class LoggerExtension extends BaseExtension {
394
- name = "Logger";
395
- priority = 10;
396
-
397
- async beforePlay(context: ExtensionContext, request: any) {
398
- console.log(`Playing: ${request.query}`);
399
- return { handled: false };
400
- }
401
-
402
- async afterPlay(context: ExtensionContext, payload: any) {
403
- if (payload.success) {
404
- console.log(`Successfully played ${payload.tracks?.length} tracks`);
405
- }
406
- }
407
- }
408
- ```
409
-
410
- ### 6. Event Handling
411
-
412
- ```typescript
413
- manager.on("trackStart", (player, track) => {
414
- console.log(`Now playing: ${track.title}`);
415
- });
416
-
417
- manager.on("queueEnd", (player) => {
418
- console.log("Queue finished!");
419
- });
420
-
421
- manager.on("playerError", (player, error, track) => {
422
- console.error(`Error on ${track?.title}:`, error.message);
423
- });
424
-
425
- manager.on("playerSaved", (guildId) => {
426
- console.log(`Saved state for guild ${guildId}`);
427
- });
428
-
429
- manager.on("stats", (stats) => {
430
- console.log(`Active players: ${stats.activePlayers}`);
431
- });
432
- ```
433
-
434
- ---
435
-
436
- ## 🐛 Troubleshooting
437
-
438
- ### Common Issues
439
-
440
- | Issue | Solution |
441
- | ------------------------ | ----------------------------------------------------------- |
442
- | **No audio** | Check `player.connection` exists, voice channel permissions |
443
- | **Plugin not working** | Verify `canHandle()` returns true, check priority |
444
- | **Filters not applying** | Call `refreshPlayerResource(true)` after applying |
445
- | **Memory leak** | Enable `autoCleanup`, call `player.destroy()` when done |
446
- | **Rate limiting** | Use search cache, increase `extractorTimeout` |
447
-
448
- ### Debug Mode
449
-
450
- ```typescript
451
- // Enable debug logging
452
- manager.on("debug", (message) => {
453
- console.log("[DEBUG]", message);
454
- });
455
-
456
- // Or check debug flag
457
- if (manager.debugEnabled) {
458
- // Debug-specific logic
459
- }
460
- ```
461
-
462
- ### Performance Tips
463
-
464
- 1. **Enable caching** for search and stream results
465
- 2. **Set appropriate timeouts** based on network conditions
466
- 3. **Batch operations** when modifying queue
467
- 4. **Destroy players** when no longer needed
468
-
469
- ---
470
-
471
- ## 📝 Code Examples
472
-
473
- ### Full Bot Example
474
-
475
- ```typescript
476
- import { Client, GatewayIntentBits, EmbedBuilder } from "discord.js";
477
- import { PlayerManager } from "ziplayer";
478
- import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
479
-
480
- const client = new Client({
481
- intents: [
482
- GatewayIntentBits.Guilds,
483
- GatewayIntentBits.GuildVoiceStates,
484
- GatewayIntentBits.GuildMessages,
485
- GatewayIntentBits.MessageContent,
486
- ],
487
- });
488
-
489
- const manager = new PlayerManager({
490
- plugins: [new YouTubePlugin(), new SpotifyPlugin(), new TTSPlugin()],
491
- autoCleanup: true,
492
- extractorTimeout: 30000,
493
- });
494
-
495
- client.on("messageCreate", async (msg) => {
496
- if (!msg.guildId || msg.author.bot) return;
497
-
498
- const args = msg.content.slice(1).split(" ");
499
- const command = args[0].toLowerCase();
500
- const query = args.slice(1).join(" ");
501
-
502
- const player = await manager.create(msg.guildId);
503
- const voiceChannel = msg.member?.voice.channel;
504
-
505
- switch (command) {
506
- case "play":
507
- if (!voiceChannel) return msg.reply("Join a voice channel!");
508
- if (!player.connection) await player.connect(voiceChannel);
509
- await player.play(query, msg.author.id);
510
- break;
511
-
512
- case "pause":
513
- player.pause();
514
- break;
515
-
516
- case "resume":
517
- player.resume();
518
- break;
519
-
520
- case "skip":
521
- player.skip();
522
- break;
523
-
524
- case "stop":
525
- player.stop();
526
- break;
527
-
528
- case "volume":
529
- const vol = parseInt(query);
530
- if (isNaN(vol)) return msg.reply("Volume must be a number!");
531
- player.setVolume(vol);
532
- break;
533
-
534
- case "queue":
535
- const tracks = player.upcomingTracks.slice(0, 10);
536
- const embed = new EmbedBuilder()
537
- .setTitle("Queue")
538
- .setDescription(tracks.map((t, i) => `${i + 1}. ${t.title}`).join("\n") || "Empty");
539
- msg.reply({ embeds: [embed] });
540
- break;
541
-
542
- case "nowplaying":
543
- const track = player.currentTrack;
544
- if (!track) return msg.reply("Nothing playing!");
545
-
546
- const progress = player.getProgressBar({ size: 15 });
547
- const time = player.getTime();
548
-
549
- const embed = new EmbedBuilder()
550
- .setTitle(track.title)
551
- .setURL(track.url)
552
- .setThumbnail(track.thumbnail)
553
- .setDescription(`\`${progress}\`\n${time.formatted.current} / ${time.formatted.total}`);
554
- msg.reply({ embeds: [embed] });
555
- break;
556
- }
557
- });
558
-
559
- client.login(process.env.DISCORD_TOKEN);
560
- ```
561
-
562
- ---
563
-
564
- ## 🔗 Quick Reference
565
-
566
- ### Import Paths
567
-
568
- ```typescript
569
- // Core
570
- import { PlayerManager, Player, Queue } from "ziplayer";
571
-
572
- // Types
573
- import type { Track, SearchResult, LoopMode, StreamInfo } from "ziplayer";
574
-
575
- // Plugins (external package)
576
- import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
577
-
578
- // infinity plugin support stream audio from YouTube, TikTok, Instagram, Twitter/X, SoundCloud, Reddit, Twitch, Bilibili, and 1000+ other sites
579
-
580
- import { InfinityPlugin } from "@ziplayer/infinity";
581
-
582
- // Extensions (external package)
583
- import { voiceExt, lyricsExt, lavalinkExt } from "@ziplayer/extension";
584
- ```
585
-
586
- ### Type Definitions
587
-
588
- ```typescript
589
- interface Track {
590
- id: string;
591
- title: string;
592
- url: string;
593
- source: string;
594
- duration: number;
595
- thumbnail?: string;
596
- requestedBy?: string;
597
- isLive?: boolean;
598
- }
599
-
600
- type LoopMode = "off" | "track" | "queue";
601
-
602
- interface SearchResult {
603
- tracks: Track[];
604
- playlist?: { name: string; url?: string };
605
- }
606
- ```
607
-
608
- ## Terminology
609
-
610
- - **Plugins** (`SourcePlugin`): search, `getStream`, playlists audio **sources** (YouTube, SoundCloud, etc.).
611
- - **Extensions** (`SourceExtension` / `BaseExtension`): cross-cutting behavior — **not** the same as **`trackMiddleware`**.
612
-
613
- ## Track transforms before stream
614
-
615
- | Goal | Mechanism |
616
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
617
- | Enrich every queued track right before stream extraction (preload, playback, `save`, TTS interrupt path uses middleware before raw plugin stream where applicable) | **`trackMiddleware`** on `PlayerManagerOptions` / `PlayerOptions` — ordered chain; merged order: manager chain first, then per-player chain. Implemented in `Player.applyTrackMiddleware` → called at start of `Player.getStream`, and before direct `pluginManager.getStream` in TTS interrupt / `save`. |
618
- | Change query or inject tracks before search resolves | Extension **`beforePlay`** — mutate `payload.query` or return `{ tracks }`. |
619
- | Custom stream backend | Extension **`provideStream`** — runs **after** track middleware, before plugins. |
620
-
621
- Types: `TrackMiddleware`, `TrackMiddlewareContext`, `normalizeTrackMiddleware` in `src/types/index.ts`.
622
-
623
- When returning a **new** `Track` from middleware, the core **merges** into the original reference (`mergeTrackPreserveRef`) so
624
- queue pointers stay valid.
625
-
626
- ## Metadata: BPM, LUFS, genre
627
-
628
- Advanced playback reads **`track.metadata`**:
629
-
630
- - **`bpm`**, **`genre`**, **`lufs`** — smart transition + loudness (see `Player`).
631
-
632
- ## Multi-guild broadcast & mirror
633
-
634
- | API | Behavior |
635
- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
636
- | `broadcast(action, ...args)` | Sync fan-out of `player[action]` to **all** players. |
637
- | `broadcastAsync(action, ...args)` | Same, but `Promise.allSettled` on return values (use for `play`). |
638
- | `broadcastGuilds(guildIds, action, ...args)` | Subset of guilds. |
639
- | `subscribePlaybackMirror({ leaderGuildId, followerGuildIds, mirrorUserId, syncVolume? })` | Leader `trackStart` → followers `stop()` + `play(track, mirrorUserId)`; mirrors pause/resume/stop/volume (volume optional). Unsubscribe returned; also cleans up when leader player destroys. |
640
-
641
- Followers must already exist (`create`). One mirror subscription per leader id replaces the previous.
642
-
643
- ---
644
-
645
- ## 📖 Additional Resources
646
-
647
- - [GitHub Repository](https://github.com/ZiProject/ZiPlayer)
648
- - [npm Package](https://www.npmjs.com/package/ziplayer)
649
- - [Examples Folder](https://github.com/ZiProject/ZiPlayer/tree/main/examples)
650
-
651
- ---
652
-
653
- _This guide is maintained for AI assistants and developers. For questions or contributions, please open an issue on GitHub._
1
+ # AI / agent notes — ZiPlayer core
2
+
3
+ A comprehensive guide for AI assistants and developers working with ZiPlayer - a powerful Discord music player library.
4
+
5
+ ## 📋 Table of Contents
6
+
7
+ 1. [Project Overview](#project-overview)
8
+ 2. [Architecture](#architecture)
9
+ 3. [Core Concepts](#core-concepts)
10
+ 4. [API Reference](#api-reference)
11
+ 5. [Common Patterns](#common-patterns)
12
+ 6. [Troubleshooting](#troubleshooting)
13
+ 7. [Code Examples](#code-examples)
14
+
15
+ ---
16
+
17
+ ## 🎯 Project Overview
18
+
19
+ **ZiPlayer** is an extensible Discord music engine built on `@discordjs/voice`.
20
+
21
+ ### Key Features
22
+
23
+ - Plugin-driven architecture (YouTube, SoundCloud, Spotify, TTS)
24
+ - Extension system (voice commands, lyrics, Lavalink)
25
+ - Audio filters (bassboost, nightcore, etc.)
26
+ - Smart caching and fallback system
27
+
28
+ ### Tech Stack
29
+
30
+ - TypeScript
31
+ - `@discordjs/voice` for audio
32
+ - FFmpeg for audio processing
33
+ - Node.js EventEmitter for events
34
+
35
+ ## 📦 Installation
36
+
37
+ ```bash
38
+ npm install ziplayer @ziplayer/plugin @ziplayer/extension @ziplayer/infinity @discordjs/voice discord.js opusscript
39
+ ```
40
+
41
+ ---
42
+
43
+ ### Component Responsibilities
44
+
45
+ | Component | Responsibility |
46
+ | ------------------ | -------------------------------------------------- |
47
+ | `PlayerManager` | Creates/manages players, global event bus |
48
+ | `Player` | Per-guild audio playback, controls, event emission |
49
+ | `Queue` | Track management, loop modes, history, auto-play |
50
+ | `PluginManager` | Audio source resolution, streaming, fallback logic |
51
+ | `ExtensionManager` | Custom hooks (search, stream, before/after play) |
52
+ | `FilterManager` | FFmpeg audio effects |
53
+ | `StreamManager` | Centralized stream management/Auto cleanup |
54
+
55
+ ---
56
+
57
+ ## 🧠 Core Concepts
58
+
59
+ ### 1. Player Lifecycle
60
+
61
+ ```typescript
62
+ // Create → Connect → Play → Destroy
63
+ const player = await manager.create(guildId, options);
64
+ await player.connect(voiceChannel);
65
+ await player.play(query, userId);
66
+ player.destroy();
67
+ ```
68
+
69
+ ### 2. Queue Loop Modes
70
+
71
+ ```typescript
72
+ player.loop("off"); // No loop (default)
73
+ player.loop("track"); // Repeat current track
74
+ player.loop("queue"); // Repeat entire queue
75
+ ```
76
+
77
+ ### 3. Event Flow
78
+
79
+ ```
80
+ trackStart → playing → trackEnd → playNext → (loop/autoplay)
81
+
82
+ queueEnd leave
83
+ ```
84
+
85
+ ### 4. Plugin Priority & Fallback
86
+
87
+ ```typescript
88
+ // Plugins are tried in priority order (higher = first)
89
+ // If primary fails, fallback plugins are attempted sequentially
90
+ // Failed plugins don't block the queue
91
+
92
+ plugin.priority = 10; // Higher priority
93
+ ```
94
+
95
+ ### 5. Caching Strategy
96
+
97
+ | Cache Type | TTL | Purpose |
98
+ | --------------- | ------- | --------------------------- |
99
+ | Search cache | 2 min | Avoid duplicate API calls |
100
+ | Stream cache | 5 min | Cache resolved streams |
101
+ | Extension cache | 1-5 min | Extension operation results |
102
+
103
+ ---
104
+
105
+ ## 📚 API Reference
106
+
107
+ ### PlayerManager
108
+
109
+ #### Constructor Options
110
+
111
+ ```typescript
112
+ interface PlayerManagerOptions {
113
+ plugins?: SourcePlugin[]; // Audio source plugins
114
+ extensions?: BaseExtension[]; // Custom extensions
115
+ extractorTimeout?: number; // Default: 10000ms
116
+ autoCleanup?: boolean; // Default: true
117
+ cleanupInterval?: number; // Default: 60000ms
118
+ enableSearchCache?: boolean; // Default: true
119
+ enableStatsCollection?: boolean; // Default: false
120
+ }
121
+ ```
122
+
123
+ #### Player Runtime Options (Performance Profile)
124
+
125
+ ```typescript
126
+ interface PlayerOptions {
127
+ lowPerformance?: boolean; // Default: false (or true when quality === "low")
128
+ preload?: {
129
+ enabled?: boolean; // Default: true
130
+ autoDisableInLowPerformance?: boolean; // Default: true
131
+ };
132
+ crossfade?: {
133
+ enabled?: boolean; // Explicit on/off
134
+ autoEnable?: boolean; // Default: true when enabled is undefined
135
+ autoDisableInLowPerformance?: boolean; // Default: true
136
+ durationMs?: number; // Default: 5000
137
+ };
138
+ smartTransition?: {
139
+ enabled?: boolean;
140
+ genreAware?: boolean;
141
+ beatAlign?: boolean;
142
+ baseDurationMs?: number;
143
+ minDurationMs?: number;
144
+ maxDurationMs?: number;
145
+ genreDurations?: Record<string, number>;
146
+ beatAlignMaxWaitMs?: number;
147
+ };
148
+ antiStuck?: {
149
+ enabled?: boolean;
150
+ maxRetries?: number;
151
+ retryDelayMs?: number;
152
+ reusePreloadFirst?: boolean;
153
+ reduceQualityOnRetry?: boolean;
154
+ controlledSkipThreshold?: number;
155
+ };
156
+ loudnessNormalization?: {
157
+ enabled?: boolean;
158
+ targetLUFS?: number;
159
+ maxBoostDb?: number;
160
+ maxCutDb?: number;
161
+ limiterCeiling?: number;
162
+ };
163
+ }
164
+ ```
165
+
166
+ - If `lowPerformance=true`, preload and crossfade are auto-disabled by default.
167
+ - `crossfade.autoEnable=true` allows crossfade to be enabled automatically when `crossfade.enabled` is not explicitly set.
168
+ - You can still force behavior by setting `enabled` flags directly.
169
+ - Runtime behavior: crossfade is used for next-track transitions and `skip()`.
170
+ - Smart transition can tune fade by `metadata.genre` and beat-align by `metadata.bpm`.
171
+ - Loudness normalization uses `metadata.lufs` with limiter ceiling protection.
172
+ - Anti-stuck retries in-place before controlled skip to avoid skip storms.
173
+
174
+ #### Key Methods
175
+
176
+ | Method | Description |
177
+ | ---------------------------- | -------------------------- |
178
+ | `create(guildId, options)` | Create new player |
179
+ | `get(guildId)` | Get existing player |
180
+ | `delete(guildId)` | Destroy and remove player |
181
+ | `getAll()` | Get all players |
182
+ | `broadcast(action, ...args)` | Send action to all players |
183
+
184
+ ### Player
185
+
186
+ #### Core Methods
187
+
188
+ | Method | Description | Returns |
189
+ | ------------------------------ | ---------------------------------------------------------- | ------------------- |
190
+ | `play(query, userId)` | Play track/search/queue | `Promise<boolean>` |
191
+ | `pause()` | Pause current | `boolean` |
192
+ | `resume()` | Resume playback | `boolean` |
193
+ | `skip(index?)` | Skip to next/index | `boolean` |
194
+ | `stop()` | Stop and clear queue | `boolean` |
195
+ | `seek(position)` | Seek to position (ms) | `Promise<boolean>` |
196
+ | `previous()` | Play previous track | `Promise<boolean>` |
197
+ | `setVolume(vol)` | Set volume (0-200) | `boolean` |
198
+ | `loop(mode)` | Set loop mode | `LoopMode` |
199
+ | `shuffle()` | Shuffle queue | `void` |
200
+ | `insert(query, index, userId)` | Insert at position | `Promise<boolean>` |
201
+ | `save(track, options)` | Save track to stream | `Promise<Readable>` |
202
+ | `subscribeTo(leader, options)` | Subscribe this player to another player's playback stream. | `boolean` |
203
+ | `unsubscribeForward()` | Unsubscribe this player from its current playback leader. | `boolean` |
204
+
205
+ #### Getters
206
+
207
+ ```typescript
208
+ player.currentTrack; // Track | null
209
+ player.queueSize; // number
210
+ player.isPlaying; // boolean
211
+ player.isPaused; // boolean
212
+ player.volume; // number
213
+ player.upcomingTracks; // Track[]
214
+ player.previousTracks; // Track[]
215
+ player.relatedTracks; // Track[] | null
216
+ ```
217
+
218
+ ### Queue
219
+
220
+ #### Methods
221
+
222
+ | Method | Description |
223
+ | ------------------------- | -------------------- |
224
+ | `add(track)` | Add single track |
225
+ | `addMultiple(tracks)` | Add multiple tracks |
226
+ | `insert(track, index)` | Insert at position |
227
+ | `remove(index)` | Remove at index |
228
+ | `removeMultiple(indices)` | Remove multiple |
229
+ | `removeWhere(predicate)` | Remove by condition |
230
+ | `move(from, to)` | Move track |
231
+ | `swap(a, b)` | Swap tracks |
232
+ | `shuffle()` | Randomize order |
233
+ | `clear()` | Clear all tracks |
234
+ | `loop(mode)` | Set loop mode |
235
+ | `autoPlay(enabled)` | Enable/disable |
236
+ | `previous()` | Get previous track |
237
+ | `jumpToHistory(steps)` | Jump back in history |
238
+
239
+ #### Properties
240
+
241
+ ```typescript
242
+ queue.size; // number
243
+ queue.isEmpty; // boolean
244
+ queue.currentTrack; // Track | null
245
+ queue.nextTrack; // Track | null
246
+ queue.lastTrack; // Track | null
247
+ queue.previousTracks; // Track[]
248
+ ```
249
+
250
+ ### FilterManager
251
+
252
+ ```typescript
253
+ // Apply filters
254
+ await player.filter.applyFilter("bassboost");
255
+ await player.filter.applyFilters(["bassboost", "nightcore"]);
256
+
257
+ // Available filters
258
+ // bassboost, trebleboost, nightcore, lofi, vaporwave,
259
+ // echo, reverb, chorus, karaoke, normalize, compressor, limiter
260
+
261
+ // Clear filters
262
+ await player.filter.clearAll();
263
+ await player.filter.clear("bassboost");
264
+
265
+ // Get current filters
266
+ const filterString = player.filter.getFilterString(); // "bassboost,nightcore"
267
+ ```
268
+
269
+ ## 🔧 Common Patterns
270
+
271
+ ### 1. Basic Music Bot Setup
272
+
273
+ ```typescript
274
+ import { Client, GatewayIntentBits } from "discord.js";
275
+ import { PlayerManager } from "ziplayer";
276
+ import { YouTubePlugin, SpotifyPlugin } from "@ziplayer/plugin";
277
+
278
+ const client = new Client({
279
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
280
+ });
281
+
282
+ const manager = new PlayerManager({
283
+ plugins: [new YouTubePlugin({}), new SpotifyPlugin()],
284
+ autoCleanup: true,
285
+ });
286
+
287
+ client.on("messageCreate", async (msg) => {
288
+ if (msg.content.startsWith("!play")) {
289
+ const player = await manager.create(msg.guildId);
290
+ const voiceChannel = msg.member?.voice.channel;
291
+
292
+ if (!player.connection) {
293
+ await player.connect(voiceChannel);
294
+ }
295
+
296
+ const query = msg.content.slice(6);
297
+ await player.play(query, msg.author.id);
298
+ }
299
+ });
300
+
301
+ const player = await manager.create(guildId, {
302
+ lowPerformance: false,
303
+ preload: { enabled: true, autoDisableInLowPerformance: true },
304
+ crossfade: { autoEnable: true, autoDisableInLowPerformance: true, durationMs: 5000 },
305
+ smartTransition: {
306
+ enabled: true,
307
+ genreAware: true,
308
+ beatAlign: true,
309
+ baseDurationMs: 5000,
310
+ genreDurations: { chill: 7000, edm: 2200 },
311
+ },
312
+ antiStuck: {
313
+ enabled: true,
314
+ maxRetries: 2,
315
+ retryDelayMs: 900,
316
+ reusePreloadFirst: true,
317
+ reduceQualityOnRetry: true,
318
+ controlledSkipThreshold: 3,
319
+ },
320
+ loudnessNormalization: {
321
+ enabled: true,
322
+ targetLUFS: -14,
323
+ limiterCeiling: 0.95,
324
+ },
325
+ });
326
+ ```
327
+
328
+ ### 2. Progress Bar with Time Format
329
+
330
+ ```typescript
331
+ // Get formatted time
332
+ const time = player.getTime();
333
+ console.log(`Current: ${time.formatted.current}`); // "1:22:12"
334
+ console.log(`Total: ${time.formatted.total}`); // "3:45:30"
335
+
336
+ // Progress bar
337
+ const progressBar = player.getProgressBar({
338
+ size: 20,
339
+ barChar: "▬",
340
+ progressChar: "🔘",
341
+ timeFormat: "compact",
342
+ showPercentage: true,
343
+ });
344
+ // Output: "1:22:12 ▬▬▬▬▬▬▬▬▬🔘▬▬▬▬▬▬▬▬ 3:45:30 (36%)"
345
+ ```
346
+
347
+ ### 3. Queue Management Commands
348
+
349
+ ```typescript
350
+ // Skip to specific track
351
+ await player.skip(3); // Skip to index 3
352
+
353
+ // Move track to front
354
+ player.queue.move(5, 0);
355
+
356
+ // Remove all tracks from specific source
357
+ player.queue.removeWhere((t) => t.source === "soundcloud");
358
+
359
+ // Jump back 2 tracks
360
+ await player.queue.jumpToHistory(2);
361
+
362
+ // Insert as next track
363
+ await player.insert("song name", 0, userId);
364
+ ```
365
+
366
+ ### 4. Custom Plugin Implementation
367
+
368
+ ```typescript
369
+ import { BasePlugin, Track, StreamInfo } from "ziplayer";
370
+
371
+ class CustomPlugin extends BasePlugin {
372
+ name = "CustomPlugin";
373
+ priority = 5;
374
+
375
+ canHandle(query: string): boolean {
376
+ return query.startsWith("custom:");
377
+ }
378
+
379
+ async search(query: string, requestedBy: string): Promise<SearchResult> {
380
+ // Implementation
381
+ return { tracks: [] };
382
+ }
383
+
384
+ async getStream(track: Track, signal: AbortSignal): Promise<StreamInfo> {
385
+ // Return audio stream
386
+ return { stream: readableStream, type: "arbitrary" };
387
+ }
388
+
389
+ async getRelatedTracks(track: Track): Promise<Track[]> {
390
+ // Return recommendations
391
+ return [];
392
+ }
393
+ }
394
+ ```
395
+
396
+ ### 5. Custom Extension Implementation
397
+
398
+ ```typescript
399
+ import { BaseExtension, ExtensionContext } from "ziplayer";
400
+
401
+ class LoggerExtension extends BaseExtension {
402
+ name = "Logger";
403
+ priority = 10;
404
+
405
+ async beforePlay(context: ExtensionContext, request: any) {
406
+ console.log(`Playing: ${request.query}`);
407
+ return { handled: false };
408
+ }
409
+
410
+ async afterPlay(context: ExtensionContext, payload: any) {
411
+ if (payload.success) {
412
+ console.log(`Successfully played ${payload.tracks?.length} tracks`);
413
+ }
414
+ }
415
+ }
416
+ ```
417
+
418
+ ### 6. Event Handling
419
+
420
+ ```typescript
421
+ manager.on("trackStart", (player, track) => {
422
+ console.log(`Now playing: ${track.title}`);
423
+ });
424
+
425
+ manager.on("queueEnd", (player) => {
426
+ console.log("Queue finished!");
427
+ });
428
+
429
+ manager.on("playerError", (player, error, track) => {
430
+ console.error(`Error on ${track?.title}:`, error.message);
431
+ });
432
+
433
+ manager.on("stats", (stats) => {
434
+ console.log(`Active players: ${stats.activePlayers}`);
435
+ });
436
+
437
+ // Listen globally via manager:
438
+ manager.on("trackStart", (player, track) => {});
439
+ manager.on("trackEnd", (player, track) => {});
440
+ manager.on("queueEnd", (player) => {});
441
+ manager.on("playerError", (player, error, track) => {});
442
+ manager.on("playerPause", (player, track) => {});
443
+ manager.on("playerResume", (player, track) => {});
444
+ manager.on("volumeChange", (player, oldVolume, newVolume) => {});
445
+ manager.on("queueAdd", (player, track) => {});
446
+ manager.on("queueAddList", (player, tracks) => {});
447
+ manager.on("queueRemove", (player, track, index) => {});
448
+ manager.on("playerDestroy", (player) => {});
449
+ manager.on("ttsStart", (player, payload) => {});
450
+ manager.on("ttsEnd", (player) => {});
451
+ manager.on("stats", (PlayerStats) => {});
452
+ manager.on("forwardModeStart", (player, leader) => {});
453
+ manager.on("forwardModeEnd", (player, leader) => {});
454
+ ```
455
+
456
+ ---
457
+
458
+ ## 🐛 Troubleshooting
459
+
460
+ ### Common Issues
461
+
462
+ | Issue | Solution |
463
+ | ------------------------ | ----------------------------------------------------------- |
464
+ | **No audio** | Check `player.connection` exists, voice channel permissions |
465
+ | **Plugin not working** | Verify `canHandle()` returns true, check priority |
466
+ | **Filters not applying** | Call `refreshPlayerResource(true)` after applying |
467
+ | **Memory leak** | Enable `autoCleanup`, call `player.destroy()` when done |
468
+ | **Rate limiting** | Use search cache, increase `extractorTimeout` |
469
+
470
+ ### Debug Mode
471
+
472
+ ```typescript
473
+ // Enable debug logging
474
+ manager.on("debug", (message) => {
475
+ console.log("[DEBUG]", message);
476
+ });
477
+
478
+ // Or check debug flag
479
+ if (manager.debugEnabled) {
480
+ // Debug-specific logic
481
+ }
482
+ ```
483
+
484
+ ### Performance Tips
485
+
486
+ 1. **Enable caching** for search and stream results
487
+ 2. **Set appropriate timeouts** based on network conditions
488
+ 3. **Batch operations** when modifying queue
489
+ 4. **Destroy players** when no longer needed
490
+
491
+ ---
492
+
493
+ ## 📝 Code Examples
494
+
495
+ ### Full Bot Example
496
+
497
+ ```typescript
498
+ import { Client, GatewayIntentBits, EmbedBuilder } from "discord.js";
499
+ import { PlayerManager } from "ziplayer";
500
+ import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
501
+
502
+ const client = new Client({
503
+ intents: [
504
+ GatewayIntentBits.Guilds,
505
+ GatewayIntentBits.GuildVoiceStates,
506
+ GatewayIntentBits.GuildMessages,
507
+ GatewayIntentBits.MessageContent,
508
+ ],
509
+ });
510
+
511
+ const manager = new PlayerManager({
512
+ plugins: [new YouTubePlugin(), new SpotifyPlugin(), new TTSPlugin()],
513
+ autoCleanup: true,
514
+ extractorTimeout: 30000,
515
+ });
516
+
517
+ client.on("messageCreate", async (msg) => {
518
+ if (!msg.guildId || msg.author.bot) return;
519
+
520
+ const args = msg.content.slice(1).split(" ");
521
+ const command = args[0].toLowerCase();
522
+ const query = args.slice(1).join(" ");
523
+
524
+ const player = await manager.create(msg.guildId);
525
+ const voiceChannel = msg.member?.voice.channel;
526
+
527
+ switch (command) {
528
+ case "play":
529
+ if (!voiceChannel) return msg.reply("Join a voice channel!");
530
+ if (!player.connection) await player.connect(voiceChannel);
531
+ await player.play(query, msg.author.id);
532
+ break;
533
+
534
+ case "pause":
535
+ player.pause();
536
+ break;
537
+
538
+ case "resume":
539
+ player.resume();
540
+ break;
541
+
542
+ case "skip":
543
+ player.skip();
544
+ break;
545
+
546
+ case "stop":
547
+ player.stop();
548
+ break;
549
+
550
+ case "volume":
551
+ const vol = parseInt(query);
552
+ if (isNaN(vol)) return msg.reply("Volume must be a number!");
553
+ player.setVolume(vol);
554
+ break;
555
+
556
+ case "queue":
557
+ const tracks = player.upcomingTracks.slice(0, 10);
558
+ const embed = new EmbedBuilder()
559
+ .setTitle("Queue")
560
+ .setDescription(tracks.map((t, i) => `${i + 1}. ${t.title}`).join("\n") || "Empty");
561
+ msg.reply({ embeds: [embed] });
562
+ break;
563
+
564
+ case "nowplaying":
565
+ const track = player.currentTrack;
566
+ if (!track) return msg.reply("Nothing playing!");
567
+
568
+ const progress = player.getProgressBar({ size: 15 });
569
+ const time = player.getTime();
570
+
571
+ const embed = new EmbedBuilder()
572
+ .setTitle(track.title)
573
+ .setURL(track.url)
574
+ .setThumbnail(track.thumbnail)
575
+ .setDescription(`\`${progress}\`\n${time.formatted.current} / ${time.formatted.total}`);
576
+ msg.reply({ embeds: [embed] });
577
+ break;
578
+ }
579
+ });
580
+
581
+ client.login(process.env.DISCORD_TOKEN);
582
+ ```
583
+
584
+ ---
585
+
586
+ ## 🔗 Quick Reference
587
+
588
+ ### Import Paths
589
+
590
+ ```typescript
591
+ // Core
592
+ import { PlayerManager, Player, Queue } from "ziplayer";
593
+
594
+ // Types
595
+ import type { Track, SearchResult, LoopMode, StreamInfo } from "ziplayer";
596
+
597
+ // Plugins (external package)
598
+ import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
599
+
600
+ // infinity plugin support stream audio from YouTube, TikTok, Instagram, Twitter/X, SoundCloud, Reddit, Twitch, Bilibili, and 1000+ other sites
601
+
602
+ import { InfinityPlugin } from "@ziplayer/infinity";
603
+
604
+ // Extensions (external package)
605
+ import { voiceExt, lyricsExt, lavalinkExt } from "@ziplayer/extension";
606
+
607
+ //fallback for youtube plugin if YouTubePlugin getStream error tunnel
608
+ import { YTexec } from "@ziplayer/ytexecplug"; //ytexecplug needs python, install python fist
609
+
610
+ const ytbplg = new YouTubePlugin({ fistStream: new YTexec().getStream });
611
+ ```
612
+
613
+ ### Type Definitions
614
+
615
+ ```typescript
616
+ interface Track {
617
+ id: string;
618
+ title: string;
619
+ url: string;
620
+ source: string;
621
+ duration: number;
622
+ thumbnail?: string;
623
+ requestedBy?: string;
624
+ isLive?: boolean;
625
+ }
626
+
627
+ type LoopMode = "off" | "track" | "queue";
628
+
629
+ interface SearchResult {
630
+ tracks: Track[];
631
+ playlist?: { name: string; url?: string };
632
+ }
633
+ ```
634
+
635
+ ## Terminology
636
+
637
+ - **Plugins** (`SourcePlugin`): search, `getStream`, playlists audio **sources** (YouTube, SoundCloud, etc.).
638
+ - **Extensions** (`SourceExtension` / `BaseExtension`): cross-cutting behavior — **not** the same as **`trackMiddleware`**.
639
+
640
+ ## Track transforms before stream
641
+
642
+ | Goal | Mechanism |
643
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
644
+ | Enrich every queued track right before stream extraction (preload, playback, `save`, TTS interrupt path uses middleware before raw plugin stream where applicable) | **`trackMiddleware`** on `PlayerManagerOptions` / `PlayerOptions` — ordered chain; merged order: manager chain first, then per-player chain. Implemented in `Player.applyTrackMiddleware` → called at start of `Player.getStream`, and before direct `pluginManager.getStream` in TTS interrupt / `save`. |
645
+ | Change query or inject tracks before search resolves | Extension **`beforePlay`** — mutate `payload.query` or return `{ tracks }`. |
646
+ | Custom stream backend | Extension **`provideStream`** — runs **after** track middleware, before plugins. |
647
+
648
+ Types: `TrackMiddleware`, `TrackMiddlewareContext`, `normalizeTrackMiddleware` in `src/types/index.ts`.
649
+
650
+ When returning a **new** `Track` from middleware, the core **merges** into the original reference (`mergeTrackPreserveRef`) so
651
+ queue pointers stay valid.
652
+
653
+ ## Metadata: BPM, LUFS, genre
654
+
655
+ Advanced playback reads **`track.metadata`**:
656
+
657
+ - **`bpm`**, **`genre`**, **`lufs`** — smart transition + loudness (see `Player`).
658
+
659
+ ## Multi-guild broadcast
660
+
661
+ | API | Behavior |
662
+ | -------------------------------------------- | ----------------------------------------------------------------- |
663
+ | `broadcast(action, ...args)` | Sync fan-out of `player[action]` to **all** players. |
664
+ | `broadcastAsync(action, ...args)` | Same, but `Promise.allSettled` on return values (use for `play`). |
665
+ | `broadcastGuilds(guildIds, action, ...args)` | Subset of guilds. |
666
+
667
+ Followers must already exist (`create`). One mirror subscription per leader id replaces the previous.
668
+
669
+ ### Playback Mirror / Forward Mode
670
+
671
+ Ziplayer supports built-in multi-guild playback mirroring using shared audio forwarding. A leader player streams audio normally,
672
+ while followers directly subscribe to the leader's internal audioPlayer.
673
+
674
+ This allows multiple guilds to hear the exact same playback while using only:
675
+
676
+ - one stream
677
+ - one decoder
678
+ - one extractor pipeline
679
+
680
+ Resulting in extremely low CPU and bandwidth usage.
681
+
682
+ ```ts
683
+ const stopMirror = manager.subscribeForwardMirror({
684
+ leaderGuildId: "123",
685
+ followerGuildIds: ["456", "789"],
686
+ syncVolume: true,
687
+ });
688
+
689
+ // later
690
+ stopMirror();
691
+ ```
692
+
693
+ **Direct Player Subscription:**
694
+
695
+ Followers may also subscribe manually:
696
+
697
+ ````ts
698
+ const leader = manager.get("123");
699
+ const follower = manager.get("456");
700
+
701
+ follower.subscribeTo(leader);
702
+ //Unsubscribe:
703
+ //follower.unsubscribeForward();
704
+ ```
705
+
706
+ ---
707
+
708
+ ## 📖 Additional Resources
709
+
710
+ - [GitHub Repository](https://github.com/ZiProject/ZiPlayer)
711
+ - [npm Package](https://www.npmjs.com/package/ziplayer)
712
+ - [Examples Folder](https://github.com/ZiProject/ZiPlayer/tree/main/examples)
713
+
714
+ ---
715
+
716
+ _This guide is maintained for AI assistants and developers. For questions or contributions, please open an issue on GitHub._
717
+ ````