ziplayer 0.2.7-dev.0 → 0.2.7-dev.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 (54) hide show
  1. package/AI-Guide.md +407 -756
  2. package/README.md +265 -11
  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 +61 -0
  11. package/dist/persistence/PersistenceManager.d.ts.map +1 -0
  12. package/dist/persistence/PersistenceManager.js +551 -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 +19 -4
  17. package/dist/plugins/index.d.ts.map +1 -1
  18. package/dist/plugins/index.js +204 -113
  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/Player.d.ts +64 -14
  23. package/dist/structures/Player.d.ts.map +1 -1
  24. package/dist/structures/Player.js +326 -88
  25. package/dist/structures/Player.js.map +1 -1
  26. package/dist/structures/PlayerManager.d.ts +125 -91
  27. package/dist/structures/PlayerManager.d.ts.map +1 -1
  28. package/dist/structures/PlayerManager.js +406 -111
  29. package/dist/structures/PlayerManager.js.map +1 -1
  30. package/dist/structures/Queue.d.ts +136 -31
  31. package/dist/structures/Queue.d.ts.map +1 -1
  32. package/dist/structures/Queue.js +265 -46
  33. package/dist/structures/Queue.js.map +1 -1
  34. package/dist/types/index.d.ts +39 -6
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/types/index.js +1 -0
  37. package/dist/types/index.js.map +1 -1
  38. package/dist/types/persistence.d.ts +55 -0
  39. package/dist/types/persistence.d.ts.map +1 -0
  40. package/dist/types/persistence.js +3 -0
  41. package/dist/types/persistence.js.map +1 -0
  42. package/package.json +3 -2
  43. package/src/extensions/BaseExtension.ts +1 -0
  44. package/src/extensions/index.ts +320 -37
  45. package/src/persistence/PersistenceManager.ts +572 -0
  46. package/src/plugins/BasePlugin.ts +1 -1
  47. package/src/plugins/index.ts +248 -133
  48. package/src/structures/FilterManager.ts +3 -3
  49. package/src/structures/Player.ts +352 -94
  50. package/src/structures/PlayerManager.ts +488 -116
  51. package/src/structures/Queue.ts +300 -55
  52. package/src/types/index.ts +43 -10
  53. package/src/types/persistence.ts +65 -0
  54. package/src/types/plugin.ts +1 -1
package/AI-Guide.md CHANGED
@@ -1,725 +1,464 @@
1
- # ZiPlayer AI Developer Guide
1
+ # 🤖 AI Guide for ZiPlayer
2
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.
3
+ A comprehensive guide for AI assistants and developers working with ZiPlayer - a powerful Discord music player library.
5
4
 
6
- ---
5
+ ## 📋 Table of Contents
7
6
 
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)
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)
24
14
 
25
15
  ---
26
16
 
27
- ## 1. Package Overview
17
+ ## 🎯 Project Overview
28
18
 
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 |
19
+ **ZiPlayer** is an extensible Discord music engine built on `@discordjs/voice`.
38
20
 
39
- ---
21
+ ### Key Features
40
22
 
41
- ## 2. Installation
23
+ - Plugin-driven architecture (YouTube, SoundCloud, Spotify, TTS)
24
+ - Extension system (voice commands, lyrics, Lavalink)
25
+ - Audio filters (bassboost, nightcore, etc.)
26
+ - Auto-save/restore (persistence)
27
+ - Smart caching and fallback system
42
28
 
43
- ```bash
44
- npm install ziplayer @ziplayer/plugin @ziplayer/extension @ziplayer/infinity @discordjs/voice discord.js opusscript
45
- ```
29
+ ### Tech Stack
30
+
31
+ - TypeScript
32
+ - `@discordjs/voice` for audio
33
+ - FFmpeg for audio processing
34
+ - Node.js EventEmitter for events
46
35
 
47
36
  ---
48
37
 
49
- ## 3. Core Architecture
38
+ ## 🧱 Architecture
50
39
 
51
40
  ```
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
41
 
60
- ### Lifecycle flow
42
+ ┌─────────────────────────────────────────────────────────────┐ PlayerManager (Global) │ │
43
+ ┌─────────────────────────────────────────────────────────┐│ │ │ PersistenceManager (Auto-save) ││ │
44
+ └─────────────────────────────────────────────────────────┘│ │ │ │ │ ┌────────────┴────────────┐ │ │ ▼ ▼ │ │ ┌──────────────┐
45
+ ┌──────────────┐ │ │ │ Player 1 │ │ Player 2 │ │ │ │ (Guild A) │ │ (Guild B) │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ │ │
46
+ ┌───────┴────────┐ ┌───────┴────────┐ │ │ ▼ ▼ ▼ ▼ │ │ Queue Filter Queue Filter │ │ PluginMgr ExtMgr PluginMgr ExtMgr │
47
+ └─────────────────────────────────────────────────────────────┘
61
48
 
62
49
  ```
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
- ```
50
+
51
+ ### Component Responsibilities
52
+
53
+ | Component | Responsibility |
54
+ | -------------------- | ------------------------------------------------------ |
55
+ | `PlayerManager` | Creates/manages players, global event bus, persistence |
56
+ | `Player` | Per-guild audio playback, controls, event emission |
57
+ | `Queue` | Track management, loop modes, history, auto-play |
58
+ | `PluginManager` | Audio source resolution, streaming, fallback logic |
59
+ | `ExtensionManager` | Custom hooks (search, stream, before/after play) |
60
+ | `FilterManager` | FFmpeg audio effects |
61
+ | `PersistenceManager` | Auto-save/restore player state |
75
62
 
76
63
  ---
77
64
 
78
- ## 4. PlayerManager
65
+ ## 🧠 Core Concepts
79
66
 
80
- ### Construction
67
+ ### 1. Player Lifecycle
81
68
 
82
69
  ```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";
70
+ // Create Connect Play → (Auto-save) → Destroy
71
+ const player = await manager.create(guildId, options);
72
+ await player.connect(voiceChannel);
73
+ await player.play(query, userId);
74
+ // ... auto-saves periodically
75
+ player.destroy();
76
+ ```
87
77
 
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
- });
78
+ ### 2. Queue Loop Modes
79
+
80
+ ```typescript
81
+ player.loop("off"); // No loop (default)
82
+ player.loop("track"); // Repeat current track
83
+ player.loop("queue"); // Repeat entire queue
98
84
  ```
99
85
 
100
- ### Key methods
86
+ ### 3. Event Flow
101
87
 
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) |
88
+ ```
89
+ trackStart playing trackEnd playNext → (loop/autoplay)
90
+
91
+ queueEndleave
92
+ ```
111
93
 
112
- ### PlayerManager options
94
+ ### 4. Plugin Priority & Fallback
113
95
 
114
96
  ```typescript
115
- interface PlayerManagerOptions {
116
- plugins?: SourcePluginLike[];
117
- extensions?: any[];
118
- extractorTimeout?: number; // ms, default 10000
119
- }
97
+ // Plugins are tried in priority order (higher = first)
98
+ // If primary fails, fallback plugins are attempted sequentially
99
+ // Failed plugins don't block the queue
100
+
101
+ plugin.priority = 10; // Higher priority
120
102
  ```
121
103
 
122
- ---
104
+ ### 5. Caching Strategy
123
105
 
124
- ## 5. Player
106
+ | Cache Type | TTL | Purpose |
107
+ | --------------- | ------- | --------------------------- |
108
+ | Search cache | 2 min | Avoid duplicate API calls |
109
+ | Stream cache | 5 min | Cache resolved streams |
110
+ | Extension cache | 1-5 min | Extension operation results |
125
111
 
126
- ### Creating a player
112
+ ---
127
113
 
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
- ```
114
+ ## 📚 API Reference
148
115
 
149
- ### Connecting
116
+ ### PlayerManager
117
+
118
+ #### Constructor Options
150
119
 
151
120
  ```typescript
152
- await player.connect(voiceChannel); // discord.js VoiceChannel
121
+ interface PlayerManagerOptions {
122
+ plugins?: SourcePlugin[]; // Audio source plugins
123
+ extensions?: BaseExtension[]; // Custom extensions
124
+ extractorTimeout?: number; // Default: 10000ms
125
+ autoCleanup?: boolean; // Default: true
126
+ cleanupInterval?: number; // Default: 60000ms
127
+ enableSearchCache?: boolean; // Default: true
128
+ enableStatsCollection?: boolean; // Default: false
129
+ persistence?: PersistenceOptions; // Auto-save config
130
+ }
153
131
  ```
154
132
 
155
- ### Playing
133
+ #### Key Methods
156
134
 
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
- ```
135
+ | Method | Description |
136
+ | ---------------------------- | -------------------------- |
137
+ | `create(guildId, options)` | Create new player |
138
+ | `get(guildId)` | Get existing player |
139
+ | `delete(guildId)` | Destroy and remove player |
140
+ | `getAll()` | Get all players |
141
+ | `broadcast(action, ...args)` | Send action to all players |
142
+ | `saveAllPlayers()` | Manual save all players |
143
+ | `loadAllPlayers()` | Manual load all players |
173
144
 
174
- ### Playback controls
145
+ ### Player
175
146
 
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
- ```
147
+ #### Core Methods
191
148
 
192
- ### Information
149
+ | Method | Description | Returns |
150
+ | ------------------------------ | ----------------------- | ------------------- |
151
+ | `play(query, userId)` | Play track/search/queue | `Promise<boolean>` |
152
+ | `pause()` | Pause current | `boolean` |
153
+ | `resume()` | Resume playback | `boolean` |
154
+ | `skip(index?)` | Skip to next/index | `boolean` |
155
+ | `stop()` | Stop and clear queue | `boolean` |
156
+ | `seek(position)` | Seek to position (ms) | `Promise<boolean>` |
157
+ | `previous()` | Play previous track | `Promise<boolean>` |
158
+ | `setVolume(vol)` | Set volume (0-200) | `boolean` |
159
+ | `loop(mode)` | Set loop mode | `LoopMode` |
160
+ | `shuffle()` | Shuffle queue | `void` |
161
+ | `insert(query, index, userId)` | Insert at position | `Promise<boolean>` |
162
+ | `save(track, options)` | Save track to stream | `Promise<Readable>` |
163
+
164
+ #### Getters
193
165
 
194
166
  ```typescript
195
167
  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
168
  player.queueSize; // number
201
- player.volume; // number
202
169
  player.isPlaying; // boolean
203
170
  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
- });
171
+ player.volume; // number
172
+ player.upcomingTracks; // Track[]
173
+ player.previousTracks; // Track[]
174
+ player.relatedTracks; // Track[] | null
225
175
  ```
226
176
 
227
- ### Inserting tracks mid-queue
177
+ ### Queue
228
178
 
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
- ```
179
+ #### Methods
235
180
 
236
- ### Removing a track
181
+ | Method | Description |
182
+ | ------------------------- | -------------------- |
183
+ | `add(track)` | Add single track |
184
+ | `addMultiple(tracks)` | Add multiple tracks |
185
+ | `insert(track, index)` | Insert at position |
186
+ | `remove(index)` | Remove at index |
187
+ | `removeMultiple(indices)` | Remove multiple |
188
+ | `removeWhere(predicate)` | Remove by condition |
189
+ | `move(from, to)` | Move track |
190
+ | `swap(a, b)` | Swap tracks |
191
+ | `shuffle()` | Randomize order |
192
+ | `clear()` | Clear all tracks |
193
+ | `loop(mode)` | Set loop mode |
194
+ | `autoPlay(enabled)` | Enable/disable |
195
+ | `previous()` | Get previous track |
196
+ | `jumpToHistory(steps)` | Jump back in history |
237
197
 
238
- ```typescript
239
- const removed = player.queue.remove(2); // removes track at index 2
240
- ```
241
-
242
- ### Destroying
198
+ #### Properties
243
199
 
244
200
  ```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.
201
+ queue.size; // number
202
+ queue.isEmpty; // boolean
203
+ queue.currentTrack; // Track | null
204
+ queue.nextTrack; // Track | null
205
+ queue.lastTrack; // Track | null
206
+ queue.previousTracks; // Track[]
248
207
  ```
249
208
 
250
- ### Extension management
209
+ ### FilterManager
251
210
 
252
211
  ```typescript
253
- player.attachExtension(myExt);
254
- player.detachExtension(myExt);
255
- player.getExtensions(); // readonly BaseExtension[]
256
- ```
212
+ // Apply filters
213
+ await player.filter.applyFilter("bassboost");
214
+ await player.filter.applyFilters(["bassboost", "nightcore"]);
257
215
 
258
- ### Filter management (see §9)
216
+ // Available filters
217
+ // bassboost, trebleboost, nightcore, lofi, vaporwave,
218
+ // echo, reverb, chorus, karaoke, normalize, compressor, limiter
259
219
 
260
- ```typescript
261
- await player.filter.applyFilter("bassboost");
262
- await player.filter.removeFilter("bassboost");
220
+ // Clear filters
263
221
  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
- ```
222
+ await player.filter.clear("bassboost");
269
223
 
270
- ---
271
-
272
- ## 6. Queue
224
+ // Get current filters
225
+ const filterString = player.filter.getFilterString(); // "bassboost,nightcore"
226
+ ```
273
227
 
274
- Access via `player.queue`.
228
+ ### PersistenceManager
275
229
 
276
- ### Adding
230
+ #### Options
277
231
 
278
232
  ```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);
233
+ interface PersistenceOptions {
234
+ enabled: boolean; // Enable persistence
235
+ provider: "file" | "redis" | "database";
236
+ filePath?: string; // For file provider
237
+ saveInterval?: number; // Auto-save interval (ms)
238
+ autoLoad?: boolean; // Auto-load on start
239
+ compress?: boolean; // Compress data
240
+ maxBackups?: number; // Max backup files
241
+ }
283
242
  ```
284
243
 
285
- ### Removing / navigating
244
+ #### Methods
286
245
 
287
246
  ```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
247
+ const persistence = manager.getPersistence();
296
248
 
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
249
+ await persistence.savePlayer(player);
250
+ await persistence.saveAll();
251
+ await persistence.loadPlayer(guildId, restorePosition);
252
+ await persistence.loadAll();
253
+ await persistence.deletePlayer(guildId);
254
+ await persistence.restoreBackup(guildId, timestamp);
307
255
  ```
308
256
 
309
- ### Loop & autoplay
257
+ ---
310
258
 
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
- ```
259
+ ## 🔧 Common Patterns
317
260
 
318
- ---
261
+ ### 1. Basic Music Bot Setup
319
262
 
320
- ## 7. Plugins
263
+ ```typescript
264
+ import { Client, GatewayIntentBits } from "discord.js";
265
+ import { PlayerManager } from "ziplayer";
266
+ import { YouTubePlugin, SpotifyPlugin } from "@ziplayer/plugin";
321
267
 
322
- ### Built-in plugins (`@ziplayer/plugin`)
268
+ const client = new Client({
269
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
270
+ });
323
271
 
324
- #### YouTubePlugin
272
+ const manager = new PlayerManager({
273
+ plugins: [new YouTubePlugin(), new SpotifyPlugin()],
274
+ autoCleanup: true,
275
+ persistence: {
276
+ enabled: true,
277
+ filePath: "./data/players",
278
+ },
279
+ });
325
280
 
326
- ```typescript
327
- new YouTubePlugin({
328
- searchLimit: 10, // max search results
329
- // fallbackStream: fn(Track) => Promise<StreamInfo>
330
- // fistStream: fn(Track) => Promise<StreamInfo>
281
+ client.on("ready", async () => {
282
+ // Auto-load saved players
283
+ await manager.loadAllPlayers();
331
284
  });
332
- ```
333
285
 
334
- Handles: `youtube.com`, `youtu.be`, `music.youtube.com`, any free text search.
286
+ client.on("messageCreate", async (msg) => {
287
+ if (msg.content.startsWith("!play")) {
288
+ const player = await manager.create(msg.guildId);
289
+ const voiceChannel = msg.member?.voice.channel;
335
290
 
336
- #### SoundCloudPlugin
291
+ if (!player.connection) {
292
+ await player.connect(voiceChannel);
293
+ }
337
294
 
338
- ```typescript
339
- new SoundCloudPlugin();
295
+ const query = msg.content.slice(6);
296
+ await player.play(query, msg.author.id);
297
+ }
298
+ });
340
299
  ```
341
300
 
342
- Handles: `soundcloud.com` URLs, free text search (if not a URL for another service).
343
-
344
- #### SpotifyPlugin
301
+ ### 2. Progress Bar with Time Format
345
302
 
346
303
  ```typescript
347
- new SpotifyPlugin();
348
- ```
304
+ // Get formatted time
305
+ const time = player.getTime();
306
+ console.log(`Current: ${time.formatted.current}`); // "1:22:12"
307
+ console.log(`Total: ${time.formatted.total}`); // "3:45:30"
349
308
 
350
- Handles: `spotify:track:...`, `open.spotify.com/...` ⚠️ Metadata only — does NOT stream. Relies on YouTube/SoundCloud fallback for
351
- audio.
309
+ // Progress bar
310
+ const progressBar = player.getProgressBar({
311
+ size: 20,
312
+ barChar: "▬",
313
+ progressChar: "🔘",
314
+ timeFormat: "compact",
315
+ showPercentage: true,
316
+ });
317
+ // Output: "1:22:12 ▬▬▬▬▬▬▬▬▬🔘▬▬▬▬▬▬▬▬ 3:45:30 (36%)"
318
+ ```
352
319
 
353
- #### TTSPlugin
320
+ ### 3. Queue Management Commands
354
321
 
355
322
  ```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
- ```
323
+ // Skip to specific track
324
+ await player.skip(3); // Skip to index 3
364
325
 
365
- Query formats:
326
+ // Move track to front
327
+ player.queue.move(5, 0);
366
328
 
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)
329
+ // Remove all tracks from specific source
330
+ player.queue.removeWhere((t) => t.source === "soundcloud");
370
331
 
371
- #### AttachmentsPlugin
332
+ // Jump back 2 tracks
333
+ await player.queue.jumpToHistory(2);
372
334
 
373
- ```typescript
374
- new AttachmentsPlugin({
375
- maxFileSize: 25 * 1024 * 1024, // 25 MB
376
- allowedExtensions: ["mp3", "wav", "ogg", "m4a", "flac"],
377
- debug: false,
378
- });
335
+ // Insert as next track
336
+ await player.insert("song name", 0, userId);
379
337
  ```
380
338
 
381
- Handles: Discord CDN URLs (`cdn.discordapp.com`), direct audio file URLs.
382
-
383
- ### Writing a custom plugin
339
+ ### 4. Custom Plugin Implementation
384
340
 
385
341
  ```typescript
386
- import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
387
- import { Readable } from "stream";
342
+ import { BasePlugin, Track, StreamInfo } from "ziplayer";
388
343
 
389
- export class MyRadioPlugin extends BasePlugin {
390
- name = "myradio";
391
- version = "1.0.0";
392
- priority = 5; // lower = tried first in fallback; default 0
344
+ class CustomPlugin extends BasePlugin {
345
+ name = "CustomPlugin";
346
+ priority = 5;
393
347
 
394
348
  canHandle(query: string): boolean {
395
- return query.startsWith("radio:");
349
+ return query.startsWith("custom:");
396
350
  }
397
351
 
398
352
  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" };
353
+ // Implementation
354
+ return { tracks: [] };
415
355
  }
416
356
 
417
- // Optional: fallback when getStream fails
418
- async getFallback(track: Track): Promise<StreamInfo> {
419
- /* ... */
357
+ async getStream(track: Track, signal: AbortSignal): Promise<StreamInfo> {
358
+ // Return audio stream
359
+ return { stream: readableStream, type: "arbitrary" };
420
360
  }
421
361
 
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[]> {
362
+ async getRelatedTracks(track: Track): Promise<Track[]> {
363
+ // Return recommendations
429
364
  return [];
430
365
  }
431
366
  }
432
367
  ```
433
368
 
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.
369
+ ### 5. Custom Extension Implementation
438
370
 
439
371
  ```typescript
440
- new MyPlugin({ priority: 10 }); // tried last in fallback
441
- ```
442
-
443
- ---
444
-
445
- ## 8. Extensions
372
+ import { BaseExtension, ExtensionContext } from "ziplayer";
446
373
 
447
- ### Built-in extensions (`@ziplayer/extension`)
374
+ class LoggerExtension extends BaseExtension {
375
+ name = "Logger";
376
+ priority = 10;
448
377
 
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:
378
+ async beforePlay(context: ExtensionContext, request: any) {
379
+ console.log(`Playing: ${request.query}`);
380
+ return { handled: false };
381
+ }
471
382
 
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
- });
383
+ async afterPlay(context: ExtensionContext, payload: any) {
384
+ if (payload.success) {
385
+ console.log(`Successfully played ${payload.tracks?.length} tracks`);
386
+ }
387
+ }
388
+ }
477
389
  ```
478
390
 
479
- #### lyricsExt Auto lyrics
391
+ ### 6. Event Handling
480
392
 
481
393
  ```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,
394
+ manager.on("trackStart", (player, track) => {
395
+ console.log(`Now playing: ${track.title}`);
490
396
  });
491
- ```
492
-
493
- Events:
494
397
 
495
- ```typescript
496
- manager.on("lyricsCreate", (player, track, result) => {
497
- console.log(result.text); // plain text
498
- console.log(result.synced); // LRC string
398
+ manager.on("queueEnd", (player) => {
399
+ console.log("Queue finished!");
499
400
  });
500
401
 
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);
402
+ manager.on("playerError", (player, error, track) => {
403
+ console.error(`Error on ${track?.title}:`, error.message);
505
404
  });
506
- ```
507
405
 
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,
406
+ manager.on("playerSaved", (guildId) => {
407
+ console.log(`Saved state for guild ${guildId}`);
522
408
  });
523
- ```
524
-
525
- ### Writing a custom extension
526
-
527
- ```typescript
528
- import { BaseExtension, Player, ExtensionContext } from "ziplayer";
529
409
 
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",
410
+ manager.on("stats", (stats) => {
411
+ console.log(`Active players: ${stats.activePlayers}`);
616
412
  });
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
413
  ```
632
414
 
633
415
  ---
634
416
 
635
- ## 10. Events Reference
417
+ ## 🐛 Troubleshooting
636
418
 
637
- ### Manager events (recommended — all player events forwarded here)
419
+ ### Common Issues
638
420
 
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
- ```
421
+ | Issue | Solution |
422
+ | -------------------------- | ----------------------------------------------------------- |
423
+ | **No audio** | Check `player.connection` exists, voice channel permissions |
424
+ | **Plugin not working** | Verify `canHandle()` returns true, check priority |
425
+ | **Filters not applying** | Call `refreshPlayerResource(true)` after applying |
426
+ | **Persistence not saving** | Check `enabled: true`, file path writable |
427
+ | **Memory leak** | Enable `autoCleanup`, call `player.destroy()` when done |
428
+ | **Rate limiting** | Use search cache, increase `extractorTimeout` |
661
429
 
662
- ### Direct player events
430
+ ### Debug Mode
663
431
 
664
432
  ```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
- }
433
+ // Enable debug logging
434
+ manager.on("debug", (message) => {
435
+ console.log("[DEBUG]", message);
436
+ });
691
437
 
692
- interface SearchResult {
693
- tracks: Track[];
694
- playlist?: { name: string; url: string; thumbnail?: string };
438
+ // Or check debug flag
439
+ if (manager.debugEnabled) {
440
+ // Debug-specific logic
695
441
  }
442
+ ```
696
443
 
697
- interface StreamInfo {
698
- stream: Readable;
699
- type: "webm/opus" | "ogg/opus" | "arbitrary";
700
- metadata?: Record<string, any>;
701
- }
444
+ ### Performance Tips
702
445
 
703
- type LoopMode = "off" | "track" | "queue";
704
-
705
- interface AudioFilter {
706
- name: string;
707
- ffmpegFilter: string;
708
- description: string;
709
- category?: string;
710
- }
711
- ```
446
+ 1. **Enable caching** for search and stream results
447
+ 2. **Use persistence** to avoid re-fetching on restart
448
+ 3. **Set appropriate timeouts** based on network conditions
449
+ 4. **Batch operations** when modifying queue
450
+ 5. **Destroy players** when no longer needed
712
451
 
713
452
  ---
714
453
 
715
- ## 12. Common Patterns & Recipes
454
+ ## 📝 Code Examples
716
455
 
717
- ### Basic Discord bot setup (TypeScript)
456
+ ### Full Bot Example
718
457
 
719
458
  ```typescript
720
- import { Client, GatewayIntentBits } from "discord.js";
459
+ import { Client, GatewayIntentBits, EmbedBuilder } from "discord.js";
721
460
  import { PlayerManager } from "ziplayer";
722
- import { YouTubePlugin, SoundCloudPlugin, SpotifyPlugin } from "@ziplayer/plugin";
461
+ import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
723
462
 
724
463
  const client = new Client({
725
464
  intents: [
@@ -731,226 +470,138 @@ const client = new Client({
731
470
  });
732
471
 
733
472
  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}**`);
473
+ plugins: [new YouTubePlugin(), new SpotifyPlugin(), new TTSPlugin()],
474
+ autoCleanup: true,
475
+ extractorTimeout: 30000,
476
+ persistence: {
477
+ enabled: true,
478
+ filePath: "./player_data",
479
+ saveInterval: 60000,
480
+ autoLoad: true,
481
+ },
739
482
  });
740
483
 
741
484
  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}**`);
485
+ if (!msg.guildId || msg.author.bot) return;
486
+
487
+ const args = msg.content.slice(1).split(" ");
488
+ const command = args[0].toLowerCase();
489
+ const query = args.slice(1).join(" ");
490
+
491
+ const player = await manager.create(msg.guildId);
492
+ const voiceChannel = msg.member?.voice.channel;
493
+
494
+ switch (command) {
495
+ case "play":
496
+ if (!voiceChannel) return msg.reply("Join a voice channel!");
497
+ if (!player.connection) await player.connect(voiceChannel);
498
+ await player.play(query, msg.author.id);
499
+ break;
500
+
501
+ case "pause":
502
+ player.pause();
503
+ break;
504
+
505
+ case "resume":
506
+ player.resume();
507
+ break;
508
+
509
+ case "skip":
510
+ player.skip();
511
+ break;
512
+
513
+ case "stop":
514
+ player.stop();
515
+ break;
516
+
517
+ case "volume":
518
+ const vol = parseInt(query);
519
+ if (isNaN(vol)) return msg.reply("Volume must be a number!");
520
+ player.setVolume(vol);
521
+ break;
522
+
523
+ case "queue":
524
+ const tracks = player.upcomingTracks.slice(0, 10);
525
+ const embed = new EmbedBuilder()
526
+ .setTitle("Queue")
527
+ .setDescription(tracks.map((t, i) => `${i + 1}. ${t.title}`).join("\n") || "Empty");
528
+ msg.reply({ embeds: [embed] });
529
+ break;
530
+
531
+ case "nowplaying":
532
+ const track = player.currentTrack;
533
+ if (!track) return msg.reply("Nothing playing!");
534
+
535
+ const progress = player.getProgressBar({ size: 15 });
536
+ const time = player.getTime();
537
+
538
+ const embed = new EmbedBuilder()
539
+ .setTitle(track.title)
540
+ .setURL(track.url)
541
+ .setThumbnail(track.thumbnail)
542
+ .setDescription(`\`${progress}\`\n${time.formatted.current} / ${time.formatted.total}`);
543
+ msg.reply({ embeds: [embed] });
544
+ break;
545
+ }
758
546
  });
759
547
 
760
548
  client.login(process.env.DISCORD_TOKEN);
761
549
  ```
762
550
 
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
551
+ ---
775
552
 
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
- ```
553
+ ## 🔗 Quick Reference
789
554
 
790
- ### Autoplay (related tracks)
555
+ ### Import Paths
791
556
 
792
557
  ```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
- ```
558
+ // Core
559
+ import { PlayerManager, Player, Queue } from "ziplayer";
797
560
 
798
- ### Loop patterns
561
+ // Types
562
+ import type { Track, SearchResult, LoopMode, StreamInfo } from "ziplayer";
799
563
 
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
- ```
564
+ // Plugins (external package)
565
+ import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
805
566
 
806
- ### Progress bar in embeds
567
+ // infinity plugin support stream audio from YouTube, TikTok, Instagram, Twitter/X, SoundCloud, Reddit, Twitch, Bilibili, and 1000+ other sites
807
568
 
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.
569
+ import { InfinityPlugin } from "@ziplayer/infinity";
824
570
 
825
- // Or via manager (no player needed):
826
- const result = await manager.search("lofi hip hop", userId);
571
+ // Extensions (external package)
572
+ import { voiceExt, lyricsExt, lavalinkExt } from "@ziplayer/extension";
827
573
  ```
828
574
 
829
- ### Custom extension for auto-announce
575
+ ### Type Definitions
830
576
 
831
577
  ```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
- }
578
+ interface Track {
579
+ id: string;
580
+ title: string;
581
+ url: string;
582
+ source: string;
583
+ duration: number;
584
+ thumbnail?: string;
585
+ requestedBy?: string;
586
+ isLive?: boolean;
851
587
  }
852
588
 
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
589
+ type LoopMode = "off" | "track" | "queue";
871
590
 
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}`);
591
+ interface SearchResult {
592
+ tracks: Track[];
593
+ playlist?: { name: string; url?: string };
879
594
  }
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
595
  ```
900
596
 
901
597
  ---
902
598
 
903
- ## 14. Anti-Patterns to Avoid
599
+ ## 📖 Additional Resources
904
600
 
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
- ```
601
+ - [GitHub Repository](https://github.com/ZiProject/ZiPlayer)
602
+ - [npm Package](https://www.npmjs.com/package/ziplayer)
603
+ - [Examples Folder](https://github.com/ZiProject/ZiPlayer/tree/main/examples)
924
604
 
925
605
  ---
926
606
 
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
- ```
607
+ _This guide is maintained for AI assistants and developers. For questions or contributions, please open an issue on GitHub._