ziplayer 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AI-Guide.md ADDED
@@ -0,0 +1,956 @@
1
+ # ZiPlayer — AI Developer Guide
2
+
3
+ <img width="1175" height="305" alt="logo" src="https://raw.githubusercontent.com/ZiProject/ZiPlayer/refs/heads/main/publish/logo.png" />
4
+ > A complete reference for AI assistants helping developers build Discord music bots with the `ziplayer` ecosystem.
5
+
6
+ ---
7
+
8
+ ## Table of Contents
9
+
10
+ 1. [Package Overview](#1-package-overview)
11
+ 2. [Installation](#2-installation)
12
+ 3. [Core Architecture](#3-core-architecture)
13
+ 4. [PlayerManager](#4-playermanager)
14
+ 5. [Player](#5-player)
15
+ 6. [Queue](#6-queue)
16
+ 7. [Plugins](#7-plugins)
17
+ 8. [Extensions](#8-extensions)
18
+ 9. [Audio Filters](#9-audio-filters)
19
+ 10. [Events Reference](#10-events-reference)
20
+ 11. [TypeScript Types](#11-typescript-types)
21
+ 12. [Common Patterns & Recipes](#12-common-patterns--recipes)
22
+ 13. [Error Handling](#13-error-handling)
23
+ 14. [Anti-Patterns to Avoid](#14-anti-patterns-to-avoid)
24
+
25
+ ---
26
+
27
+ ## 1. Package Overview
28
+
29
+ | Package | Role | npm |
30
+ | ---------------------- | --------------------------------------------------------------- | ------------------ |
31
+ | `ziplayer` | Core player engine | Required |
32
+ | `@ziplayer/plugin` | Source plugins (YouTube, SoundCloud, Spotify, TTS, Attachments) | Required for audio |
33
+ | `@ziplayer/extension` | Extensions (voice STT, Lavalink, lyrics) | Optional |
34
+ | `@ziplayer/infinity` | Cobalt-powered multi-platform plugin | Optional |
35
+ | `@ziplayer/ytexecplug` | yt-dlp fallback for YouTube | Optional |
36
+ | `@discordjs/voice` | Discord voice layer | Peer dep |
37
+ | `discord.js` | Discord bot framework | Peer dep |
38
+
39
+ ---
40
+
41
+ ## 2. Installation
42
+
43
+ ```bash
44
+ npm install ziplayer @ziplayer/plugin @ziplayer/extension @ziplayer/infinity @discordjs/voice discord.js opusscript
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 3. Core Architecture
50
+
51
+ ```
52
+ PlayerManager ← singleton-like, manages all guilds
53
+ └── Player (per guild) ← controls audio for one server
54
+ ├── Queue ← ordered list of tracks
55
+ ├── PluginManager← resolves queries → streams
56
+ ├── ExtensionManager ← hooks into lifecycle
57
+ └── FilterManager← real-time FFmpeg audio effects
58
+ ```
59
+
60
+ ### Lifecycle flow
61
+
62
+ ```
63
+ manager.create(guildId, opts)
64
+ → Player constructed → extensions attached
65
+ → player.connect(voiceChannel)
66
+ → player.play(query, userId)
67
+ → extensionManager.BeforePlayHooks() ← extensions may intercept
68
+ → pluginManager.search() / getStream()
69
+ → AudioResource created (with filters)
70
+ → audioPlayer.play(resource)
71
+ → events: queueAdd → trackStart → trackEnd → queueEnd
72
+ → player.destroy()
73
+ → extensions.onDestroy() → voice disconnected → cleanup
74
+ ```
75
+
76
+ ---
77
+
78
+ ## 4. PlayerManager
79
+
80
+ ### Construction
81
+
82
+ ```typescript
83
+ import { PlayerManager } from "ziplayer";
84
+ import { YouTubePlugin, SoundCloudPlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
85
+ import { InfinityPlugin } from "@ziplayer/infinity";
86
+ import { voiceExt, lyricsExt } from "@ziplayer/extension";
87
+
88
+ const manager = new PlayerManager({
89
+ plugins: [
90
+ new TTSPlugin({ defaultLang: "en" }),
91
+ new YouTubePlugin({}),
92
+ new SoundCloudPlugin(),
93
+ new InfinityPlugin(),
94
+ new SpotifyPlugin(),
95
+ ],
96
+ extensions: [new voiceExt(null, { lang: "en-US" }), new lyricsExt(null, { provider: "lrclib", includeSynced: true })],
97
+ });
98
+ ```
99
+
100
+ ### Key methods
101
+
102
+ | Method | Signature | Description |
103
+ | --------- | ---------------------------------------------- | ---------------------------------------------------- |
104
+ | `create` | `(guildOrId, options?) → Promise<Player>` | Creates (or returns existing) player for a guild |
105
+ | `get` | `(guildOrId) → Player \| undefined` | Gets existing player |
106
+ | `has` | `(guildOrId) → boolean` | Checks if player exists |
107
+ | `delete` | `(guildOrId) → boolean` | Destroys and removes a player |
108
+ | `getall` | `() → Player[]` | All active players |
109
+ | `destroy` | `() → void` | Destroys ALL players |
110
+ | `search` | `(query, requestedBy) → Promise<SearchResult>` | Search without a player (uses first matching plugin) |
111
+
112
+ ### PlayerManager options
113
+
114
+ ```typescript
115
+ interface PlayerManagerOptions {
116
+ plugins?: SourcePluginLike[];
117
+ extensions?: any[];
118
+ extractorTimeout?: number; // ms, default 10000
119
+ }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## 5. Player
125
+
126
+ ### Creating a player
127
+
128
+ ```typescript
129
+ const player = await manager.create(guildId, {
130
+ leaveOnEnd: true, // leave voice when queue ends
131
+ leaveTimeout: 30_000, // ms before leaving (default 100000)
132
+ volume: 100, // 0–200, default 100
133
+ quality: "high", // "high" | "low"
134
+ selfDeaf: true,
135
+ selfMute: false,
136
+ extractorTimeout: 50_000, // ms per plugin operation
137
+ userdata: { channel: textChannel }, // arbitrary data, access via player.userdata
138
+ extensions: ["voiceExt", "lyricsExt"], // activate by name or instance
139
+ filters: ["bassboost", "normalize"], // pre-apply audio filters
140
+ tts: {
141
+ createPlayer: true, // pre-create TTS AudioPlayer
142
+ interrupt: true, // pause music → play TTS → resume
143
+ volume: 100, // TTS volume (0–200)
144
+ Max_Time_TTS: 60_000, // max TTS playback time ms
145
+ },
146
+ });
147
+ ```
148
+
149
+ ### Connecting
150
+
151
+ ```typescript
152
+ await player.connect(voiceChannel); // discord.js VoiceChannel
153
+ ```
154
+
155
+ ### Playing
156
+
157
+ ```typescript
158
+ // All of these work:
159
+ await player.play("Never Gonna Give You Up", userId); // text search
160
+ await player.play("https://youtube.com/watch?v=...", userId); // direct URL
161
+ await player.play("https://youtube.com/playlist?list=...", userId); // playlist
162
+ await player.play("tts: Hello everyone!", userId); // TTS
163
+ await player.play(trackObject, userId); // Track object
164
+ await player.play(searchResult, userId); // SearchResult object
165
+ await player.play(null); // resume from queue
166
+
167
+ //InfinityPlugin:
168
+ await player.play("https://www.youtube.com/watch?v=dQw4w9WgXcQ", userId);
169
+ await player.play("https://www.tiktok.com/@user/video/123", userId);
170
+ await player.play("https://soundcloud.com/artist/track", userId);
171
+ await player.play("https://twitter.com/user/status/123", userId);
172
+ ```
173
+
174
+ ### Playback controls
175
+
176
+ ```typescript
177
+ player.pause(); // → boolean
178
+ player.resume(); // → boolean
179
+ player.skip(); // → boolean (skip to next)
180
+ player.skip(2); // → boolean (skip to index 2)
181
+ await player.previous(); // → boolean (go back one)
182
+ player.stop(); // → boolean (stop + clear queue)
183
+ await player.seek(30_000); // → boolean (seek to 30s)
184
+ player.setVolume(75); // 0–200, returns boolean
185
+ player.shuffle(); // shuffles queue
186
+ player.clearQueue();
187
+ player.loop("off"); // "off" | "track" | "queue"
188
+ player.loop(0); // same as "off"
189
+ player.autoPlay(true); // enable auto-play (related tracks)
190
+ ```
191
+
192
+ ### Information
193
+
194
+ ```typescript
195
+ player.currentTrack; // Track | null
196
+ player.previousTrack; // Track | null (last played)
197
+ player.upcomingTracks; // Track[]
198
+ player.previousTracks; // Track[]
199
+ player.relatedTracks; // Track[] | null
200
+ player.queueSize; // number
201
+ player.volume; // number
202
+ player.isPlaying; // boolean
203
+ player.isPaused; // boolean
204
+ player.availablePlugins; // string[] (plugin names)
205
+ player.userdata; // Record<string, any>
206
+
207
+ player.getProgressBar(); // "0:00 | ▬▬▬🔘▬ | 3:32"
208
+ player.getProgressBar({ size: 30, barChar: "━", progressChar: "⬤" });
209
+ player.getTime(); // { current: ms, total: ms, format: "1:23" }
210
+ player.formatTime(90_000); // "01:30"
211
+ ```
212
+
213
+ ### Saving a track stream
214
+
215
+ ```typescript
216
+ const stream = await player.save(track);
217
+ stream.pipe(fs.createWriteStream("output.mp3"));
218
+
219
+ // With options:
220
+ const stream = await player.save(track, {
221
+ filename: "my-song.mp3",
222
+ seek: 30_000, // start at 30s
223
+ filter: [{ name: "normalize", ffmpegFilter: "loudnorm", description: "Normalize" }],
224
+ });
225
+ ```
226
+
227
+ ### Inserting tracks mid-queue
228
+
229
+ ```typescript
230
+ // Insert at position 0 = plays after current track
231
+ await player.insert("song name", 0, userId);
232
+ await player.insert(trackObject, 0);
233
+ await player.insert([track1, track2], 0);
234
+ ```
235
+
236
+ ### Removing a track
237
+
238
+ ```typescript
239
+ const removed = player.queue.remove(2); // removes track at index 2
240
+ ```
241
+
242
+ ### Destroying
243
+
244
+ ```typescript
245
+ player.destroy();
246
+ // Stops audio, disconnects voice, clears queue, fires onDestroy on all extensions.
247
+ // After this call, do NOT reuse the player instance — call manager.create() again.
248
+ ```
249
+
250
+ ### Extension management
251
+
252
+ ```typescript
253
+ player.attachExtension(myExt);
254
+ player.detachExtension(myExt);
255
+ player.getExtensions(); // readonly BaseExtension[]
256
+ ```
257
+
258
+ ### Filter management (see §9)
259
+
260
+ ```typescript
261
+ await player.filter.applyFilter("bassboost");
262
+ await player.filter.removeFilter("bassboost");
263
+ await player.filter.clearAll();
264
+ player.filter.getActiveFilters(); // AudioFilter[]
265
+ player.filter.hasFilter("nightcore"); // boolean
266
+ player.filter.getAvailableFilters();
267
+ player.filter.getFiltersByCategory("eq");
268
+ ```
269
+
270
+ ---
271
+
272
+ ## 6. Queue
273
+
274
+ Access via `player.queue`.
275
+
276
+ ### Adding
277
+
278
+ ```typescript
279
+ player.queue.add(track);
280
+ player.queue.addMultiple([track1, track2]);
281
+ player.queue.insert(track, 0); // 0 = next up
282
+ player.queue.insertMultiple([t1, t2], 0);
283
+ ```
284
+
285
+ ### Removing / navigating
286
+
287
+ ```typescript
288
+ player.queue.remove(index) // Track | null
289
+ player.queue.next(ignoreLoop?) // Track | null — advances queue
290
+ player.queue.previous() // Track | null — goes back in history
291
+ player.queue.clear()
292
+ player.queue.shuffle()
293
+ ```
294
+
295
+ ### State / getters
296
+
297
+ ```typescript
298
+ player.queue.size; // number of upcoming tracks
299
+ player.queue.isEmpty; // boolean
300
+ player.queue.currentTrack; // Track | null
301
+ player.queue.nextTrack; // Track | null (peek)
302
+ player.queue.previousTracks; // Track[] (history, max 200)
303
+ player.queue.getTracks(); // Track[] (all upcoming)
304
+ player.queue.getTrack(index); // Track | null
305
+ player.queue.willNextTrack(); // Track | null (autoplay hint)
306
+ player.queue.relatedTracks(); // Track[] | null
307
+ ```
308
+
309
+ ### Loop & autoplay
310
+
311
+ ```typescript
312
+ player.queue.loop(); // get current mode
313
+ player.queue.loop("track"); // "off" | "track" | "queue"
314
+ player.queue.autoPlay(); // get state
315
+ player.queue.autoPlay(true); // enable/disable
316
+ ```
317
+
318
+ ---
319
+
320
+ ## 7. Plugins
321
+
322
+ ### Built-in plugins (`@ziplayer/plugin`)
323
+
324
+ #### YouTubePlugin
325
+
326
+ ```typescript
327
+ new YouTubePlugin({
328
+ searchLimit: 10, // max search results
329
+ // fallbackStream: fn(Track) => Promise<StreamInfo>
330
+ // fistStream: fn(Track) => Promise<StreamInfo>
331
+ });
332
+ ```
333
+
334
+ Handles: `youtube.com`, `youtu.be`, `music.youtube.com`, any free text search.
335
+
336
+ #### SoundCloudPlugin
337
+
338
+ ```typescript
339
+ new SoundCloudPlugin();
340
+ ```
341
+
342
+ Handles: `soundcloud.com` URLs, free text search (if not a URL for another service).
343
+
344
+ #### SpotifyPlugin
345
+
346
+ ```typescript
347
+ new SpotifyPlugin();
348
+ ```
349
+
350
+ Handles: `spotify:track:...`, `open.spotify.com/...` ⚠️ Metadata only — does NOT stream. Relies on YouTube/SoundCloud fallback for
351
+ audio.
352
+
353
+ #### TTSPlugin
354
+
355
+ ```typescript
356
+ new TTSPlugin({
357
+ defaultLang: "vi", // language code
358
+ slow: false,
359
+ createStream: async (text, ctx) => {
360
+ // return Readable | URL string | Buffer
361
+ },
362
+ });
363
+ ```
364
+
365
+ Query formats:
366
+
367
+ - `tts: <text>` — uses defaultLang
368
+ - `tts:<lang>:<text>` — e.g., `tts:en:Hello`
369
+ - `tts:<lang>:<slow>:<text>` — e.g., `tts:en:1:Hello` (slow=true)
370
+
371
+ #### AttachmentsPlugin
372
+
373
+ ```typescript
374
+ new AttachmentsPlugin({
375
+ maxFileSize: 25 * 1024 * 1024, // 25 MB
376
+ allowedExtensions: ["mp3", "wav", "ogg", "m4a", "flac"],
377
+ debug: false,
378
+ });
379
+ ```
380
+
381
+ Handles: Discord CDN URLs (`cdn.discordapp.com`), direct audio file URLs.
382
+
383
+ ### Writing a custom plugin
384
+
385
+ ```typescript
386
+ import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
387
+ import { Readable } from "stream";
388
+
389
+ export class MyRadioPlugin extends BasePlugin {
390
+ name = "myradio";
391
+ version = "1.0.0";
392
+ priority = 5; // lower = tried first in fallback; default 0
393
+
394
+ canHandle(query: string): boolean {
395
+ return query.startsWith("radio:");
396
+ }
397
+
398
+ async search(query: string, requestedBy: string): Promise<SearchResult> {
399
+ const track: Track = {
400
+ id: query,
401
+ title: "My Radio Station",
402
+ url: "https://stream.myradio.com/live.mp3",
403
+ duration: 0, // 0 for live streams
404
+ requestedBy,
405
+ source: this.name,
406
+ metadata: { isLive: true },
407
+ };
408
+ return { tracks: [track] };
409
+ }
410
+
411
+ async getStream(track: Track): Promise<StreamInfo> {
412
+ const response = await fetch(track.url);
413
+ const stream = Readable.fromWeb(response.body as any);
414
+ return { stream, type: "arbitrary" };
415
+ }
416
+
417
+ // Optional: fallback when getStream fails
418
+ async getFallback(track: Track): Promise<StreamInfo> {
419
+ /* ... */
420
+ }
421
+
422
+ // Optional: related track suggestions for autoplay
423
+ async getRelatedTracks(track: Track, opts = {}): Promise<Track[]> {
424
+ return [];
425
+ }
426
+
427
+ // Optional: extract all tracks from a playlist URL
428
+ async extractPlaylist(url: string, requestedBy: string): Promise<Track[]> {
429
+ return [];
430
+ }
431
+ }
432
+ ```
433
+
434
+ ### Plugin priority & fallback order
435
+
436
+ When `getStream` fails on the primary plugin, ZiPlayer tries all other plugins in order of `priority` (ascending — lower = higher
437
+ priority). Within the same priority group, `Promise.any` races them.
438
+
439
+ ```typescript
440
+ new MyPlugin({ priority: 10 }); // tried last in fallback
441
+ ```
442
+
443
+ ---
444
+
445
+ ## 8. Extensions
446
+
447
+ ### Built-in extensions (`@ziplayer/extension`)
448
+
449
+ #### voiceExt — Speech-to-Text
450
+
451
+ ```typescript
452
+ import { voiceExt } from "@ziplayer/extension";
453
+
454
+ new voiceExt(null, {
455
+ lang: "en-US", // Google Speech language
456
+ ignoreBots: true,
457
+ focusUser: "userId", // only listen to one user
458
+ minimalVoiceMessageDuration: 1, // seconds
459
+ postSilenceDelayMs: 2000, // wait after silence before STT
460
+ profanityFilter: false,
461
+ key: process.env.GSPEECH_V2_KEY, // own API key (recommended)
462
+ resolveSpeech: async (monoBuffer, opts) => "custom transcript",
463
+ onVoiceChange: async ({ userId, guildId, current }) => {
464
+ // return partial overrides per session
465
+ return { lang: "vi-VN" };
466
+ },
467
+ });
468
+ ```
469
+
470
+ Listen for results:
471
+
472
+ ```typescript
473
+ manager.on("voiceCreate", (player, evt) => {
474
+ console.log(evt.userId, evt.content, evt.channelId, evt.guildId);
475
+ // evt.user and evt.channel if client was passed
476
+ });
477
+ ```
478
+
479
+ #### lyricsExt — Auto lyrics
480
+
481
+ ```typescript
482
+ import { lyricsExt } from "@ziplayer/extension";
483
+
484
+ new lyricsExt(null, {
485
+ provider: "lrclib", // "lrclib" | "lyricsovh"
486
+ includeSynced: true, // prefer LRC synced lyrics
487
+ autoFetchOnTrackStart: true,
488
+ sanitizeTitle: true, // clean title before querying
489
+ maxLength: 32_000,
490
+ });
491
+ ```
492
+
493
+ Events:
494
+
495
+ ```typescript
496
+ manager.on("lyricsCreate", (player, track, result) => {
497
+ console.log(result.text); // plain text
498
+ console.log(result.synced); // LRC string
499
+ });
500
+
501
+ manager.on("lyricsChange", (player, track, result) => {
502
+ // Fires per line when synced lyrics available
503
+ console.log(result.current, result.previous, result.next);
504
+ console.log(result.lineIndex, result.timeMs);
505
+ });
506
+ ```
507
+
508
+ #### lavalinkExt — Lavalink server
509
+
510
+ ```typescript
511
+ import { lavalinkExt } from "@ziplayer/extension";
512
+
513
+ new lavalinkExt(null, {
514
+ nodes: [{ host: "localhost", port: 2333, password: "youshallnotpass", secure: false }],
515
+ client: discordClient, // discord.js Client (for voice events)
516
+ userId: "botUserId", // auto-detected from client if omitted
517
+ searchPrefix: "scsearch", // default search prefix for Lavalink
518
+ nodeSort: "players", // "players" | "cpu" | "memory" | "random"
519
+ requestTimeoutMs: 10_000,
520
+ updateInterval: 5_000,
521
+ debug: false,
522
+ });
523
+ ```
524
+
525
+ ### Writing a custom extension
526
+
527
+ ```typescript
528
+ import { BaseExtension, Player, ExtensionContext } from "ziplayer";
529
+
530
+ export class MyExtension extends BaseExtension {
531
+ name = "myExtension";
532
+ version = "1.0.0";
533
+ player: Player | null = null;
534
+
535
+ // Called to check if extension should activate for this player
536
+ active(ctx: { player: Player; manager: any }): boolean {
537
+ if (!this.player) this.player = ctx.player;
538
+ return true;
539
+ }
540
+
541
+ // Called once when registered to a player
542
+ onRegister(ctx: ExtensionContext): void {
543
+ ctx.player.on("trackStart", (track) => {
544
+ console.log("Custom ext: now playing", track.title);
545
+ });
546
+ }
547
+
548
+ // Called when player is destroyed
549
+ onDestroy(ctx: ExtensionContext): void {
550
+ // cleanup
551
+ }
552
+
553
+ // Intercept play requests BEFORE they resolve
554
+ async beforePlay(ctx, payload) {
555
+ // Can mutate payload.query, return tracks, or set handled: true
556
+ return undefined; // let normal flow continue
557
+ }
558
+
559
+ // Called AFTER play resolves (success or failure)
560
+ async afterPlay(ctx, payload) {
561
+ console.log("Played:", payload.tracks?.length, "tracks, success:", payload.success);
562
+ }
563
+
564
+ // Provide search results (skips plugins if returns tracks)
565
+ async provideSearch(ctx, { query, requestedBy }) {
566
+ return null; // return SearchResult to intercept
567
+ }
568
+
569
+ // Provide audio stream (skips plugins if returns stream)
570
+ async provideStream(ctx, { track }) {
571
+ return null; // return StreamInfo to intercept
572
+ }
573
+ }
574
+ ```
575
+
576
+ ---
577
+
578
+ ## 9. Audio Filters
579
+
580
+ ZiPlayer applies FFmpeg filters in real-time. Filters are re-applied immediately to the current track.
581
+
582
+ ### Predefined filters
583
+
584
+ | Name | Category | Description |
585
+ | ------------- | -------- | ---------------------- |
586
+ | `bassboost` | eq | Bass boost |
587
+ | `trebleboost` | eq | Treble boost |
588
+ | `normalize` | volume | Loudness normalization |
589
+ | `nightcore` | speed | Speed + pitch up |
590
+ | `lofi` | speed | Slow + lo-fi effect |
591
+ | `vaporwave` | speed | Vaporwave aesthetic |
592
+ | `8D` | effect | 8D surround effect |
593
+ | `echo` | effect | Echo/reverb |
594
+ | `reverb` | effect | Reverb |
595
+ | `chorus` | effect | Chorus |
596
+ | `karaoke` | vocal | Remove vocals |
597
+ | `slow` | speed | 0.5× speed |
598
+ | `fast` | speed | 2.0× speed |
599
+ | `mono` | channel | Mono output |
600
+ | `compressor` | dynamics | Dynamic compression |
601
+ | `limiter` | dynamics | Limiter |
602
+
603
+ ### Usage
604
+
605
+ ```typescript
606
+ // Apply
607
+ await player.filter.applyFilter("bassboost");
608
+ await player.filter.applyFilter("nightcore");
609
+
610
+ // Custom filter
611
+ await player.filter.applyFilter({
612
+ name: "custom",
613
+ ffmpegFilter: "volume=1.5,treble=g=5",
614
+ description: "Volume + treble boost",
615
+ category: "custom",
616
+ });
617
+
618
+ // Multiple at once
619
+ await player.filter.applyFilters(["bassboost", "normalize"]);
620
+
621
+ // Remove
622
+ await player.filter.removeFilter("bassboost");
623
+ await player.filter.clearAll();
624
+
625
+ // Query
626
+ player.filter.hasFilter("nightcore"); // boolean
627
+ player.filter.getActiveFilters(); // AudioFilter[]
628
+ player.filter.getFilterString(); // raw FFmpeg string
629
+ player.filter.getAvailableFilters(); // all predefined
630
+ player.filter.getFiltersByCategory("eq");
631
+ ```
632
+
633
+ ---
634
+
635
+ ## 10. Events Reference
636
+
637
+ ### Manager events (recommended — all player events forwarded here)
638
+
639
+ ```typescript
640
+ manager.on("trackStart", (player, track) => {});
641
+ manager.on("trackEnd", (player, track) => {});
642
+ manager.on("queueEnd", (player) => {});
643
+ manager.on("willPlay", (player, track, upcomingTracks) => {});
644
+ manager.on("queueAdd", (player, track) => {});
645
+ manager.on("queueAddList", (player, tracks) => {});
646
+ manager.on("queueRemove", (player, track, index) => {});
647
+ manager.on("playerPause", (player, track) => {});
648
+ manager.on("playerResume", (player, track) => {});
649
+ manager.on("playerStop", (player) => {});
650
+ manager.on("playerDestroy", (player) => {});
651
+ manager.on("playerError", (player, error, track?) => {});
652
+ manager.on("connectionError", (player, error) => {});
653
+ manager.on("volumeChange", (player, oldVolume, newVolume) => {});
654
+ manager.on("ttsStart", (player, { track }) => {});
655
+ manager.on("ttsEnd", (player) => {});
656
+ manager.on("voiceCreate", (player, evt) => {}); // voiceExt
657
+ manager.on("lyricsCreate", (player, track, result) => {}); // lyricsExt
658
+ manager.on("lyricsChange", (player, track, result) => {}); // lyricsExt
659
+ manager.on("debug", (message, ...args) => {});
660
+ ```
661
+
662
+ ### Direct player events
663
+
664
+ ```typescript
665
+ player.on("trackStart", (track) => {});
666
+ player.on("trackEnd", (track) => {});
667
+ player.on("queueEnd", () => {});
668
+ player.on("willPlay", (track, upcomingTracks) => {});
669
+ player.on("playerError", (error, track?) => {});
670
+ player.on("ttsStart", ({ track }) => {});
671
+ player.on("ttsEnd", () => {});
672
+ player.on("debug", (message) => {});
673
+ // ... same names as manager, minus the leading `player` param
674
+ ```
675
+
676
+ ---
677
+
678
+ ## 11. TypeScript Types
679
+
680
+ ```typescript
681
+ interface Track {
682
+ id: string;
683
+ title: string;
684
+ url: string;
685
+ duration: number; // milliseconds (some plugins use seconds — check source)
686
+ thumbnail?: string;
687
+ requestedBy: string;
688
+ source: string; // plugin name: "youtube" | "soundcloud" | "tts" | ...
689
+ metadata?: Record<string, any>;
690
+ }
691
+
692
+ interface SearchResult {
693
+ tracks: Track[];
694
+ playlist?: { name: string; url: string; thumbnail?: string };
695
+ }
696
+
697
+ interface StreamInfo {
698
+ stream: Readable;
699
+ type: "webm/opus" | "ogg/opus" | "arbitrary";
700
+ metadata?: Record<string, any>;
701
+ }
702
+
703
+ type LoopMode = "off" | "track" | "queue";
704
+
705
+ interface AudioFilter {
706
+ name: string;
707
+ ffmpegFilter: string;
708
+ description: string;
709
+ category?: string;
710
+ }
711
+ ```
712
+
713
+ ---
714
+
715
+ ## 12. Common Patterns & Recipes
716
+
717
+ ### Basic Discord bot setup (TypeScript)
718
+
719
+ ```typescript
720
+ import { Client, GatewayIntentBits } from "discord.js";
721
+ import { PlayerManager } from "ziplayer";
722
+ import { YouTubePlugin, SoundCloudPlugin, SpotifyPlugin } from "@ziplayer/plugin";
723
+
724
+ const client = new Client({
725
+ intents: [
726
+ GatewayIntentBits.Guilds,
727
+ GatewayIntentBits.GuildVoiceStates,
728
+ GatewayIntentBits.GuildMessages,
729
+ GatewayIntentBits.MessageContent,
730
+ ],
731
+ });
732
+
733
+ const manager = new PlayerManager({
734
+ plugins: [new YouTubePlugin({}), new SoundCloudPlugin(), new SpotifyPlugin()],
735
+ });
736
+
737
+ manager.on("trackStart", (player, track) => {
738
+ (player.userdata?.channel as any)?.send(`▶ **${track.title}**`);
739
+ });
740
+
741
+ client.on("messageCreate", async (msg) => {
742
+ if (msg.author.bot || !msg.guildId) return;
743
+ if (!msg.content.startsWith("!play ")) return;
744
+
745
+ const query = msg.content.slice(6).trim();
746
+ const voiceChannel = (msg.member as any)?.voice?.channel;
747
+ if (!voiceChannel) return msg.reply("Join a voice channel first!");
748
+
749
+ const player = await manager.create(msg.guildId, {
750
+ leaveOnEnd: true,
751
+ leaveTimeout: 30_000,
752
+ userdata: { channel: msg.channel },
753
+ });
754
+
755
+ if (!player.connection) await player.connect(voiceChannel);
756
+ await player.play(query, msg.author.id);
757
+ msg.reply(`Queued: **${query}**`);
758
+ });
759
+
760
+ client.login(process.env.DISCORD_TOKEN);
761
+ ```
762
+
763
+ ### TTS with music interrupt
764
+
765
+ ```typescript
766
+ const player = await manager.create(guildId, {
767
+ tts: { createPlayer: true, interrupt: true, volume: 100 },
768
+ });
769
+
770
+ // This pauses music, speaks, then auto-resumes:
771
+ await player.play("tts: Now playing your requested song!", userId);
772
+ ```
773
+
774
+ ### Voice-controlled bot
775
+
776
+ ```typescript
777
+ manager.on("voiceCreate", (player, evt) => {
778
+ const text = evt.content.toLowerCase();
779
+
780
+ if (text.includes("skip")) player.skip();
781
+ else if (text.includes("pause")) player.pause();
782
+ else if (text.includes("resume")) player.resume();
783
+ else if (text.includes("stop")) player.stop();
784
+ else if (text.startsWith("play ")) {
785
+ player.play(text.slice(5), evt.userId);
786
+ }
787
+ });
788
+ ```
789
+
790
+ ### Autoplay (related tracks)
791
+
792
+ ```typescript
793
+ player.queue.autoPlay(true);
794
+ // When queue empties, willNextTrack() is used (set by generateWillNext internally)
795
+ // ZiPlayer auto-fetches related tracks via pluginManager.getRelatedTracks()
796
+ ```
797
+
798
+ ### Loop patterns
799
+
800
+ ```typescript
801
+ player.loop("track"); // repeat current song forever
802
+ player.loop("queue"); // loop entire playlist
803
+ player.loop("off"); // no loop (default)
804
+ ```
805
+
806
+ ### Progress bar in embeds
807
+
808
+ ```typescript
809
+ manager.on("trackStart", (player, track) => {
810
+ const progress = player.getProgressBar({
811
+ size: 20,
812
+ barChar: "▬",
813
+ progressChar: "🔘",
814
+ });
815
+ // "0:00 | ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬🔘 | 3:32"
816
+ });
817
+ ```
818
+
819
+ ### Search without playing
820
+
821
+ ```typescript
822
+ const result = await player.search("lofi hip hop", userId);
823
+ // result.tracks[0].title, .duration, .thumbnail, .url, etc.
824
+
825
+ // Or via manager (no player needed):
826
+ const result = await manager.search("lofi hip hop", userId);
827
+ ```
828
+
829
+ ### Custom extension for auto-announce
830
+
831
+ ```typescript
832
+ class AutoAnnounceExt extends BaseExtension {
833
+ name = "autoAnnounce";
834
+ version = "1.0.0";
835
+ player: Player | null = null;
836
+
837
+ active(ctx: any): boolean {
838
+ if (!this.player) this.player = ctx.player;
839
+ const p = this.player!;
840
+ if ((p as any).__announced) return true;
841
+ (p as any).__announced = true;
842
+
843
+ p.on("trackStart", (track) => {
844
+ p.userdata?.channel?.send(`▶ Now playing: **${track.title}**`);
845
+ });
846
+ p.on("queueEnd", () => {
847
+ p.userdata?.channel?.send("Queue finished.");
848
+ });
849
+ return true;
850
+ }
851
+ }
852
+
853
+ const manager = new PlayerManager({ extensions: [new AutoAnnounceExt()] });
854
+ const player = await manager.create(guildId, { extensions: ["autoAnnounce"] });
855
+ ```
856
+
857
+ ### Getting global manager from anywhere
858
+
859
+ ```typescript
860
+ import { getManager, getPlayer } from "ziplayer";
861
+
862
+ const manager = getManager(); // PlayerManager | null
863
+ const player = getPlayer("guild-id"); // Player | null
864
+ ```
865
+
866
+ ---
867
+
868
+ ## 13. Error Handling
869
+
870
+ ### Recommended pattern
871
+
872
+ ```typescript
873
+ try {
874
+ await player.connect(voiceChannel);
875
+ const success = await player.play(query, userId);
876
+ if (!success) channel.send("❌ Could not play that.");
877
+ } catch (err) {
878
+ channel.send(`❌ Error: ${(err as Error).message}`);
879
+ }
880
+
881
+ manager.on("playerError", (player, error, track) => {
882
+ console.error(`[${player.guildId}] Error on "${track?.title}":`, error.message);
883
+ // ZiPlayer auto-skips to next track after playerError
884
+ });
885
+
886
+ manager.on("connectionError", (player, error) => {
887
+ console.error(`[${player.guildId}] Voice error:`, error.message);
888
+ player.destroy();
889
+ });
890
+ ```
891
+
892
+ ### Plugin timeout
893
+
894
+ ```typescript
895
+ // Per-player timeout for plugin operations:
896
+ const player = await manager.create(guildId, {
897
+ extractorTimeout: 15_000, // 15 seconds (default: 50000)
898
+ });
899
+ ```
900
+
901
+ ---
902
+
903
+ ## 14. Anti-Patterns to Avoid
904
+
905
+ | ❌ Wrong | ✅ Correct |
906
+ | ------------------------------------------------------ | ------------------------------------------ |
907
+ | Reusing `player` after `player.destroy()` | Call `manager.create()` again |
908
+ | Creating a new `PlayerManager` per command | One manager for the whole bot |
909
+ | Not awaiting `player.connect()` before `player.play()` | Always `await connect()` first |
910
+ | Ignoring `playerError` events | Always attach an error handler |
911
+ | Calling `player.play()` with empty string | Validate input before calling |
912
+ | Setting `leaveTimeout: 0` | Use `leaveOnEnd: false` instead |
913
+ | Using `player.queue.next()` directly | Use `player.skip()` to preserve events |
914
+ | Forgetting `disconnect()` on bot shutdown | Call `manager.destroy()` in SIGINT handler |
915
+
916
+ ```typescript
917
+ // Clean shutdown
918
+ process.on("SIGINT", () => {
919
+ manager.destroy();
920
+ client.destroy();
921
+ process.exit(0);
922
+ });
923
+ ```
924
+
925
+ ---
926
+
927
+ ## Quick Reference Card
928
+
929
+ ```
930
+ CREATE manager.create(guildId, opts) → Player
931
+ CONNECT player.connect(voiceChannel)
932
+ PLAY player.play(query | Track | SearchResult | null, userId?)
933
+ PAUSE player.pause()
934
+ RESUME player.resume()
935
+ SKIP player.skip(index?)
936
+ PREVIOUS player.previous()
937
+ STOP player.stop() // also clears queue
938
+ SEEK player.seek(ms)
939
+ VOLUME player.setVolume(0–200)
940
+ LOOP player.loop("off"|"track"|"queue")
941
+ SHUFFLE player.shuffle()
942
+ AUTOPLAY player.autoPlay(bool)
943
+ DESTROY player.destroy()
944
+
945
+ FILTER player.filter.applyFilter("bassboost")
946
+ player.filter.removeFilter("nightcore")
947
+ player.filter.clearAll()
948
+
949
+ QUEUE player.queue.size / isEmpty / currentTrack / nextTrack
950
+ player.queue.add(track) / insert(track, 0) / remove(index)
951
+ player.queue.getTracks() / getTrack(index)
952
+
953
+ INFO player.currentTrack / previousTrack / upcomingTracks
954
+ player.getProgressBar() / getTime()
955
+ player.isPlaying / isPaused / volume
956
+ ```