ziplayer 0.2.7-dev.0 → 0.2.7-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-Guide.md +407 -756
- package/README.md +275 -10
- package/dist/extensions/BaseExtension.d.ts +1 -0
- package/dist/extensions/BaseExtension.d.ts.map +1 -1
- package/dist/extensions/BaseExtension.js.map +1 -1
- package/dist/extensions/index.d.ts +38 -3
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +259 -41
- package/dist/extensions/index.js.map +1 -1
- package/dist/persistence/PersistenceManager.d.ts +95 -0
- package/dist/persistence/PersistenceManager.d.ts.map +1 -0
- package/dist/persistence/PersistenceManager.js +968 -0
- package/dist/persistence/PersistenceManager.js.map +1 -0
- package/dist/plugins/BasePlugin.js +1 -1
- package/dist/plugins/BasePlugin.js.map +1 -1
- package/dist/plugins/index.d.ts +19 -4
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +204 -113
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.js +3 -3
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/Player.d.ts +65 -14
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +330 -88
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +127 -91
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +437 -124
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +136 -31
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +265 -46
- package/dist/structures/Queue.js.map +1 -1
- package/dist/types/index.d.ts +46 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/persistence.d.ts +74 -0
- package/dist/types/persistence.d.ts.map +1 -0
- package/dist/types/persistence.js +3 -0
- package/dist/types/persistence.js.map +1 -0
- package/package.json +3 -2
- package/src/extensions/BaseExtension.ts +1 -0
- package/src/extensions/index.ts +320 -37
- package/src/persistence/PersistenceManager.ts +1073 -0
- package/src/plugins/BasePlugin.ts +1 -1
- package/src/plugins/index.ts +248 -133
- package/src/structures/FilterManager.ts +3 -3
- package/src/structures/Player.ts +358 -94
- package/src/structures/PlayerManager.ts +535 -129
- package/src/structures/Queue.ts +300 -55
- package/src/types/index.ts +52 -10
- package/src/types/persistence.ts +83 -0
- package/src/types/plugin.ts +1 -1
package/AI-Guide.md
CHANGED
|
@@ -1,725 +1,464 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🤖 AI Guide for ZiPlayer
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
##
|
|
17
|
+
## 🎯 Project Overview
|
|
28
18
|
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
##
|
|
65
|
+
## 🧠 Core Concepts
|
|
79
66
|
|
|
80
|
-
###
|
|
67
|
+
### 1. Player Lifecycle
|
|
81
68
|
|
|
82
69
|
```typescript
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
###
|
|
86
|
+
### 3. Event Flow
|
|
101
87
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
queueEnd → leave
|
|
92
|
+
```
|
|
111
93
|
|
|
112
|
-
###
|
|
94
|
+
### 4. Plugin Priority & Fallback
|
|
113
95
|
|
|
114
96
|
```typescript
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
+
---
|
|
127
113
|
|
|
128
|
-
|
|
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
|
-
###
|
|
116
|
+
### PlayerManager
|
|
117
|
+
|
|
118
|
+
#### Constructor Options
|
|
150
119
|
|
|
151
120
|
```typescript
|
|
152
|
-
|
|
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
|
-
|
|
133
|
+
#### Key Methods
|
|
156
134
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
###
|
|
145
|
+
### Player
|
|
175
146
|
|
|
176
|
-
|
|
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
|
-
|
|
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.
|
|
205
|
-
player.
|
|
206
|
-
|
|
207
|
-
player.
|
|
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
|
-
###
|
|
177
|
+
### Queue
|
|
228
178
|
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
const removed = player.queue.remove(2); // removes track at index 2
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### Destroying
|
|
198
|
+
#### Properties
|
|
243
199
|
|
|
244
200
|
```typescript
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
//
|
|
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
|
-
###
|
|
209
|
+
### FilterManager
|
|
251
210
|
|
|
252
211
|
```typescript
|
|
253
|
-
|
|
254
|
-
player.
|
|
255
|
-
player.
|
|
256
|
-
```
|
|
212
|
+
// Apply filters
|
|
213
|
+
await player.filter.applyFilter("bassboost");
|
|
214
|
+
await player.filter.applyFilters(["bassboost", "nightcore"]);
|
|
257
215
|
|
|
258
|
-
|
|
216
|
+
// Available filters
|
|
217
|
+
// bassboost, trebleboost, nightcore, lofi, vaporwave,
|
|
218
|
+
// echo, reverb, chorus, karaoke, normalize, compressor, limiter
|
|
259
219
|
|
|
260
|
-
|
|
261
|
-
await player.filter.applyFilter("bassboost");
|
|
262
|
-
await player.filter.removeFilter("bassboost");
|
|
220
|
+
// Clear filters
|
|
263
221
|
await player.filter.clearAll();
|
|
264
|
-
player.filter.
|
|
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
|
-
|
|
224
|
+
// Get current filters
|
|
225
|
+
const filterString = player.filter.getFilterString(); // "bassboost,nightcore"
|
|
226
|
+
```
|
|
273
227
|
|
|
274
|
-
|
|
228
|
+
### PersistenceManager
|
|
275
229
|
|
|
276
|
-
|
|
230
|
+
#### Options
|
|
277
231
|
|
|
278
232
|
```typescript
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
244
|
+
#### Methods
|
|
286
245
|
|
|
287
246
|
```typescript
|
|
288
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
257
|
+
---
|
|
310
258
|
|
|
311
|
-
|
|
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
|
-
|
|
263
|
+
```typescript
|
|
264
|
+
import { Client, GatewayIntentBits } from "discord.js";
|
|
265
|
+
import { PlayerManager } from "ziplayer";
|
|
266
|
+
import { YouTubePlugin, SpotifyPlugin } from "@ziplayer/plugin";
|
|
321
267
|
|
|
322
|
-
|
|
268
|
+
const client = new Client({
|
|
269
|
+
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
|
|
270
|
+
});
|
|
323
271
|
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
291
|
+
if (!player.connection) {
|
|
292
|
+
await player.connect(voiceChannel);
|
|
293
|
+
}
|
|
337
294
|
|
|
338
|
-
|
|
339
|
-
|
|
295
|
+
const query = msg.content.slice(6);
|
|
296
|
+
await player.play(query, msg.author.id);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
340
299
|
```
|
|
341
300
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
#### SpotifyPlugin
|
|
301
|
+
### 2. Progress Bar with Time Format
|
|
345
302
|
|
|
346
303
|
```typescript
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
320
|
+
### 3. Queue Management Commands
|
|
354
321
|
|
|
355
322
|
```typescript
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
326
|
+
// Move track to front
|
|
327
|
+
player.queue.move(5, 0);
|
|
366
328
|
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
332
|
+
// Jump back 2 tracks
|
|
333
|
+
await player.queue.jumpToHistory(2);
|
|
372
334
|
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
### Writing a custom plugin
|
|
339
|
+
### 4. Custom Plugin Implementation
|
|
384
340
|
|
|
385
341
|
```typescript
|
|
386
|
-
import { BasePlugin, Track,
|
|
387
|
-
import { Readable } from "stream";
|
|
342
|
+
import { BasePlugin, Track, StreamInfo } from "ziplayer";
|
|
388
343
|
|
|
389
|
-
|
|
390
|
-
name = "
|
|
391
|
-
|
|
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("
|
|
349
|
+
return query.startsWith("custom:");
|
|
396
350
|
}
|
|
397
351
|
|
|
398
352
|
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
## 8. Extensions
|
|
372
|
+
import { BaseExtension, ExtensionContext } from "ziplayer";
|
|
446
373
|
|
|
447
|
-
|
|
374
|
+
class LoggerExtension extends BaseExtension {
|
|
375
|
+
name = "Logger";
|
|
376
|
+
priority = 10;
|
|
448
377
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
391
|
+
### 6. Event Handling
|
|
480
392
|
|
|
481
393
|
```typescript
|
|
482
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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("
|
|
502
|
-
|
|
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
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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
|
-
##
|
|
417
|
+
## 🐛 Troubleshooting
|
|
636
418
|
|
|
637
|
-
###
|
|
419
|
+
### Common Issues
|
|
638
420
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
###
|
|
430
|
+
### Debug Mode
|
|
663
431
|
|
|
664
432
|
```typescript
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
438
|
+
// Or check debug flag
|
|
439
|
+
if (manager.debugEnabled) {
|
|
440
|
+
// Debug-specific logic
|
|
695
441
|
}
|
|
442
|
+
```
|
|
696
443
|
|
|
697
|
-
|
|
698
|
-
stream: Readable;
|
|
699
|
-
type: "webm/opus" | "ogg/opus" | "arbitrary";
|
|
700
|
-
metadata?: Record<string, any>;
|
|
701
|
-
}
|
|
444
|
+
### Performance Tips
|
|
702
445
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
##
|
|
454
|
+
## 📝 Code Examples
|
|
716
455
|
|
|
717
|
-
###
|
|
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,
|
|
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(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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.
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
555
|
+
### Import Paths
|
|
791
556
|
|
|
792
557
|
```typescript
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
// ZiPlayer auto-fetches related tracks via pluginManager.getRelatedTracks()
|
|
796
|
-
```
|
|
558
|
+
// Core
|
|
559
|
+
import { PlayerManager, Player, Queue } from "ziplayer";
|
|
797
560
|
|
|
798
|
-
|
|
561
|
+
// Types
|
|
562
|
+
import type { Track, SearchResult, LoopMode, StreamInfo } from "ziplayer";
|
|
799
563
|
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
567
|
+
// infinity plugin support stream audio from YouTube, TikTok, Instagram, Twitter/X, SoundCloud, Reddit, Twitch, Bilibili, and 1000+ other sites
|
|
807
568
|
|
|
808
|
-
|
|
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
|
-
//
|
|
826
|
-
|
|
571
|
+
// Extensions (external package)
|
|
572
|
+
import { voiceExt, lyricsExt, lavalinkExt } from "@ziplayer/extension";
|
|
827
573
|
```
|
|
828
574
|
|
|
829
|
-
###
|
|
575
|
+
### Type Definitions
|
|
830
576
|
|
|
831
577
|
```typescript
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
##
|
|
599
|
+
## 📖 Additional Resources
|
|
904
600
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
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._
|