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