ziplayer 0.2.7 → 0.3.1

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 (63) hide show
  1. package/AI-Guide.md +624 -956
  2. package/README.md +277 -10
  3. package/dist/extensions/BaseExtension.d.ts +1 -0
  4. package/dist/extensions/BaseExtension.d.ts.map +1 -1
  5. package/dist/extensions/BaseExtension.js.map +1 -1
  6. package/dist/extensions/index.d.ts +38 -3
  7. package/dist/extensions/index.d.ts.map +1 -1
  8. package/dist/extensions/index.js +259 -41
  9. package/dist/extensions/index.js.map +1 -1
  10. package/dist/persistence/PersistenceManager.d.ts +95 -0
  11. package/dist/persistence/PersistenceManager.d.ts.map +1 -0
  12. package/dist/persistence/PersistenceManager.js +975 -0
  13. package/dist/persistence/PersistenceManager.js.map +1 -0
  14. package/dist/plugins/BasePlugin.js +1 -1
  15. package/dist/plugins/BasePlugin.js.map +1 -1
  16. package/dist/plugins/index.d.ts +74 -8
  17. package/dist/plugins/index.d.ts.map +1 -1
  18. package/dist/plugins/index.js +657 -116
  19. package/dist/plugins/index.js.map +1 -1
  20. package/dist/structures/FilterManager.js +3 -3
  21. package/dist/structures/FilterManager.js.map +1 -1
  22. package/dist/structures/PersistenceManager.d.ts +96 -0
  23. package/dist/structures/PersistenceManager.d.ts.map +1 -0
  24. package/dist/structures/PersistenceManager.js +1008 -0
  25. package/dist/structures/PersistenceManager.js.map +1 -0
  26. package/dist/structures/Player.d.ts +158 -14
  27. package/dist/structures/Player.d.ts.map +1 -1
  28. package/dist/structures/Player.js +1175 -188
  29. package/dist/structures/Player.js.map +1 -1
  30. package/dist/structures/PlayerManager.d.ts +106 -91
  31. package/dist/structures/PlayerManager.d.ts.map +1 -1
  32. package/dist/structures/PlayerManager.js +365 -124
  33. package/dist/structures/PlayerManager.js.map +1 -1
  34. package/dist/structures/Queue.d.ts +136 -31
  35. package/dist/structures/Queue.d.ts.map +1 -1
  36. package/dist/structures/Queue.js +265 -46
  37. package/dist/structures/Queue.js.map +1 -1
  38. package/dist/structures/StreamManager.d.ts +137 -0
  39. package/dist/structures/StreamManager.d.ts.map +1 -0
  40. package/dist/structures/StreamManager.js +420 -0
  41. package/dist/structures/StreamManager.js.map +1 -0
  42. package/dist/types/index.d.ts +181 -8
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/dist/types/index.js.map +1 -1
  45. package/dist/types/persistence.d.ts +77 -0
  46. package/dist/types/persistence.d.ts.map +1 -0
  47. package/dist/types/persistence.js +3 -0
  48. package/dist/types/persistence.js.map +1 -0
  49. package/package.json +3 -2
  50. package/src/extensions/BaseExtension.ts +1 -0
  51. package/src/extensions/index.ts +320 -37
  52. package/src/plugins/BasePlugin.ts +1 -1
  53. package/src/plugins/index.ts +809 -139
  54. package/src/structures/FilterManager.ts +3 -3
  55. package/src/structures/Player.ts +2810 -1693
  56. package/src/structures/PlayerManager.ts +438 -129
  57. package/src/structures/Queue.ts +300 -55
  58. package/src/structures/StreamManager.ts +524 -0
  59. package/src/types/extension.ts +129 -129
  60. package/src/types/fillter.ts +264 -264
  61. package/src/types/index.ts +187 -12
  62. package/src/types/plugin.ts +59 -59
  63. package/tsconfig.json +0 -1
package/AI-Guide.md CHANGED
@@ -1,956 +1,624 @@
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
- ```
1
+ # 🤖 AI Guide for ZiPlayer
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
+ // ... auto-saves periodically
61
+ player.destroy();
62
+ ```
63
+
64
+ ### 2. Queue Loop Modes
65
+
66
+ ```typescript
67
+ player.loop("off"); // No loop (default)
68
+ player.loop("track"); // Repeat current track
69
+ player.loop("queue"); // Repeat entire queue
70
+ ```
71
+
72
+ ### 3. Event Flow
73
+
74
+ ```
75
+ trackStart → playing → trackEnd → playNext → (loop/autoplay)
76
+
77
+ queueEnd → leave
78
+ ```
79
+
80
+ ### 4. Plugin Priority & Fallback
81
+
82
+ ```typescript
83
+ // Plugins are tried in priority order (higher = first)
84
+ // If primary fails, fallback plugins are attempted sequentially
85
+ // Failed plugins don't block the queue
86
+
87
+ plugin.priority = 10; // Higher priority
88
+ ```
89
+
90
+ ### 5. Caching Strategy
91
+
92
+ | Cache Type | TTL | Purpose |
93
+ | --------------- | ------- | --------------------------- |
94
+ | Search cache | 2 min | Avoid duplicate API calls |
95
+ | Stream cache | 5 min | Cache resolved streams |
96
+ | Extension cache | 1-5 min | Extension operation results |
97
+
98
+ ---
99
+
100
+ ## 📚 API Reference
101
+
102
+ ### PlayerManager
103
+
104
+ #### Constructor Options
105
+
106
+ ```typescript
107
+ interface PlayerManagerOptions {
108
+ plugins?: SourcePlugin[]; // Audio source plugins
109
+ extensions?: BaseExtension[]; // Custom extensions
110
+ extractorTimeout?: number; // Default: 10000ms
111
+ autoCleanup?: boolean; // Default: true
112
+ cleanupInterval?: number; // Default: 60000ms
113
+ enableSearchCache?: boolean; // Default: true
114
+ enableStatsCollection?: boolean; // Default: false
115
+ }
116
+ ```
117
+
118
+ #### Player Runtime Options (Performance Profile)
119
+
120
+ ```typescript
121
+ interface PlayerOptions {
122
+ lowPerformance?: boolean; // Default: false (or true when quality === "low")
123
+ preload?: {
124
+ enabled?: boolean; // Default: true
125
+ autoDisableInLowPerformance?: boolean; // Default: true
126
+ };
127
+ crossfade?: {
128
+ enabled?: boolean; // Explicit on/off
129
+ autoEnable?: boolean; // Default: true when enabled is undefined
130
+ autoDisableInLowPerformance?: boolean; // Default: true
131
+ durationMs?: number; // Default: 5000
132
+ };
133
+ smartTransition?: {
134
+ enabled?: boolean;
135
+ genreAware?: boolean;
136
+ beatAlign?: boolean;
137
+ baseDurationMs?: number;
138
+ minDurationMs?: number;
139
+ maxDurationMs?: number;
140
+ genreDurations?: Record<string, number>;
141
+ beatAlignMaxWaitMs?: number;
142
+ };
143
+ antiStuck?: {
144
+ enabled?: boolean;
145
+ maxRetries?: number;
146
+ retryDelayMs?: number;
147
+ reusePreloadFirst?: boolean;
148
+ reduceQualityOnRetry?: boolean;
149
+ controlledSkipThreshold?: number;
150
+ };
151
+ loudnessNormalization?: {
152
+ enabled?: boolean;
153
+ targetLUFS?: number;
154
+ maxBoostDb?: number;
155
+ maxCutDb?: number;
156
+ limiterCeiling?: number;
157
+ };
158
+ }
159
+ ```
160
+
161
+ - If `lowPerformance=true`, preload and crossfade are auto-disabled by default.
162
+ - `crossfade.autoEnable=true` allows crossfade to be enabled automatically when `crossfade.enabled` is not explicitly set.
163
+ - You can still force behavior by setting `enabled` flags directly.
164
+ - Runtime behavior: crossfade is used for next-track transitions and `skip()`.
165
+ - Smart transition can tune fade by `metadata.genre` and beat-align by `metadata.bpm`.
166
+ - Loudness normalization uses `metadata.lufs` with limiter ceiling protection.
167
+ - Anti-stuck retries in-place before controlled skip to avoid skip storms.
168
+
169
+ #### Key Methods
170
+
171
+ | Method | Description |
172
+ | ---------------------------- | -------------------------- |
173
+ | `create(guildId, options)` | Create new player |
174
+ | `get(guildId)` | Get existing player |
175
+ | `delete(guildId)` | Destroy and remove player |
176
+ | `getAll()` | Get all players |
177
+ | `broadcast(action, ...args)` | Send action to all players |
178
+
179
+ ### Player
180
+
181
+ #### Core Methods
182
+
183
+ | Method | Description | Returns |
184
+ | ------------------------------ | ----------------------- | ------------------- |
185
+ | `play(query, userId)` | Play track/search/queue | `Promise<boolean>` |
186
+ | `pause()` | Pause current | `boolean` |
187
+ | `resume()` | Resume playback | `boolean` |
188
+ | `skip(index?)` | Skip to next/index | `boolean` |
189
+ | `stop()` | Stop and clear queue | `boolean` |
190
+ | `seek(position)` | Seek to position (ms) | `Promise<boolean>` |
191
+ | `previous()` | Play previous track | `Promise<boolean>` |
192
+ | `setVolume(vol)` | Set volume (0-200) | `boolean` |
193
+ | `loop(mode)` | Set loop mode | `LoopMode` |
194
+ | `shuffle()` | Shuffle queue | `void` |
195
+ | `insert(query, index, userId)` | Insert at position | `Promise<boolean>` |
196
+ | `save(track, options)` | Save track to stream | `Promise<Readable>` |
197
+
198
+ #### Getters
199
+
200
+ ```typescript
201
+ player.currentTrack; // Track | null
202
+ player.queueSize; // number
203
+ player.isPlaying; // boolean
204
+ player.isPaused; // boolean
205
+ player.volume; // number
206
+ player.upcomingTracks; // Track[]
207
+ player.previousTracks; // Track[]
208
+ player.relatedTracks; // Track[] | null
209
+ ```
210
+
211
+ ### Queue
212
+
213
+ #### Methods
214
+
215
+ | Method | Description |
216
+ | ------------------------- | -------------------- |
217
+ | `add(track)` | Add single track |
218
+ | `addMultiple(tracks)` | Add multiple tracks |
219
+ | `insert(track, index)` | Insert at position |
220
+ | `remove(index)` | Remove at index |
221
+ | `removeMultiple(indices)` | Remove multiple |
222
+ | `removeWhere(predicate)` | Remove by condition |
223
+ | `move(from, to)` | Move track |
224
+ | `swap(a, b)` | Swap tracks |
225
+ | `shuffle()` | Randomize order |
226
+ | `clear()` | Clear all tracks |
227
+ | `loop(mode)` | Set loop mode |
228
+ | `autoPlay(enabled)` | Enable/disable |
229
+ | `previous()` | Get previous track |
230
+ | `jumpToHistory(steps)` | Jump back in history |
231
+
232
+ #### Properties
233
+
234
+ ```typescript
235
+ queue.size; // number
236
+ queue.isEmpty; // boolean
237
+ queue.currentTrack; // Track | null
238
+ queue.nextTrack; // Track | null
239
+ queue.lastTrack; // Track | null
240
+ queue.previousTracks; // Track[]
241
+ ```
242
+
243
+ ### FilterManager
244
+
245
+ ```typescript
246
+ // Apply filters
247
+ await player.filter.applyFilter("bassboost");
248
+ await player.filter.applyFilters(["bassboost", "nightcore"]);
249
+
250
+ // Available filters
251
+ // bassboost, trebleboost, nightcore, lofi, vaporwave,
252
+ // echo, reverb, chorus, karaoke, normalize, compressor, limiter
253
+
254
+ // Clear filters
255
+ await player.filter.clearAll();
256
+ await player.filter.clear("bassboost");
257
+
258
+ // Get current filters
259
+ const filterString = player.filter.getFilterString(); // "bassboost,nightcore"
260
+ ```
261
+
262
+ ## 🔧 Common Patterns
263
+
264
+ ### 1. Basic Music Bot Setup
265
+
266
+ ```typescript
267
+ import { Client, GatewayIntentBits } from "discord.js";
268
+ import { PlayerManager } from "ziplayer";
269
+ import { YouTubePlugin, SpotifyPlugin } from "@ziplayer/plugin";
270
+
271
+ const client = new Client({
272
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
273
+ });
274
+
275
+ const manager = new PlayerManager({
276
+ plugins: [new YouTubePlugin(), new SpotifyPlugin()],
277
+ autoCleanup: true,
278
+ });
279
+
280
+ client.on("ready", async () => {
281
+ // Auto-load saved players
282
+ await manager.loadAllPlayers();
283
+ });
284
+
285
+ client.on("messageCreate", async (msg) => {
286
+ if (msg.content.startsWith("!play")) {
287
+ const player = await manager.create(msg.guildId);
288
+ const voiceChannel = msg.member?.voice.channel;
289
+
290
+ if (!player.connection) {
291
+ await player.connect(voiceChannel);
292
+ }
293
+
294
+ const query = msg.content.slice(6);
295
+ await player.play(query, msg.author.id);
296
+ }
297
+ });
298
+
299
+ const player = await manager.create(guildId, {
300
+ lowPerformance: false,
301
+ preload: { enabled: true, autoDisableInLowPerformance: true },
302
+ crossfade: { autoEnable: true, autoDisableInLowPerformance: true, durationMs: 5000 },
303
+ smartTransition: {
304
+ enabled: true,
305
+ genreAware: true,
306
+ beatAlign: true,
307
+ baseDurationMs: 5000,
308
+ genreDurations: { chill: 7000, edm: 2200 },
309
+ },
310
+ antiStuck: {
311
+ enabled: true,
312
+ maxRetries: 2,
313
+ retryDelayMs: 900,
314
+ reusePreloadFirst: true,
315
+ reduceQualityOnRetry: true,
316
+ controlledSkipThreshold: 3,
317
+ },
318
+ loudnessNormalization: {
319
+ enabled: true,
320
+ targetLUFS: -14,
321
+ limiterCeiling: 0.95,
322
+ },
323
+ });
324
+ ```
325
+
326
+ ### 2. Progress Bar with Time Format
327
+
328
+ ```typescript
329
+ // Get formatted time
330
+ const time = player.getTime();
331
+ console.log(`Current: ${time.formatted.current}`); // "1:22:12"
332
+ console.log(`Total: ${time.formatted.total}`); // "3:45:30"
333
+
334
+ // Progress bar
335
+ const progressBar = player.getProgressBar({
336
+ size: 20,
337
+ barChar: "▬",
338
+ progressChar: "🔘",
339
+ timeFormat: "compact",
340
+ showPercentage: true,
341
+ });
342
+ // Output: "1:22:12 ▬▬▬▬▬▬▬▬▬🔘▬▬▬▬▬▬▬▬ 3:45:30 (36%)"
343
+ ```
344
+
345
+ ### 3. Queue Management Commands
346
+
347
+ ```typescript
348
+ // Skip to specific track
349
+ await player.skip(3); // Skip to index 3
350
+
351
+ // Move track to front
352
+ player.queue.move(5, 0);
353
+
354
+ // Remove all tracks from specific source
355
+ player.queue.removeWhere((t) => t.source === "soundcloud");
356
+
357
+ // Jump back 2 tracks
358
+ await player.queue.jumpToHistory(2);
359
+
360
+ // Insert as next track
361
+ await player.insert("song name", 0, userId);
362
+ ```
363
+
364
+ ### 4. Custom Plugin Implementation
365
+
366
+ ```typescript
367
+ import { BasePlugin, Track, StreamInfo } from "ziplayer";
368
+
369
+ class CustomPlugin extends BasePlugin {
370
+ name = "CustomPlugin";
371
+ priority = 5;
372
+
373
+ canHandle(query: string): boolean {
374
+ return query.startsWith("custom:");
375
+ }
376
+
377
+ async search(query: string, requestedBy: string): Promise<SearchResult> {
378
+ // Implementation
379
+ return { tracks: [] };
380
+ }
381
+
382
+ async getStream(track: Track, signal: AbortSignal): Promise<StreamInfo> {
383
+ // Return audio stream
384
+ return { stream: readableStream, type: "arbitrary" };
385
+ }
386
+
387
+ async getRelatedTracks(track: Track): Promise<Track[]> {
388
+ // Return recommendations
389
+ return [];
390
+ }
391
+ }
392
+ ```
393
+
394
+ ### 5. Custom Extension Implementation
395
+
396
+ ```typescript
397
+ import { BaseExtension, ExtensionContext } from "ziplayer";
398
+
399
+ class LoggerExtension extends BaseExtension {
400
+ name = "Logger";
401
+ priority = 10;
402
+
403
+ async beforePlay(context: ExtensionContext, request: any) {
404
+ console.log(`Playing: ${request.query}`);
405
+ return { handled: false };
406
+ }
407
+
408
+ async afterPlay(context: ExtensionContext, payload: any) {
409
+ if (payload.success) {
410
+ console.log(`Successfully played ${payload.tracks?.length} tracks`);
411
+ }
412
+ }
413
+ }
414
+ ```
415
+
416
+ ### 6. Event Handling
417
+
418
+ ```typescript
419
+ manager.on("trackStart", (player, track) => {
420
+ console.log(`Now playing: ${track.title}`);
421
+ });
422
+
423
+ manager.on("queueEnd", (player) => {
424
+ console.log("Queue finished!");
425
+ });
426
+
427
+ manager.on("playerError", (player, error, track) => {
428
+ console.error(`Error on ${track?.title}:`, error.message);
429
+ });
430
+
431
+ manager.on("playerSaved", (guildId) => {
432
+ console.log(`Saved state for guild ${guildId}`);
433
+ });
434
+
435
+ manager.on("stats", (stats) => {
436
+ console.log(`Active players: ${stats.activePlayers}`);
437
+ });
438
+ ```
439
+
440
+ ---
441
+
442
+ ## 🐛 Troubleshooting
443
+
444
+ ### Common Issues
445
+
446
+ | Issue | Solution |
447
+ | ------------------------ | ----------------------------------------------------------- |
448
+ | **No audio** | Check `player.connection` exists, voice channel permissions |
449
+ | **Plugin not working** | Verify `canHandle()` returns true, check priority |
450
+ | **Filters not applying** | Call `refreshPlayerResource(true)` after applying |
451
+ | **Memory leak** | Enable `autoCleanup`, call `player.destroy()` when done |
452
+ | **Rate limiting** | Use search cache, increase `extractorTimeout` |
453
+
454
+ ### Debug Mode
455
+
456
+ ```typescript
457
+ // Enable debug logging
458
+ manager.on("debug", (message) => {
459
+ console.log("[DEBUG]", message);
460
+ });
461
+
462
+ // Or check debug flag
463
+ if (manager.debugEnabled) {
464
+ // Debug-specific logic
465
+ }
466
+ ```
467
+
468
+ ### Performance Tips
469
+
470
+ 1. **Enable caching** for search and stream results
471
+ 2. **Set appropriate timeouts** based on network conditions
472
+ 3. **Batch operations** when modifying queue
473
+ 4. **Destroy players** when no longer needed
474
+
475
+ ---
476
+
477
+ ## 📝 Code Examples
478
+
479
+ ### Full Bot Example
480
+
481
+ ```typescript
482
+ import { Client, GatewayIntentBits, EmbedBuilder } from "discord.js";
483
+ import { PlayerManager } from "ziplayer";
484
+ import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
485
+
486
+ const client = new Client({
487
+ intents: [
488
+ GatewayIntentBits.Guilds,
489
+ GatewayIntentBits.GuildVoiceStates,
490
+ GatewayIntentBits.GuildMessages,
491
+ GatewayIntentBits.MessageContent,
492
+ ],
493
+ });
494
+
495
+ const manager = new PlayerManager({
496
+ plugins: [new YouTubePlugin(), new SpotifyPlugin(), new TTSPlugin()],
497
+ autoCleanup: true,
498
+ extractorTimeout: 30000,
499
+ });
500
+
501
+ client.on("messageCreate", async (msg) => {
502
+ if (!msg.guildId || msg.author.bot) return;
503
+
504
+ const args = msg.content.slice(1).split(" ");
505
+ const command = args[0].toLowerCase();
506
+ const query = args.slice(1).join(" ");
507
+
508
+ const player = await manager.create(msg.guildId);
509
+ const voiceChannel = msg.member?.voice.channel;
510
+
511
+ switch (command) {
512
+ case "play":
513
+ if (!voiceChannel) return msg.reply("Join a voice channel!");
514
+ if (!player.connection) await player.connect(voiceChannel);
515
+ await player.play(query, msg.author.id);
516
+ break;
517
+
518
+ case "pause":
519
+ player.pause();
520
+ break;
521
+
522
+ case "resume":
523
+ player.resume();
524
+ break;
525
+
526
+ case "skip":
527
+ player.skip();
528
+ break;
529
+
530
+ case "stop":
531
+ player.stop();
532
+ break;
533
+
534
+ case "volume":
535
+ const vol = parseInt(query);
536
+ if (isNaN(vol)) return msg.reply("Volume must be a number!");
537
+ player.setVolume(vol);
538
+ break;
539
+
540
+ case "queue":
541
+ const tracks = player.upcomingTracks.slice(0, 10);
542
+ const embed = new EmbedBuilder()
543
+ .setTitle("Queue")
544
+ .setDescription(tracks.map((t, i) => `${i + 1}. ${t.title}`).join("\n") || "Empty");
545
+ msg.reply({ embeds: [embed] });
546
+ break;
547
+
548
+ case "nowplaying":
549
+ const track = player.currentTrack;
550
+ if (!track) return msg.reply("Nothing playing!");
551
+
552
+ const progress = player.getProgressBar({ size: 15 });
553
+ const time = player.getTime();
554
+
555
+ const embed = new EmbedBuilder()
556
+ .setTitle(track.title)
557
+ .setURL(track.url)
558
+ .setThumbnail(track.thumbnail)
559
+ .setDescription(`\`${progress}\`\n${time.formatted.current} / ${time.formatted.total}`);
560
+ msg.reply({ embeds: [embed] });
561
+ break;
562
+ }
563
+ });
564
+
565
+ client.login(process.env.DISCORD_TOKEN);
566
+ ```
567
+
568
+ ---
569
+
570
+ ## 🔗 Quick Reference
571
+
572
+ ### Import Paths
573
+
574
+ ```typescript
575
+ // Core
576
+ import { PlayerManager, Player, Queue } from "ziplayer";
577
+
578
+ // Types
579
+ import type { Track, SearchResult, LoopMode, StreamInfo } from "ziplayer";
580
+
581
+ // Plugins (external package)
582
+ import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
583
+
584
+ // infinity plugin support stream audio from YouTube, TikTok, Instagram, Twitter/X, SoundCloud, Reddit, Twitch, Bilibili, and 1000+ other sites
585
+
586
+ import { InfinityPlugin } from "@ziplayer/infinity";
587
+
588
+ // Extensions (external package)
589
+ import { voiceExt, lyricsExt, lavalinkExt } from "@ziplayer/extension";
590
+ ```
591
+
592
+ ### Type Definitions
593
+
594
+ ```typescript
595
+ interface Track {
596
+ id: string;
597
+ title: string;
598
+ url: string;
599
+ source: string;
600
+ duration: number;
601
+ thumbnail?: string;
602
+ requestedBy?: string;
603
+ isLive?: boolean;
604
+ }
605
+
606
+ type LoopMode = "off" | "track" | "queue";
607
+
608
+ interface SearchResult {
609
+ tracks: Track[];
610
+ playlist?: { name: string; url?: string };
611
+ }
612
+ ```
613
+
614
+ ---
615
+
616
+ ## 📖 Additional Resources
617
+
618
+ - [GitHub Repository](https://github.com/ZiProject/ZiPlayer)
619
+ - [npm Package](https://www.npmjs.com/package/ziplayer)
620
+ - [Examples Folder](https://github.com/ZiProject/ZiPlayer/tree/main/examples)
621
+
622
+ ---
623
+
624
+ _This guide is maintained for AI assistants and developers. For questions or contributions, please open an issue on GitHub._