@students-dev/audify-js 1.0.0 → 1.0.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -310
  3. package/dist/AudioEngine.js +232 -0
  4. package/dist/cjs/index.js +1497 -1392
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/constants/index.js +35 -0
  7. package/dist/engine/Filters.js +137 -0
  8. package/dist/engine/MockAudioContext.js +53 -0
  9. package/dist/engine/Player.js +209 -0
  10. package/dist/esm/index.js +1490 -1389
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/events/EventBus.js +61 -0
  13. package/dist/index.js +18 -0
  14. package/dist/interfaces/index.js +1 -0
  15. package/dist/plugins/Plugin.js +27 -0
  16. package/dist/plugins/PluginManager.js +106 -0
  17. package/dist/providers/LavalinkProvider.js +81 -0
  18. package/dist/providers/LocalProvider.js +70 -0
  19. package/dist/providers/ProviderRegistry.js +20 -0
  20. package/dist/providers/SpotifyProvider.js +59 -0
  21. package/dist/providers/YouTubeProvider.js +48 -0
  22. package/dist/queue/Queue.js +186 -0
  23. package/dist/queue/Track.js +54 -0
  24. package/dist/types/AudioEngine.d.ts +107 -0
  25. package/dist/types/constants/index.d.ts +39 -0
  26. package/dist/types/engine/AudioEngine.d.ts +44 -1
  27. package/dist/types/engine/Filters.d.ts +25 -24
  28. package/dist/types/engine/MockAudioContext.d.ts +43 -0
  29. package/dist/types/engine/Player.d.ts +25 -21
  30. package/dist/types/events/EventBus.d.ts +17 -15
  31. package/dist/types/index.d.ts +17 -13
  32. package/dist/types/interfaces/index.d.ts +31 -0
  33. package/dist/types/plugins/Plugin.d.ts +11 -43
  34. package/dist/types/plugins/PluginManager.d.ts +19 -19
  35. package/dist/types/providers/LavalinkProvider.d.ts +17 -0
  36. package/dist/types/providers/LocalProvider.d.ts +11 -22
  37. package/dist/types/providers/ProviderRegistry.d.ts +10 -0
  38. package/dist/types/providers/SpotifyProvider.d.ts +14 -0
  39. package/dist/types/providers/YouTubeProvider.d.ts +11 -28
  40. package/dist/types/queue/Queue.d.ts +28 -22
  41. package/dist/types/queue/Track.d.ts +18 -16
  42. package/dist/types/utils/Logger.d.ts +12 -16
  43. package/dist/types/utils/Metadata.d.ts +16 -15
  44. package/dist/types/utils/Probe.d.ts +7 -7
  45. package/dist/types/utils/Time.d.ts +9 -9
  46. package/dist/utils/Logger.js +59 -0
  47. package/dist/utils/Metadata.js +90 -0
  48. package/dist/utils/Probe.js +59 -0
  49. package/dist/utils/Time.js +54 -0
  50. package/package.json +19 -9
package/dist/esm/index.js CHANGED
@@ -1,1477 +1,1578 @@
1
1
  import { promises } from 'fs';
2
2
  import { extname } from 'path';
3
+ import SpotifyWebApi from 'spotify-web-api-node';
4
+ import { LavalinkManager } from 'lavalink-client';
3
5
 
4
- /**
5
- * Loop modes for playback
6
- */
7
- const LOOP_MODES$1 = {
8
- OFF: 'off',
9
- TRACK: 'track',
10
- QUEUE: 'queue'
6
+ const EVENTS = {
7
+ READY: 'ready',
8
+ ERROR: 'error',
9
+ PLAY: 'play',
10
+ PAUSE: 'pause',
11
+ STOP: 'stop',
12
+ TRACK_START: 'trackStart',
13
+ TRACK_END: 'trackEnd',
14
+ TRACK_ADD: 'trackAdd',
15
+ TRACK_REMOVE: 'trackRemove',
16
+ QUEUE_UPDATE: 'queueUpdate',
17
+ FILTER_APPLIED: 'filterApplied',
18
+ VOLUME_CHANGE: 'volumeChange',
19
+ SEEK: 'seek'
11
20
  };
12
-
13
- /**
14
- * Repeat modes (alias for loop modes)
15
- */
16
- const REPEAT_MODES = LOOP_MODES$1;
17
-
18
- /**
19
- * Filter types
20
- */
21
- const FILTER_TYPES = {
22
- BASSBOOST: 'bassboost',
23
- TREBLEBOOST: 'trebleboost',
24
- NIGHTCORE: 'nightcore',
25
- VAPORWAVE: 'vaporwave',
26
- ROTATE_8D: '8d',
27
- PITCH: 'pitch',
28
- SPEED: 'speed',
29
- REVERB: 'reverb'
21
+ const LOOP_MODES = {
22
+ OFF: 'off',
23
+ TRACK: 'track',
24
+ QUEUE: 'queue'
30
25
  };
31
-
32
- /**
33
- * Event types
34
- */
35
- const EVENTS = {
36
- READY: 'ready',
37
- PLAY: 'play',
38
- PAUSE: 'pause',
39
- STOP: 'stop',
40
- ERROR: 'error',
41
- QUEUE_EMPTY: 'queueEmpty',
42
- TRACK_START: 'trackStart',
43
- TRACK_END: 'trackEnd',
44
- FILTER_APPLIED: 'filterApplied',
45
- TRACK_ADD: 'trackAdd',
46
- TRACK_REMOVE: 'trackRemove',
47
- SHUFFLE: 'shuffle',
48
- CLEAR: 'clear'
26
+ const PLAYER_STATES = {
27
+ IDLE: 'idle',
28
+ PLAYING: 'playing',
29
+ PAUSED: 'paused',
30
+ BUFFERING: 'buffering'
31
+ };
32
+ const FILTER_TYPES = {
33
+ BASSBOOST: 'bassboost',
34
+ NIGHTCORE: 'nightcore',
35
+ VAPORWAVE: 'vaporwave',
36
+ ROTATE_8D: '8d',
37
+ PITCH: 'pitch',
38
+ SPEED: 'speed',
39
+ REVERB: 'reverb'
49
40
  };
50
41
 
51
42
  /**
52
43
  * Simple event emitter for handling events
53
44
  */
54
45
  class EventBus {
55
- constructor() {
56
- this.events = {};
57
- }
58
-
59
- /**
60
- * Register an event listener
61
- * @param {string} event - Event name
62
- * @param {Function} callback - Callback function
63
- */
64
- on(event, callback) {
65
- if (!this.events[event]) {
66
- this.events[event] = [];
67
- }
68
- this.events[event].push(callback);
69
- }
70
-
71
- /**
72
- * Remove an event listener
73
- * @param {string} event - Event name
74
- * @param {Function} callback - Callback function
75
- */
76
- off(event, callback) {
77
- if (!this.events[event]) return;
78
- this.events[event] = this.events[event].filter(cb => cb !== callback);
79
- }
80
-
81
- /**
82
- * Emit an event
83
- * @param {string} event - Event name
84
- * @param {*} data - Data to pass to listeners
85
- */
86
- emit(event, data) {
87
- if (!this.events[event]) return;
88
- this.events[event].forEach(callback => {
89
- try {
90
- callback(data);
91
- } catch (error) {
92
- console.error(`Error in event listener for ${event}:`, error);
93
- }
94
- });
95
- }
96
-
97
- /**
98
- * Remove all listeners for an event
99
- * @param {string} event - Event name
100
- */
101
- removeAllListeners(event) {
102
- delete this.events[event];
103
- }
46
+ constructor() {
47
+ this.events = {};
48
+ }
49
+ /**
50
+ * Register an event listener
51
+ * @param event - Event name
52
+ * @param callback - Callback function
53
+ */
54
+ on(event, callback) {
55
+ if (!this.events[event]) {
56
+ this.events[event] = [];
57
+ }
58
+ this.events[event].push(callback);
59
+ }
60
+ /**
61
+ * Remove an event listener
62
+ * @param event - Event name
63
+ * @param callback - Callback function
64
+ */
65
+ off(event, callback) {
66
+ if (!this.events[event])
67
+ return;
68
+ this.events[event] = this.events[event].filter(cb => cb !== callback);
69
+ }
70
+ /**
71
+ * Emit an event
72
+ * @param event - Event name
73
+ * @param data - Data to pass to listeners
74
+ */
75
+ emit(event, data) {
76
+ if (!this.events[event])
77
+ return;
78
+ this.events[event].forEach(callback => {
79
+ try {
80
+ callback(data);
81
+ }
82
+ catch (error) {
83
+ console.error(`Error in event listener for ${event}:`, error);
84
+ }
85
+ });
86
+ }
87
+ /**
88
+ * Remove all listeners for an event
89
+ * @param event - Event name
90
+ */
91
+ removeAllListeners(event) {
92
+ delete this.events[event];
93
+ }
94
+ /**
95
+ * Get all listeners for an event
96
+ * @param event - Event name
97
+ * @returns Array of listeners
98
+ */
99
+ listeners(event) {
100
+ return this.events[event] || [];
101
+ }
102
+ }
104
103
 
105
- /**
106
- * Get all listeners for an event
107
- * @param {string} event - Event name
108
- * @returns {Function[]} Array of listeners
109
- */
110
- listeners(event) {
111
- return this.events[event] || [];
112
- }
104
+ class MockAudioContext {
105
+ constructor() {
106
+ this.state = 'running';
107
+ this.currentTime = 0;
108
+ this.startTime = Date.now();
109
+ this.updateTime();
110
+ }
111
+ updateTime() {
112
+ if (this.state === 'running') {
113
+ const diff = (Date.now() - this.startTime) / 1000;
114
+ this.currentTime = diff;
115
+ }
116
+ // Simulate clock
117
+ if (typeof setTimeout !== 'undefined') {
118
+ setTimeout(() => this.updateTime(), 100);
119
+ }
120
+ }
121
+ createGain() { return { connect: () => { }, gain: { value: 0 } }; }
122
+ createBiquadFilter() { return { connect: () => { }, frequency: { value: 0 }, gain: { value: 0 } }; }
123
+ createPanner() { return { connect: () => { } }; }
124
+ createConvolver() { return { connect: () => { } }; }
125
+ createBufferSource() {
126
+ return new MockAudioBufferSource(this);
127
+ }
128
+ async decodeAudioData(buffer) {
129
+ return { duration: 5 }; // Mock 5 seconds duration
130
+ }
131
+ suspend() { this.state = 'suspended'; }
132
+ resume() { this.state = 'running'; this.startTime = Date.now() - (this.currentTime * 1000); }
133
+ close() { this.state = 'closed'; }
134
+ }
135
+ class MockAudioBufferSource {
136
+ constructor(context) {
137
+ this.buffer = null;
138
+ this.onended = null;
139
+ this.context = context;
140
+ }
141
+ connect() { }
142
+ start(when = 0, offset = 0) {
143
+ // Simulate playback duration
144
+ const duration = this.buffer ? this.buffer.duration : 0;
145
+ setTimeout(() => {
146
+ if (this.onended)
147
+ this.onended();
148
+ }, duration * 1000); // Speed up for tests? No, keep real time or fast?
149
+ // 5 seconds mock duration might be too long for quick examples.
150
+ // Let's make it 1 second for examples unless buffer says otherwise.
151
+ }
152
+ stop() {
153
+ if (this.onended)
154
+ this.onended();
155
+ }
113
156
  }
114
157
 
115
158
  /**
116
159
  * Audio player with playback controls
117
160
  */
118
161
  class Player {
119
- constructor(audioEngine) {
120
- this.audioEngine = audioEngine;
121
- this.audioContext = audioEngine.audioContext;
122
- this.source = null;
123
- this.isPlaying = false;
124
- this.currentTime = 0;
125
- this.duration = 0;
126
- this.volume = 1;
127
- this.loopMode = LOOP_MODES$1.OFF;
128
- this.eventBus = new EventBus();
129
- }
130
-
131
- /**
132
- * Play audio
133
- * @param {Track} track - Track to play
134
- */
135
- async play(track) {
136
- if (!track) return;
137
-
138
- try {
139
- await this.loadTrack(track);
140
- this.source.start(0);
141
- this.isPlaying = true;
142
- this.eventBus.emit(EVENTS.PLAY, track);
143
- this.eventBus.emit(EVENTS.TRACK_START, track);
144
- } catch (error) {
145
- this.eventBus.emit(EVENTS.ERROR, error);
146
- }
147
- }
148
-
149
- /**
150
- * Pause playback
151
- */
152
- pause() {
153
- if (this.source && this.isPlaying) {
154
- this.source.stop();
155
- this.isPlaying = false;
156
- this.eventBus.emit(EVENTS.PAUSE);
157
- }
158
- }
159
-
160
- /**
161
- * Resume playback
162
- */
163
- resume() {
164
- if (!this.isPlaying && this.source) {
165
- // Would need to recreate source for resume
166
- this.eventBus.emit(EVENTS.PLAY);
167
- }
168
- }
169
-
170
- /**
171
- * Stop playback
172
- */
173
- stop() {
174
- if (this.source) {
175
- this.source.stop();
176
- this.source = null;
177
- this.isPlaying = false;
178
- this.currentTime = 0;
179
- this.eventBus.emit(EVENTS.STOP);
180
- }
181
- }
182
-
183
- /**
184
- * Seek to position
185
- * @param {number} time - Time in seconds
186
- */
187
- seek(time) {
188
- // In Web Audio API, seeking requires buffer manipulation
189
- this.currentTime = Math.max(0, Math.min(time, this.duration));
190
- }
191
-
192
- /**
193
- * Set volume
194
- * @param {number} volume - Volume level (0-1)
195
- */
196
- setVolume(volume) {
197
- this.volume = Math.max(0, Math.min(1, volume));
198
- // Apply to gain node if exists
199
- }
200
-
201
- /**
202
- * Set loop mode
203
- * @param {string} mode - Loop mode
204
- */
205
- setLoopMode(mode) {
206
- this.loopMode = mode;
207
- }
208
-
209
- /**
210
- * Load track into player
211
- * @param {Track} track - Track to load
212
- */
213
- async loadTrack(track) {
214
- if (!this.audioContext) throw new Error('AudioContext not available');
215
-
216
- const response = await fetch(track.url);
217
- const arrayBuffer = await response.arrayBuffer();
218
- const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
219
-
220
- this.source = this.audioContext.createBufferSource();
221
- this.source.buffer = audioBuffer;
222
- this.duration = audioBuffer.duration;
223
-
224
- // Connect through filters
225
- this.audioEngine.filters.connect(this.source, this.audioContext.destination);
226
-
227
- // Handle end of track
228
- this.source.onended = () => {
229
- this.isPlaying = false;
230
- this.eventBus.emit(EVENTS.TRACK_END, track);
231
- this.handleTrackEnd();
232
- };
233
- }
234
-
235
- /**
236
- * Handle track end based on loop mode
237
- */
238
- handleTrackEnd() {
239
- switch (this.loopMode) {
240
- case LOOP_MODES$1.TRACK:
241
- // Replay current track
242
- break;
243
- case LOOP_MODES$1.QUEUE:
244
- // Play next in queue
245
- this.audioEngine.queue.getNext();
246
- break;
247
- case LOOP_MODES$1.OFF:
248
- }
249
- }
250
-
251
- /**
252
- * Get current playback state
253
- * @returns {Object} State object
254
- */
255
- getState() {
256
- return {
257
- isPlaying: this.isPlaying,
258
- currentTime: this.currentTime,
259
- duration: this.duration,
260
- volume: this.volume,
261
- loopMode: this.loopMode
262
- };
263
- }
162
+ constructor(audioEngine) {
163
+ this.audioEngine = audioEngine;
164
+ let AudioContextClass;
165
+ if (typeof window !== 'undefined') {
166
+ AudioContextClass = window.AudioContext || window.webkitAudioContext;
167
+ }
168
+ else {
169
+ AudioContextClass = global.AudioContext;
170
+ }
171
+ if (AudioContextClass) {
172
+ this.audioContext = new AudioContextClass();
173
+ }
174
+ else {
175
+ this.audioContext = new MockAudioContext();
176
+ }
177
+ this.source = null;
178
+ this.isPlaying = false;
179
+ this.currentTime = 0;
180
+ this.duration = 0;
181
+ this.volume = 1;
182
+ this.loopMode = LOOP_MODES.OFF;
183
+ this.eventBus = new EventBus();
184
+ }
185
+ /**
186
+ * Play audio track
187
+ * @param track - Track to play
188
+ */
189
+ async play(track) {
190
+ if (!track)
191
+ return;
192
+ // Reset state
193
+ this.stop();
194
+ try {
195
+ this.eventBus.emit(EVENTS.PLAY, track);
196
+ // Check providers via registry
197
+ const provider = this.audioEngine.getProvider(track.source || 'local');
198
+ if (provider) {
199
+ await provider.play(track);
200
+ }
201
+ else {
202
+ // Fallback to direct URL playback if no specific provider found
203
+ await this.playStream(track);
204
+ }
205
+ this.eventBus.emit(EVENTS.TRACK_START, track);
206
+ }
207
+ catch (error) {
208
+ console.error(error);
209
+ this.eventBus.emit(EVENTS.ERROR, error);
210
+ }
211
+ }
212
+ /**
213
+ * Play audio from URL/Stream directly
214
+ * This is called by Providers or as fallback
215
+ * @param track - Track object with URL
216
+ */
217
+ async playStream(track) {
218
+ if (!this.audioContext)
219
+ throw new Error('AudioContext not available');
220
+ // If already playing, stop
221
+ if (this.source) {
222
+ this.source.stop();
223
+ }
224
+ try {
225
+ // Fetch audio data
226
+ // For Node.js (Mock), we might fail to fetch if it's a real URL
227
+ // If MockAudioContext is used, we probably want to skip fetch?
228
+ // Or Mock fetch?
229
+ // In Node environment, fetch is global in recent versions (v18+)
230
+ // But if we are mocking, we can't really "decode" the buffer from a remote stream easily without logic.
231
+ // My MockAudioContext.decodeAudioData returns a mock buffer.
232
+ let audioBuffer;
233
+ // Check if real fetch is feasible
234
+ if (this.audioContext instanceof MockAudioContext) {
235
+ // Mock fetch behavior if needed or just create dummy buffer
236
+ audioBuffer = await this.audioContext.decodeAudioData(new ArrayBuffer(0));
237
+ }
238
+ else {
239
+ const response = await fetch(track.url);
240
+ const arrayBuffer = await response.arrayBuffer();
241
+ audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
242
+ }
243
+ this.source = this.audioContext.createBufferSource();
244
+ this.source.buffer = audioBuffer;
245
+ this.duration = audioBuffer.duration;
246
+ // Connect through filters
247
+ this.audioEngine.filters.connect(this.source, this.audioContext.destination);
248
+ // Handle end of track
249
+ this.source.onended = () => {
250
+ this.isPlaying = false;
251
+ this.eventBus.emit(EVENTS.TRACK_END, track);
252
+ this.handleTrackEnd();
253
+ };
254
+ this.source.start(0);
255
+ this.isPlaying = true;
256
+ }
257
+ catch (error) {
258
+ throw new Error(`Failed to play stream: ${error}`);
259
+ }
260
+ }
261
+ /**
262
+ * Pause playback
263
+ */
264
+ pause() {
265
+ if (this.audioContext.state === 'running') {
266
+ this.audioContext.suspend();
267
+ this.isPlaying = false;
268
+ this.eventBus.emit(EVENTS.PAUSE);
269
+ }
270
+ }
271
+ /**
272
+ * Resume playback
273
+ */
274
+ resume() {
275
+ if (this.audioContext.state === 'suspended') {
276
+ this.audioContext.resume();
277
+ this.isPlaying = true;
278
+ this.eventBus.emit(EVENTS.PLAY);
279
+ }
280
+ }
281
+ /**
282
+ * Stop playback
283
+ */
284
+ stop() {
285
+ if (this.source) {
286
+ try {
287
+ this.source.stop();
288
+ }
289
+ catch (e) {
290
+ // Ignore if already stopped
291
+ }
292
+ this.source = null;
293
+ }
294
+ this.isPlaying = false;
295
+ this.currentTime = 0;
296
+ this.eventBus.emit(EVENTS.STOP);
297
+ }
298
+ /**
299
+ * Seek to position
300
+ * @param time - Time in seconds
301
+ */
302
+ seek(time) {
303
+ if (this.source && this.isPlaying) {
304
+ // TODO: Implement proper seek
305
+ console.warn('Seek not fully implemented for Web Audio BufferSource');
306
+ }
307
+ this.currentTime = Math.max(0, Math.min(time, this.duration));
308
+ }
309
+ /**
310
+ * Set volume
311
+ * @param volume - Volume level (0-1)
312
+ */
313
+ setVolume(volume) {
314
+ this.volume = Math.max(0, Math.min(1, volume));
315
+ }
316
+ /**
317
+ * Set loop mode
318
+ * @param mode - Loop mode
319
+ */
320
+ setLoopMode(mode) {
321
+ this.loopMode = mode;
322
+ }
323
+ /**
324
+ * Handle track end based on loop mode
325
+ */
326
+ handleTrackEnd() {
327
+ if (this.loopMode === LOOP_MODES.TRACK) {
328
+ // Replay current track
329
+ const current = this.audioEngine.queue.getCurrent();
330
+ if (current)
331
+ this.play(current);
332
+ }
333
+ else if (this.loopMode === LOOP_MODES.QUEUE) {
334
+ // Play next in queue
335
+ const next = this.audioEngine.queue.next(true);
336
+ if (next)
337
+ this.play(next);
338
+ }
339
+ else {
340
+ // Loop OFF: Play next or stop
341
+ const next = this.audioEngine.queue.next(false);
342
+ if (next) {
343
+ this.play(next);
344
+ }
345
+ else {
346
+ this.stop();
347
+ }
348
+ }
349
+ }
350
+ /**
351
+ * Get current playback state
352
+ * @returns State object
353
+ */
354
+ getState() {
355
+ return {
356
+ isPlaying: this.isPlaying,
357
+ currentTime: this.audioContext.currentTime,
358
+ duration: this.duration,
359
+ volume: this.volume,
360
+ loopMode: this.loopMode
361
+ };
362
+ }
264
363
  }
265
364
 
266
365
  /**
267
366
  * Audio filters and effects
268
367
  */
269
368
  class Filters {
270
- constructor(audioContext) {
271
- this.audioContext = audioContext;
272
- this.filters = new Map();
273
- this.enabled = new Set();
274
- }
275
-
276
- /**
277
- * Apply filter
278
- * @param {string} type - Filter type
279
- * @param {Object} options - Filter options
280
- */
281
- apply(type, options = {}) {
282
- if (!this.audioContext) return;
283
-
284
- switch (type) {
285
- case FILTER_TYPES.BASSBOOST:
286
- this.applyBassBoost(options);
287
- break;
288
- case FILTER_TYPES.NIGHTCORE:
289
- this.applyNightcore(options);
290
- break;
291
- case FILTER_TYPES.VAPORWAVE:
292
- this.applyVaporwave(options);
293
- break;
294
- case FILTER_TYPES.ROTATE_8D:
295
- this.apply8DRotate(options);
296
- break;
297
- case FILTER_TYPES.PITCH:
298
- this.applyPitch(options);
299
- break;
300
- case FILTER_TYPES.SPEED:
301
- this.applySpeed(options);
302
- break;
303
- case FILTER_TYPES.REVERB:
304
- this.applyReverb(options);
305
- break;
369
+ constructor(audioContext) {
370
+ this.audioContext = audioContext;
371
+ this.filters = new Map();
372
+ this.enabled = new Set();
373
+ }
374
+ /**
375
+ * Apply filter
376
+ * @param type - Filter type
377
+ * @param options - Filter options
378
+ */
379
+ apply(type, options = {}) {
380
+ if (!this.audioContext)
381
+ return;
382
+ switch (type) {
383
+ case FILTER_TYPES.BASSBOOST:
384
+ this.applyBassBoost(options);
385
+ break;
386
+ case FILTER_TYPES.NIGHTCORE:
387
+ this.applyNightcore(options);
388
+ break;
389
+ case FILTER_TYPES.VAPORWAVE:
390
+ this.applyVaporwave(options);
391
+ break;
392
+ case FILTER_TYPES.ROTATE_8D:
393
+ this.apply8DRotate(options);
394
+ break;
395
+ case FILTER_TYPES.PITCH:
396
+ this.applyPitch(options);
397
+ break;
398
+ case FILTER_TYPES.SPEED:
399
+ this.applySpeed(options);
400
+ break;
401
+ case FILTER_TYPES.REVERB:
402
+ this.applyReverb(options);
403
+ break;
404
+ }
405
+ this.enabled.add(type);
406
+ }
407
+ /**
408
+ * Remove filter
409
+ * @param type - Filter type
410
+ */
411
+ remove(type) {
412
+ if (this.filters.has(type)) {
413
+ const filter = this.filters.get(type);
414
+ filter?.disconnect();
415
+ this.filters.delete(type);
416
+ this.enabled.delete(type);
417
+ }
418
+ }
419
+ /**
420
+ * Clear all filters
421
+ */
422
+ clear() {
423
+ this.filters.forEach(filter => filter.disconnect());
424
+ this.filters.clear();
425
+ this.enabled.clear();
426
+ }
427
+ /**
428
+ * Check if filter is enabled
429
+ * @param type - Filter type
430
+ * @returns Is enabled
431
+ */
432
+ isEnabled(type) {
433
+ return this.enabled.has(type);
434
+ }
435
+ /**
436
+ * Get enabled filters
437
+ * @returns Enabled filter types
438
+ */
439
+ getEnabled() {
440
+ return new Set(this.enabled);
441
+ }
442
+ // Filter implementations
443
+ applyBassBoost(options = {}) {
444
+ const gain = options.gain || 1.5;
445
+ const filter = this.audioContext.createBiquadFilter();
446
+ filter.type = 'lowshelf';
447
+ filter.frequency.value = 200;
448
+ filter.gain.value = gain * 10;
449
+ this.filters.set(FILTER_TYPES.BASSBOOST, filter);
450
+ }
451
+ applyNightcore(options = {}) {
452
+ const rate = options.rate || 1.2;
453
+ // Nightcore is pitch + speed up
454
+ this.applyPitch({ pitch: rate });
455
+ this.applySpeed({ speed: rate });
456
+ }
457
+ applyVaporwave(options = {}) {
458
+ const rate = options.rate || 0.8;
459
+ this.applyPitch({ pitch: rate });
460
+ this.applySpeed({ speed: rate });
461
+ }
462
+ apply8DRotate(options = {}) {
463
+ // 8D audio effect using panner
464
+ const panner = this.audioContext.createPanner();
465
+ panner.panningModel = 'HRTF';
466
+ // Would need to animate the position for rotation
467
+ this.filters.set(FILTER_TYPES.ROTATE_8D, panner);
468
+ }
469
+ applyPitch(options = {}) {
470
+ options.pitch || 1;
471
+ // In Web Audio API, pitch shifting requires AudioWorklet or external library
472
+ // For simplicity, we'll use a basic implementation or just log it
473
+ // console.warn('Pitch shifting requires AudioWorklet in modern browsers');
474
+ }
475
+ applySpeed(options = {}) {
476
+ options.speed || 1;
477
+ // Speed change affects playback rate
478
+ // This would be handled in the player
479
+ // console.log(`Speed filter applied: ${speed}x`);
480
+ }
481
+ applyReverb(options = {}) {
482
+ const convolver = this.audioContext.createConvolver();
483
+ // Would need an impulse response for reverb
484
+ // For simplicity, create a basic reverb
485
+ this.filters.set(FILTER_TYPES.REVERB, convolver);
486
+ }
487
+ /**
488
+ * Connect filters to audio node
489
+ * @param input - Input node
490
+ * @param output - Output node
491
+ */
492
+ connect(input, output) {
493
+ let currentNode = input;
494
+ this.filters.forEach(filter => {
495
+ currentNode.connect(filter);
496
+ currentNode = filter;
497
+ });
498
+ currentNode.connect(output);
306
499
  }
307
-
308
- this.enabled.add(type);
309
- }
310
-
311
- /**
312
- * Remove filter
313
- * @param {string} type - Filter type
314
- */
315
- remove(type) {
316
- if (this.filters.has(type)) {
317
- const filter = this.filters.get(type);
318
- filter.disconnect();
319
- this.filters.delete(type);
320
- this.enabled.delete(type);
321
- }
322
- }
323
-
324
- /**
325
- * Clear all filters
326
- */
327
- clear() {
328
- this.filters.forEach(filter => filter.disconnect());
329
- this.filters.clear();
330
- this.enabled.clear();
331
- }
332
-
333
- /**
334
- * Check if filter is enabled
335
- * @param {string} type - Filter type
336
- * @returns {boolean} Is enabled
337
- */
338
- isEnabled(type) {
339
- return this.enabled.has(type);
340
- }
341
-
342
- /**
343
- * Get enabled filters
344
- * @returns {Set} Enabled filter types
345
- */
346
- getEnabled() {
347
- return new Set(this.enabled);
348
- }
349
-
350
- // Filter implementations
351
- applyBassBoost(options = {}) {
352
- const gain = options.gain || 1.5;
353
- const filter = this.audioContext.createBiquadFilter();
354
- filter.type = 'lowshelf';
355
- filter.frequency.value = 200;
356
- filter.gain.value = gain * 10;
357
- this.filters.set(FILTER_TYPES.BASSBOOST, filter);
358
- }
359
-
360
- applyNightcore(options = {}) {
361
- const rate = options.rate || 1.2;
362
- // Nightcore is pitch + speed up
363
- this.applyPitch({ pitch: rate });
364
- this.applySpeed({ speed: rate });
365
- }
366
-
367
- applyVaporwave(options = {}) {
368
- const rate = options.rate || 0.8;
369
- this.applyPitch({ pitch: rate });
370
- this.applySpeed({ speed: rate });
371
- }
372
-
373
- apply8DRotate(options = {}) {
374
- // 8D audio effect using panner
375
- const panner = this.audioContext.createPanner();
376
- panner.panningModel = 'HRTF';
377
- // Would need to animate the position for rotation
378
- this.filters.set(FILTER_TYPES.ROTATE_8D, panner);
379
- }
380
-
381
- applyPitch(options = {}) {
382
- options.pitch || 1;
383
- // In Web Audio API, pitch shifting requires AudioWorklet or external library
384
- // For simplicity, we'll use a basic implementation
385
- console.warn('Pitch shifting requires AudioWorklet in modern browsers');
386
- }
387
-
388
- applySpeed(options = {}) {
389
- const speed = options.speed || 1;
390
- // Speed change affects playback rate
391
- // This would be handled in the player
392
- console.log(`Speed filter applied: ${speed}x`);
393
- }
394
-
395
- applyReverb(options = {}) {
396
- const convolver = this.audioContext.createConvolver();
397
- // Would need an impulse response for reverb
398
- // For simplicity, create a basic reverb
399
- this.filters.set(FILTER_TYPES.REVERB, convolver);
400
- }
401
-
402
- /**
403
- * Connect filters to audio node
404
- * @param {AudioNode} input - Input node
405
- * @param {AudioNode} output - Output node
406
- */
407
- connect(input, output) {
408
- let currentNode = input;
409
-
410
- this.filters.forEach(filter => {
411
- currentNode.connect(filter);
412
- currentNode = filter;
413
- });
414
-
415
- currentNode.connect(output);
416
- }
417
500
  }
418
501
 
419
502
  /**
420
503
  * Metadata parsing utilities
421
504
  */
422
505
  class MetadataUtils {
423
- /**
424
- * Extract basic metadata from URL or file path
425
- * @param {string} source - URL or file path
426
- * @returns {Object} Metadata object
427
- */
428
- static extract(source) {
429
- if (!source) return {};
430
-
431
- const metadata = {
432
- title: this.extractTitle(source),
433
- artist: null,
434
- duration: null,
435
- thumbnail: null
436
- };
437
-
438
- // For YouTube URLs
439
- if (source.includes('youtube.com') || source.includes('youtu.be')) {
440
- return this.extractYouTubeMetadata(source);
506
+ /**
507
+ * Extract basic metadata from URL or file path
508
+ * @param source - URL or file path
509
+ * @returns Metadata object
510
+ */
511
+ static extract(source) {
512
+ if (!source)
513
+ return {};
514
+ const metadata = {
515
+ title: this.extractTitle(source),
516
+ artist: undefined,
517
+ duration: undefined,
518
+ thumbnail: undefined
519
+ };
520
+ // For YouTube URLs
521
+ if (source.includes('youtube.com') || source.includes('youtu.be')) {
522
+ return this.extractYouTubeMetadata(source);
523
+ }
524
+ // For SoundCloud URLs
525
+ if (source.includes('soundcloud.com')) {
526
+ return this.extractSoundCloudMetadata(source);
527
+ }
528
+ // For local files
529
+ if (!source.startsWith('http')) {
530
+ return this.extractFileMetadata(source);
531
+ }
532
+ return metadata;
441
533
  }
442
-
443
- // For SoundCloud URLs
444
- if (source.includes('soundcloud.com')) {
445
- return this.extractSoundCloudMetadata(source);
534
+ /**
535
+ * Extract title from source
536
+ * @param source - Source string
537
+ * @returns Extracted title
538
+ */
539
+ static extractTitle(source) {
540
+ if (!source)
541
+ return 'Unknown Track';
542
+ // Try to extract from URL query params
543
+ try {
544
+ const url = new URL(source);
545
+ const title = url.searchParams.get('title') || url.searchParams.get('name');
546
+ if (title)
547
+ return decodeURIComponent(title);
548
+ }
549
+ catch { } // eslint-disable-line no-empty
550
+ // Extract from file path
551
+ const parts = source.split('/').pop()?.split('\\').pop();
552
+ if (parts) {
553
+ return parts.replace(/\.[^/.]+$/, ''); // Remove extension
554
+ }
555
+ return 'Unknown Track';
446
556
  }
447
-
448
- // For local files
449
- if (!source.startsWith('http')) {
450
- return this.extractFileMetadata(source);
557
+ /**
558
+ * Extract YouTube metadata (basic)
559
+ * @param url - YouTube URL
560
+ * @returns Metadata
561
+ */
562
+ static extractYouTubeMetadata(url) {
563
+ return {
564
+ title: 'YouTube Track',
565
+ source: 'youtube'
566
+ };
451
567
  }
452
-
453
- return metadata;
454
- }
455
-
456
- /**
457
- * Extract title from source
458
- * @param {string} source - Source string
459
- * @returns {string} Extracted title
460
- */
461
- static extractTitle(source) {
462
- if (!source) return 'Unknown Track';
463
-
464
- // Try to extract from URL query params
465
- try {
466
- const url = new URL(source);
467
- const title = url.searchParams.get('title') || url.searchParams.get('name');
468
- if (title) return decodeURIComponent(title);
469
- } catch {}
470
-
471
- // Extract from file path
472
- const parts = source.split('/').pop().split('\\').pop();
473
- if (parts) {
474
- return parts.replace(/\.[^/.]+$/, ''); // Remove extension
568
+ /**
569
+ * Extract SoundCloud metadata (basic)
570
+ * @param url - SoundCloud URL
571
+ * @returns Metadata
572
+ */
573
+ static extractSoundCloudMetadata(url) {
574
+ return {
575
+ title: 'SoundCloud Track',
576
+ source: 'soundcloud'
577
+ };
578
+ }
579
+ /**
580
+ * Extract file metadata (basic)
581
+ * @param path - File path
582
+ * @returns Metadata
583
+ */
584
+ static extractFileMetadata(path) {
585
+ const title = this.extractTitle(path);
586
+ return {
587
+ title,
588
+ source: 'local'
589
+ };
475
590
  }
476
-
477
- return 'Unknown Track';
478
- }
479
-
480
- /**
481
- * Extract YouTube metadata (basic)
482
- * @param {string} url - YouTube URL
483
- * @returns {Object} Metadata
484
- */
485
- static extractYouTubeMetadata(url) {
486
- // Basic extraction, in real implementation would fetch from API
487
- return {
488
- title: 'YouTube Track',
489
- artist: null,
490
- duration: null,
491
- thumbnail: null,
492
- source: 'youtube'
493
- };
494
- }
495
-
496
- /**
497
- * Extract SoundCloud metadata (basic)
498
- * @param {string} url - SoundCloud URL
499
- * @returns {Object} Metadata
500
- */
501
- static extractSoundCloudMetadata(url) {
502
- // Basic extraction
503
- return {
504
- title: 'SoundCloud Track',
505
- artist: null,
506
- duration: null,
507
- thumbnail: null,
508
- source: 'soundcloud'
509
- };
510
- }
511
-
512
- /**
513
- * Extract file metadata (basic)
514
- * @param {string} path - File path
515
- * @returns {Object} Metadata
516
- */
517
- static extractFileMetadata(path) {
518
- const title = this.extractTitle(path);
519
- return {
520
- title,
521
- artist: null,
522
- duration: null,
523
- thumbnail: null,
524
- source: 'local'
525
- };
526
- }
527
591
  }
528
592
 
529
593
  /**
530
594
  * Represents an audio track
531
595
  */
532
596
  class Track {
533
- /**
534
- * @param {string} url - Track URL or file path
535
- * @param {Object} options - Additional options
536
- */
537
- constructor(url, options = {}) {
538
- this.url = url;
539
- this.title = options.title || MetadataUtils.extract(url).title;
540
- this.artist = options.artist || null;
541
- this.duration = options.duration || null;
542
- this.thumbnail = options.thumbnail || null;
543
- this.metadata = options.metadata || {};
544
- this.id = options.id || Math.random().toString(36).substr(2, 9);
545
- }
546
-
547
- /**
548
- * Get track info
549
- * @returns {Object} Track information
550
- */
551
- getInfo() {
552
- return {
553
- id: this.id,
554
- url: this.url,
555
- title: this.title,
556
- artist: this.artist,
557
- duration: this.duration,
558
- thumbnail: this.thumbnail,
559
- metadata: this.metadata
560
- };
561
- }
562
-
563
- /**
564
- * Update track metadata
565
- * @param {Object} metadata - New metadata
566
- */
567
- updateMetadata(metadata) {
568
- Object.assign(this.metadata, metadata);
569
- }
570
-
571
- /**
572
- * Check if track is valid
573
- * @returns {boolean} Is valid
574
- */
575
- isValid() {
576
- return !!(this.url && this.title);
577
- }
597
+ /**
598
+ * @param url - Track URL or file path
599
+ * @param options - Additional options
600
+ */
601
+ constructor(url, options = {}) {
602
+ const extracted = MetadataUtils.extract(url);
603
+ this.url = url;
604
+ this.title = options.title || extracted.title || 'Unknown Title';
605
+ this.artist = options.artist || extracted.artist;
606
+ this.duration = options.duration || extracted.duration;
607
+ this.thumbnail = options.thumbnail || extracted.thumbnail;
608
+ this.source = options.source || extracted.source || 'unknown';
609
+ this.metadata = options.metadata || {};
610
+ this.id = options.id || Math.random().toString(36).substr(2, 9);
611
+ }
612
+ /**
613
+ * Get track info
614
+ * @returns Track information
615
+ */
616
+ getInfo() {
617
+ return {
618
+ id: this.id,
619
+ url: this.url,
620
+ title: this.title,
621
+ artist: this.artist,
622
+ duration: this.duration,
623
+ thumbnail: this.thumbnail,
624
+ source: this.source,
625
+ metadata: this.metadata
626
+ };
627
+ }
628
+ /**
629
+ * Update track metadata
630
+ * @param metadata - New metadata
631
+ */
632
+ updateMetadata(metadata) {
633
+ Object.assign(this, metadata);
634
+ if (metadata.metadata) {
635
+ Object.assign(this.metadata, metadata.metadata);
636
+ }
637
+ }
638
+ /**
639
+ * Check if track is valid
640
+ * @returns Is valid
641
+ */
642
+ isValid() {
643
+ return !!(this.url && this.title);
644
+ }
578
645
  }
579
646
 
580
647
  /**
581
648
  * Audio queue management
582
649
  */
583
650
  class Queue {
584
- constructor() {
585
- this.tracks = [];
586
- this.currentIndex = -1;
587
- this.eventBus = new EventBus();
588
- }
589
-
590
- /**
591
- * Add track(s) to queue
592
- * @param {Track|Track[]|string|string[]} tracks - Track(s) to add
593
- * @param {number} position - Position to insert (optional)
594
- */
595
- add(tracks, position) {
596
- const trackArray = Array.isArray(tracks) ? tracks : [tracks];
597
-
598
- const processedTracks = trackArray.map(track => {
599
- if (typeof track === 'string') {
600
- return new Track(track);
601
- }
602
- return track instanceof Track ? track : new Track(track.url, track);
603
- });
604
-
605
- if (position !== undefined && position >= 0 && position <= this.tracks.length) {
606
- this.tracks.splice(position, 0, ...processedTracks);
607
- } else {
608
- this.tracks.push(...processedTracks);
651
+ constructor() {
652
+ this.tracks = [];
653
+ this.currentIndex = -1;
654
+ this.eventBus = new EventBus();
609
655
  }
610
-
611
- processedTracks.forEach(track => {
612
- this.eventBus.emit(EVENTS.TRACK_ADD, track);
613
- });
614
- }
615
-
616
- /**
617
- * Remove track from queue
618
- * @param {number|string} identifier - Track index or ID
619
- * @returns {Track|null} Removed track
620
- */
621
- remove(identifier) {
622
- let index;
623
- if (typeof identifier === 'number') {
624
- index = identifier;
625
- } else {
626
- index = this.tracks.findIndex(track => track.id === identifier);
656
+ /**
657
+ * Add track(s) to queue
658
+ * @param tracks - Track(s) to add
659
+ * @param position - Position to insert (optional)
660
+ */
661
+ add(tracks, position) {
662
+ const trackArray = Array.isArray(tracks) ? tracks : [tracks];
663
+ const processedTracks = trackArray.map(track => {
664
+ if (typeof track === 'string') {
665
+ return new Track(track);
666
+ }
667
+ if (track instanceof Track) {
668
+ return track;
669
+ }
670
+ // It's ITrack or similar object
671
+ return new Track(track.url, track);
672
+ });
673
+ if (position !== undefined && position >= 0 && position <= this.tracks.length) {
674
+ this.tracks.splice(position, 0, ...processedTracks);
675
+ }
676
+ else {
677
+ this.tracks.push(...processedTracks);
678
+ }
679
+ processedTracks.forEach(track => {
680
+ this.eventBus.emit(EVENTS.TRACK_ADD, track);
681
+ });
682
+ this.eventBus.emit(EVENTS.QUEUE_UPDATE, this.tracks);
627
683
  }
628
-
629
- if (index < 0 || index >= this.tracks.length) return null;
630
-
631
- const removed = this.tracks.splice(index, 1)[0];
632
-
633
- if (this.currentIndex > index) {
634
- this.currentIndex--;
635
- } else if (this.currentIndex === index) {
636
- this.currentIndex = -1;
684
+ /**
685
+ * Remove track from queue
686
+ * @param identifier - Track index or ID
687
+ * @returns Removed track
688
+ */
689
+ remove(identifier) {
690
+ let index;
691
+ if (typeof identifier === 'number') {
692
+ index = identifier;
693
+ }
694
+ else {
695
+ index = this.tracks.findIndex(track => track.id === identifier);
696
+ }
697
+ if (index < 0 || index >= this.tracks.length)
698
+ return null;
699
+ const removed = this.tracks.splice(index, 1)[0];
700
+ if (this.currentIndex > index) {
701
+ this.currentIndex--;
702
+ }
703
+ else if (this.currentIndex === index) {
704
+ this.currentIndex = -1;
705
+ }
706
+ this.eventBus.emit(EVENTS.TRACK_REMOVE, removed);
707
+ this.eventBus.emit(EVENTS.QUEUE_UPDATE, this.tracks);
708
+ return removed;
637
709
  }
638
-
639
- this.eventBus.emit(EVENTS.TRACK_REMOVE, removed);
640
- return removed;
641
- }
642
-
643
- /**
644
- * Shuffle the queue
645
- */
646
- shuffle() {
647
- if (this.tracks.length <= 1) return;
648
-
649
- // Fisher-Yates shuffle
650
- for (let i = this.tracks.length - 1; i > 0; i--) {
651
- const j = Math.floor(Math.random() * (i + 1));
652
- [this.tracks[i], this.tracks[j]] = [this.tracks[j], this.tracks[i]];
710
+ /**
711
+ * Shuffle the queue
712
+ */
713
+ shuffle() {
714
+ if (this.tracks.length <= 1)
715
+ return;
716
+ let currentTrack = null;
717
+ if (this.currentIndex >= 0) {
718
+ currentTrack = this.tracks[this.currentIndex];
719
+ }
720
+ for (let i = this.tracks.length - 1; i > 0; i--) {
721
+ const j = Math.floor(Math.random() * (i + 1));
722
+ [this.tracks[i], this.tracks[j]] = [this.tracks[j], this.tracks[i]];
723
+ }
724
+ if (currentTrack) {
725
+ this.currentIndex = this.tracks.indexOf(currentTrack);
726
+ }
727
+ else {
728
+ this.currentIndex = -1;
729
+ }
730
+ this.eventBus.emit(EVENTS.QUEUE_UPDATE, this.tracks);
731
+ }
732
+ /**
733
+ * Clear the queue
734
+ */
735
+ clear() {
736
+ this.tracks = [];
737
+ this.currentIndex = -1;
738
+ this.eventBus.emit(EVENTS.QUEUE_UPDATE, this.tracks);
739
+ }
740
+ /**
741
+ * Jump to specific track
742
+ * @param index - Track index
743
+ * @returns Track at index
744
+ */
745
+ jump(index) {
746
+ if (index < 0 || index >= this.tracks.length)
747
+ return null;
748
+ this.currentIndex = index;
749
+ return this.tracks[index];
750
+ }
751
+ /**
752
+ * Get current track
753
+ * @returns Current track
754
+ */
755
+ getCurrent() {
756
+ return this.currentIndex >= 0 ? this.tracks[this.currentIndex] : null;
757
+ }
758
+ /**
759
+ * Get next track
760
+ * Moves cursor forward
761
+ * @param loop - Whether to loop back to start
762
+ * @returns Next track
763
+ */
764
+ next(loop = false) {
765
+ if (this.tracks.length === 0)
766
+ return null;
767
+ let nextIndex = this.currentIndex + 1;
768
+ if (nextIndex >= this.tracks.length) {
769
+ if (loop) {
770
+ nextIndex = 0;
771
+ }
772
+ else {
773
+ return null;
774
+ }
775
+ }
776
+ this.currentIndex = nextIndex;
777
+ return this.tracks[this.currentIndex];
778
+ }
779
+ /**
780
+ * Get previous track
781
+ * Moves cursor backward
782
+ * @param loop - Whether to loop to end
783
+ * @returns Previous track
784
+ */
785
+ previous(loop = false) {
786
+ if (this.tracks.length === 0)
787
+ return null;
788
+ let prevIndex = this.currentIndex - 1;
789
+ if (prevIndex < 0) {
790
+ if (loop) {
791
+ prevIndex = this.tracks.length - 1;
792
+ }
793
+ else {
794
+ return null;
795
+ }
796
+ }
797
+ this.currentIndex = prevIndex;
798
+ return this.tracks[this.currentIndex];
799
+ }
800
+ /**
801
+ * Get all tracks
802
+ * @returns Array of tracks
803
+ */
804
+ getTracks() {
805
+ return [...this.tracks];
806
+ }
807
+ /**
808
+ * Get queue size
809
+ * @returns Number of tracks
810
+ */
811
+ size() {
812
+ return this.tracks.length;
813
+ }
814
+ /**
815
+ * Check if queue is empty
816
+ * @returns Is empty
817
+ */
818
+ isEmpty() {
819
+ return this.tracks.length === 0;
820
+ }
821
+ /**
822
+ * Get track at index
823
+ * @param index - Track index
824
+ * @returns Track at index
825
+ */
826
+ getTrack(index) {
827
+ return index >= 0 && index < this.tracks.length ? this.tracks[index] : null;
653
828
  }
654
-
655
- this.currentIndex = -1;
656
- this.eventBus.emit(EVENTS.SHUFFLE, this.tracks);
657
- }
658
-
659
- /**
660
- * Clear the queue
661
- */
662
- clear() {
663
- this.tracks = [];
664
- this.currentIndex = -1;
665
- this.eventBus.emit(EVENTS.CLEAR);
666
- }
667
-
668
- /**
669
- * Jump to specific track
670
- * @param {number} index - Track index
671
- * @returns {Track|null} Track at index
672
- */
673
- jump(index) {
674
- if (index < 0 || index >= this.tracks.length) return null;
675
- this.currentIndex = index;
676
- return this.tracks[index];
677
- }
678
-
679
- /**
680
- * Get current track
681
- * @returns {Track|null} Current track
682
- */
683
- getCurrent() {
684
- return this.currentIndex >= 0 ? this.tracks[this.currentIndex] : null;
685
- }
686
-
687
- /**
688
- * Get next track
689
- * @returns {Track|null} Next track
690
- */
691
- getNext() {
692
- if (this.tracks.length === 0) return null;
693
- this.currentIndex = (this.currentIndex + 1) % this.tracks.length;
694
- return this.tracks[this.currentIndex];
695
- }
696
-
697
- /**
698
- * Get previous track
699
- * @returns {Track|null} Previous track
700
- */
701
- getPrevious() {
702
- if (this.tracks.length === 0) return null;
703
- this.currentIndex = this.currentIndex <= 0 ? this.tracks.length - 1 : this.currentIndex - 1;
704
- return this.tracks[this.currentIndex];
705
- }
706
-
707
- /**
708
- * Get all tracks
709
- * @returns {Track[]} Array of tracks
710
- */
711
- getTracks() {
712
- return [...this.tracks];
713
- }
714
-
715
- /**
716
- * Get queue size
717
- * @returns {number} Number of tracks
718
- */
719
- size() {
720
- return this.tracks.length;
721
- }
722
-
723
- /**
724
- * Check if queue is empty
725
- * @returns {boolean} Is empty
726
- */
727
- isEmpty() {
728
- return this.tracks.length === 0;
729
- }
730
-
731
- /**
732
- * Get track at index
733
- * @param {number} index - Track index
734
- * @returns {Track|null} Track at index
735
- */
736
- getTrack(index) {
737
- return index >= 0 && index < this.tracks.length ? this.tracks[index] : null;
738
- }
739
829
  }
740
830
 
741
- /**
742
- * Main audio engine class
743
- */
744
- class AudioEngine {
745
- constructor(options = {}) {
746
- this.options = options;
747
- this.audioContext = null;
748
- this.player = null;
749
- this.filters = null;
750
- this.queue = new Queue();
751
- this.eventBus = new EventBus();
752
- this.isReady = false;
753
-
754
- this.initialize();
755
- }
756
-
757
- /**
758
- * Initialize the audio engine
759
- */
760
- async initialize() {
761
- try {
762
- // Create AudioContext
763
- this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
764
-
765
- // Create components
766
- this.filters = new Filters(this.audioContext);
767
- this.player = new Player(this);
768
-
769
- // Connect event buses
770
- this.queue.eventBus.on(EVENTS.TRACK_ADD, (track) => this.eventBus.emit(EVENTS.TRACK_ADD, track));
771
- this.queue.eventBus.on(EVENTS.TRACK_REMOVE, (track) => this.eventBus.emit(EVENTS.TRACK_REMOVE, track));
772
- this.player.eventBus.on(EVENTS.PLAY, (data) => this.eventBus.emit(EVENTS.PLAY, data));
773
- this.player.eventBus.on(EVENTS.PAUSE, () => this.eventBus.emit(EVENTS.PAUSE));
774
- this.player.eventBus.on(EVENTS.STOP, () => this.eventBus.emit(EVENTS.STOP));
775
- this.player.eventBus.on(EVENTS.ERROR, (error) => this.eventBus.emit(EVENTS.ERROR, error));
776
- this.player.eventBus.on(EVENTS.TRACK_START, (track) => this.eventBus.emit(EVENTS.TRACK_START, track));
777
- this.player.eventBus.on(EVENTS.TRACK_END, (track) => this.eventBus.emit(EVENTS.TRACK_END, track));
778
-
779
- this.isReady = true;
780
- this.eventBus.emit(EVENTS.READY);
781
- } catch (error) {
782
- this.eventBus.emit(EVENTS.ERROR, error);
831
+ class ProviderRegistry {
832
+ constructor() {
833
+ this.providers = new Map();
783
834
  }
784
- }
785
-
786
- /**
787
- * Play track or resume playback
788
- * @param {Track|string} track - Track to play or track identifier
789
- */
790
- async play(track) {
791
- if (!this.isReady) return;
792
-
793
- if (track) {
794
- const trackObj = typeof track === 'string' ? this.queue.getCurrent() : track;
795
- await this.player.play(trackObj);
796
- } else {
797
- this.player.resume();
835
+ register(provider) {
836
+ this.providers.set(provider.name, provider);
837
+ }
838
+ unregister(name) {
839
+ this.providers.delete(name);
840
+ }
841
+ get(name) {
842
+ return this.providers.get(name);
843
+ }
844
+ getAll() {
845
+ return Array.from(this.providers.values());
846
+ }
847
+ has(name) {
848
+ return this.providers.has(name);
798
849
  }
799
- }
800
-
801
- /**
802
- * Pause playback
803
- */
804
- pause() {
805
- this.player.pause();
806
- }
807
-
808
- /**
809
- * Stop playback
810
- */
811
- stop() {
812
- this.player.stop();
813
- }
814
-
815
- /**
816
- * Seek to position
817
- * @param {number} time - Time in seconds
818
- */
819
- seek(time) {
820
- this.player.seek(time);
821
- }
822
-
823
- /**
824
- * Set volume
825
- * @param {number} volume - Volume level (0-1)
826
- */
827
- setVolume(volume) {
828
- this.player.setVolume(volume);
829
- }
830
-
831
- /**
832
- * Add track(s) to queue
833
- * @param {Track|Track[]|string|string[]} tracks - Track(s) to add
834
- */
835
- add(tracks) {
836
- this.queue.add(tracks);
837
- }
838
-
839
- /**
840
- * Remove track from queue
841
- * @param {number|string} identifier - Track index or ID
842
- */
843
- remove(identifier) {
844
- return this.queue.remove(identifier);
845
- }
846
-
847
- /**
848
- * Skip to next track
849
- */
850
- next() {
851
- const nextTrack = this.queue.getNext();
852
- if (nextTrack) {
853
- this.play(nextTrack);
854
- }
855
- }
856
-
857
- /**
858
- * Go to previous track
859
- */
860
- previous() {
861
- const prevTrack = this.queue.getPrevious();
862
- if (prevTrack) {
863
- this.play(prevTrack);
864
- }
865
- }
866
-
867
- /**
868
- * Shuffle queue
869
- */
870
- shuffle() {
871
- this.queue.shuffle();
872
- }
873
-
874
- /**
875
- * Clear queue
876
- */
877
- clear() {
878
- this.queue.clear();
879
- }
880
-
881
- /**
882
- * Jump to track in queue
883
- * @param {number} index - Track index
884
- */
885
- jump(index) {
886
- const track = this.queue.jump(index);
887
- if (track) {
888
- this.play(track);
889
- }
890
- }
891
-
892
- /**
893
- * Apply audio filter
894
- * @param {string} type - Filter type
895
- * @param {Object} options - Filter options
896
- */
897
- applyFilter(type, options) {
898
- this.filters.apply(type, options);
899
- this.eventBus.emit(EVENTS.FILTER_APPLIED, { type, options });
900
- }
901
-
902
- /**
903
- * Remove audio filter
904
- * @param {string} type - Filter type
905
- */
906
- removeFilter(type) {
907
- this.filters.remove(type);
908
- }
909
-
910
- /**
911
- * Set loop mode
912
- * @param {string} mode - Loop mode
913
- */
914
- setLoopMode(mode) {
915
- this.player.setLoopMode(mode);
916
- }
917
-
918
- /**
919
- * Get current state
920
- * @returns {Object} Engine state
921
- */
922
- getState() {
923
- return {
924
- isReady: this.isReady,
925
- isPlaying: this.player ? this.player.isPlaying : false,
926
- currentTrack: this.queue.getCurrent(),
927
- queue: this.queue.getTracks(),
928
- volume: this.player ? this.player.volume : 1,
929
- loopMode: this.player ? this.player.loopMode : LOOP_MODES.OFF,
930
- filters: this.filters ? this.filters.getEnabled() : new Set()
931
- };
932
- }
933
-
934
- /**
935
- * Destroy the engine
936
- */
937
- destroy() {
938
- if (this.audioContext) {
939
- this.audioContext.close();
940
- }
941
- this.filters.clear();
942
- this.player.stop();
943
- }
944
850
  }
945
851
 
946
- /**
947
- * Base plugin class
948
- */
949
- class Plugin {
950
- constructor(name, version = '1.0.0') {
951
- this.name = name;
952
- this.version = version;
953
- this.enabled = false;
954
- this.loaded = false;
955
- }
956
-
957
- /**
958
- * Called when plugin is loaded
959
- * @param {AudioEngine} engine - Audio engine instance
960
- */
961
- onLoad(engine) {
962
- this.engine = engine;
963
- this.loaded = true;
964
- }
965
-
966
- /**
967
- * Called when plugin is enabled
968
- */
969
- onEnable() {
970
- this.enabled = true;
971
- }
972
-
973
- /**
974
- * Called when plugin is disabled
975
- */
976
- onDisable() {
977
- this.enabled = false;
978
- }
979
-
980
- /**
981
- * Hook called before play
982
- * @param {Track} track - Track being played
983
- */
984
- beforePlay(track) {
985
- // Override in subclass
986
- }
987
-
988
- /**
989
- * Hook called after play
990
- * @param {Track} track - Track being played
991
- */
992
- afterPlay(track) {
993
- // Override in subclass
994
- }
995
-
996
- /**
997
- * Hook called when track ends
998
- * @param {Track} track - Track that ended
999
- */
1000
- trackEnd(track) {
1001
- // Override in subclass
1002
- }
1003
-
1004
- /**
1005
- * Hook called when queue updates
1006
- * @param {Queue} queue - Updated queue
1007
- */
1008
- queueUpdate(queue) {
1009
- // Override in subclass
1010
- }
1011
-
1012
- /**
1013
- * Get plugin info
1014
- * @returns {Object} Plugin information
1015
- */
1016
- getInfo() {
1017
- return {
1018
- name: this.name,
1019
- version: this.version,
1020
- enabled: this.enabled,
1021
- loaded: this.loaded
1022
- };
1023
- }
852
+ class LocalProvider {
853
+ constructor() {
854
+ this.name = 'local';
855
+ this.version = '1.0.0';
856
+ this.engine = null;
857
+ }
858
+ async initialize(engine) {
859
+ this.engine = engine;
860
+ }
861
+ async resolve(path) {
862
+ if (!await this.exists(path)) {
863
+ throw new Error('File not found');
864
+ }
865
+ // Node.js specific checks
866
+ const stats = await promises.stat(path);
867
+ if (!stats.isFile()) {
868
+ throw new Error('Path is not a file');
869
+ }
870
+ const track = new Track(`file://${path}`, {
871
+ title: path.split('/').pop()?.replace(extname(path), '') || 'Unknown',
872
+ source: 'local',
873
+ metadata: {
874
+ size: stats.size,
875
+ modified: stats.mtime
876
+ }
877
+ });
878
+ return track;
879
+ }
880
+ async play(track) {
881
+ if (!this.engine)
882
+ throw new Error('Provider not initialized');
883
+ // For local files, we assume the player can handle file:// URLs or we might need to read it into a buffer here?
884
+ // The previous Player implementation used fetch(url). fetch supports file:// in some envs but not all.
885
+ // However, given the hybrid nature, we'll assume the engine's player handles the URL.
886
+ // Actually, Player.ts uses fetch(). fetch('file://...') might fail in Node if not polyfilled or configured.
887
+ // But let's stick to the architecture: Provider calls engine.player.load(track).
888
+ // Wait, AudioEngine.ts in JS called `player.play(track)`.
889
+ // So the Provider.play just needs to confirm it CAN play or do setup?
890
+ // If AudioEngine delegates to Provider, then Provider MUST do the work.
891
+ // "AudioEngine calls provider.play(track)" -> Provider must make sound happen.
892
+ // So LocalProvider should call this.engine.player.play(track).
893
+ // BUT checking for infinite loop: Engine calls Provider.play -> Provider calls Engine.player.play?
894
+ // Engine needs to know NOT to call Provider again.
895
+ // Engine.play(track) -> check provider -> provider.play(track)
896
+ // Provider.play(track) -> engine.player.loadSource(track.url) -> source.start()
897
+ // We need to expose `loadSource` or similar on engine/player.
898
+ // For now, I'll assume engine.player has low-level methods.
899
+ // Let's assume the Player has a `playStream(url)` method.
900
+ // I'll type cast engine.player for now.
901
+ await this.engine.player.playStream(track);
902
+ }
903
+ async stop() {
904
+ // Local provider doesn't manage state separate from engine
905
+ }
906
+ destroy() {
907
+ // No cleanup needed
908
+ }
909
+ async exists(path) {
910
+ try {
911
+ await promises.access(path);
912
+ return true;
913
+ }
914
+ catch {
915
+ return false;
916
+ }
917
+ }
1024
918
  }
1025
919
 
1026
- /**
1027
- * Plugin manager for loading and managing plugins
1028
- */
1029
- class PluginManager {
1030
- constructor(audioEngine) {
1031
- this.engine = audioEngine;
1032
- this.plugins = new Map();
1033
- }
1034
-
1035
- /**
1036
- * Load a plugin
1037
- * @param {Plugin} plugin - Plugin instance
1038
- */
1039
- load(plugin) {
1040
- if (!(plugin instanceof Plugin)) {
1041
- throw new Error('Invalid plugin instance');
920
+ class YouTubeProvider {
921
+ constructor() {
922
+ this.name = 'youtube';
923
+ this.version = '1.0.0';
924
+ this.engine = null;
1042
925
  }
1043
-
1044
- plugin.onLoad(this.engine);
1045
- this.plugins.set(plugin.name, plugin);
1046
- }
1047
-
1048
- /**
1049
- * Enable a plugin
1050
- * @param {string} name - Plugin name
1051
- */
1052
- enable(name) {
1053
- const plugin = this.plugins.get(name);
1054
- if (plugin && !plugin.enabled) {
1055
- plugin.onEnable();
1056
- }
1057
- }
1058
-
1059
- /**
1060
- * Disable a plugin
1061
- * @param {string} name - Plugin name
1062
- */
1063
- disable(name) {
1064
- const plugin = this.plugins.get(name);
1065
- if (plugin && plugin.enabled) {
1066
- plugin.onDisable();
1067
- }
1068
- }
1069
-
1070
- /**
1071
- * Unload a plugin
1072
- * @param {string} name - Plugin name
1073
- */
1074
- unload(name) {
1075
- const plugin = this.plugins.get(name);
1076
- if (plugin) {
1077
- if (plugin.enabled) {
1078
- plugin.onDisable();
1079
- }
1080
- this.plugins.delete(name);
1081
- }
1082
- }
1083
-
1084
- /**
1085
- * Get plugin by name
1086
- * @param {string} name - Plugin name
1087
- * @returns {Plugin|null} Plugin instance
1088
- */
1089
- get(name) {
1090
- return this.plugins.get(name) || null;
1091
- }
1092
-
1093
- /**
1094
- * Get all plugins
1095
- * @returns {Map} Map of plugins
1096
- */
1097
- getAll() {
1098
- return new Map(this.plugins);
1099
- }
1100
-
1101
- /**
1102
- * Get enabled plugins
1103
- * @returns {Plugin[]} Array of enabled plugins
1104
- */
1105
- getEnabled() {
1106
- return Array.from(this.plugins.values()).filter(plugin => plugin.enabled);
1107
- }
1108
-
1109
- /**
1110
- * Call hook on all enabled plugins
1111
- * @param {string} hook - Hook name
1112
- * @param {...*} args - Arguments to pass
1113
- */
1114
- callHook(hook, ...args) {
1115
- this.getEnabled().forEach(plugin => {
1116
- if (typeof plugin[hook] === 'function') {
926
+ async initialize(engine) {
927
+ this.engine = engine;
928
+ }
929
+ async resolve(query) {
930
+ if (query.includes('youtube.com') || query.includes('youtu.be')) {
931
+ const videoId = this.extractVideoId(query);
932
+ if (!videoId)
933
+ throw new Error('Invalid YouTube URL');
934
+ return new Track(query, {
935
+ title: `YouTube Video ${videoId}`,
936
+ thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
937
+ source: 'youtube',
938
+ metadata: { videoId }
939
+ });
940
+ }
941
+ // Search not implemented in this mock
942
+ throw new Error('Search not implemented');
943
+ }
944
+ async play(track) {
945
+ if (!this.engine)
946
+ throw new Error('Provider not initialized');
947
+ // In a real app, resolve stream URL here (e.g. ytdl-core)
948
+ // const streamUrl = await ytdl(track.url);
949
+ // await this.engine.player.playStream(streamUrl);
950
+ throw new Error('Stream URL extraction requires additional dependencies (ytdl-core)');
951
+ }
952
+ async stop() { }
953
+ destroy() { }
954
+ extractVideoId(url) {
1117
955
  try {
1118
- plugin[hook](...args);
1119
- } catch (error) {
1120
- console.error(`Error in plugin ${plugin.name} hook ${hook}:`, error);
956
+ const urlObj = new URL(url);
957
+ if (urlObj.hostname === 'youtu.be') {
958
+ return urlObj.pathname.slice(1);
959
+ }
960
+ return urlObj.searchParams.get('v');
961
+ }
962
+ catch {
963
+ return null;
1121
964
  }
1122
- }
1123
- });
1124
- }
965
+ }
1125
966
  }
1126
967
 
1127
- /**
1128
- * Logger utility with different levels
1129
- */
1130
- class Logger {
1131
- constructor(level = 'info') {
1132
- this.levels = {
1133
- debug: 0,
1134
- info: 1,
1135
- warn: 2,
1136
- error: 3
1137
- };
1138
- this.currentLevel = this.levels[level] || this.levels.info;
1139
- }
1140
-
1141
- /**
1142
- * Set log level
1143
- * @param {string} level - Log level (debug, info, warn, error)
1144
- */
1145
- setLevel(level) {
1146
- this.currentLevel = this.levels[level] || this.levels.info;
1147
- }
1148
-
1149
- /**
1150
- * Debug log
1151
- * @param {...*} args - Arguments to log
1152
- */
1153
- debug(...args) {
1154
- if (this.currentLevel <= this.levels.debug) {
1155
- console.debug('[DEBUG]', ...args);
1156
- }
1157
- }
1158
-
1159
- /**
1160
- * Info log
1161
- * @param {...*} args - Arguments to log
1162
- */
1163
- info(...args) {
1164
- if (this.currentLevel <= this.levels.info) {
1165
- console.info('[INFO]', ...args);
1166
- }
1167
- }
1168
-
1169
- /**
1170
- * Warning log
1171
- * @param {...*} args - Arguments to log
1172
- */
1173
- warn(...args) {
1174
- if (this.currentLevel <= this.levels.warn) {
1175
- console.warn('[WARN]', ...args);
1176
- }
1177
- }
1178
-
1179
- /**
1180
- * Error log
1181
- * @param {...*} args - Arguments to log
1182
- */
1183
- error(...args) {
1184
- if (this.currentLevel <= this.levels.error) {
1185
- console.error('[ERROR]', ...args);
1186
- }
1187
- }
968
+ class SpotifyProvider {
969
+ constructor(options = {}) {
970
+ this.name = 'spotify';
971
+ this.version = '1.0.0';
972
+ this.engine = null;
973
+ this.spotifyApi = new SpotifyWebApi({
974
+ clientId: options.clientId,
975
+ clientSecret: options.clientSecret,
976
+ redirectUri: options.redirectUri,
977
+ accessToken: options.accessToken,
978
+ refreshToken: options.refreshToken
979
+ });
980
+ }
981
+ async initialize(engine) {
982
+ this.engine = engine;
983
+ }
984
+ async resolve(query) {
985
+ // Check if query is ID or URL or Search
986
+ if (query.includes('spotify.com/track/')) {
987
+ const id = query.split('track/')[1].split('?')[0];
988
+ const data = await this.spotifyApi.getTrack(id);
989
+ return this._formatTrack(data.body);
990
+ }
991
+ // Default to search
992
+ const data = await this.spotifyApi.searchTracks(query);
993
+ return data.body.tracks?.items.map(t => this._formatTrack(t)) || [];
994
+ }
995
+ async play(track) {
996
+ if (!this.engine)
997
+ throw new Error('Provider not initialized');
998
+ // Spotify playback usually requires Web SDK or resolving to another source
999
+ // Here we can throw or try to resolve if Preview URL is available
1000
+ if (track.metadata.preview_url) {
1001
+ await this.engine.player.playStream({ ...track, url: track.metadata.preview_url });
1002
+ }
1003
+ else {
1004
+ throw new Error('Spotify full playback not supported in this provider version (preview only)');
1005
+ }
1006
+ }
1007
+ async stop() { }
1008
+ destroy() { }
1009
+ _formatTrack(spotifyTrack) {
1010
+ return new Track(spotifyTrack.external_urls.spotify, {
1011
+ id: spotifyTrack.id,
1012
+ title: spotifyTrack.name,
1013
+ artist: spotifyTrack.artists.map((a) => a.name).join(', '),
1014
+ duration: Math.floor(spotifyTrack.duration_ms / 1000),
1015
+ thumbnail: spotifyTrack.album.images[0]?.url,
1016
+ source: 'spotify',
1017
+ metadata: {
1018
+ spotifyId: spotifyTrack.id,
1019
+ preview_url: spotifyTrack.preview_url,
1020
+ popularity: spotifyTrack.popularity
1021
+ }
1022
+ });
1023
+ }
1188
1024
  }
1189
1025
 
1190
- // Default logger instance
1191
- new Logger();
1192
-
1193
- /**
1194
- * Time formatting utilities
1195
- */
1196
- class TimeUtils {
1197
- /**
1198
- * Format seconds to MM:SS or HH:MM:SS
1199
- * @param {number} seconds - Time in seconds
1200
- * @returns {string} Formatted time string
1201
- */
1202
- static format(seconds) {
1203
- if (!Number.isFinite(seconds) || seconds < 0) return '00:00';
1204
-
1205
- const hours = Math.floor(seconds / 3600);
1206
- const minutes = Math.floor((seconds % 3600) / 60);
1207
- const secs = Math.floor(seconds % 60);
1208
-
1209
- if (hours > 0) {
1210
- return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1026
+ class LavalinkProvider {
1027
+ constructor(options = {}) {
1028
+ this.name = 'lavalink';
1029
+ this.version = '1.0.0';
1030
+ this.engine = null;
1031
+ this.manager = null;
1032
+ this.node = null;
1033
+ this.options = options;
1034
+ }
1035
+ async initialize(engine) {
1036
+ this.engine = engine;
1037
+ this.manager = new LavalinkManager({
1038
+ nodes: [{
1039
+ host: this.options.host || 'localhost',
1040
+ port: this.options.port || 2333,
1041
+ // @ts-ignore
1042
+ password: this.options.password || 'youshallnotpass',
1043
+ secure: this.options.secure || false,
1044
+ id: 'main'
1045
+ }],
1046
+ sendToShard: (guildId, payload) => {
1047
+ // Mock
1048
+ }
1049
+ });
1050
+ // @ts-ignore
1051
+ if (this.manager.connect)
1052
+ await this.manager.connect();
1053
+ // @ts-ignore
1054
+ this.node = this.manager.nodes ? this.manager.nodes.get('main') : this.manager.node;
1055
+ }
1056
+ async resolve(identifier) {
1057
+ if (!this.node)
1058
+ throw new Error('Lavalink not connected');
1059
+ const result = await this.node.rest.loadTracks(identifier);
1060
+ if (result.loadType === 'TRACK_LOADED') {
1061
+ return this._formatTrack(result.tracks[0]);
1062
+ }
1063
+ else if (result.loadType === 'PLAYLIST_LOADED' || result.loadType === 'SEARCH_RESULT') {
1064
+ return result.tracks.map((t) => this._formatTrack(t));
1065
+ }
1066
+ return [];
1067
+ }
1068
+ async play(track) {
1069
+ throw new Error('Lavalink play() requires guild/channel context. Use createPlayer() directly.');
1070
+ }
1071
+ createPlayer(guildId, channelId) {
1072
+ if (!this.manager)
1073
+ throw new Error('Not initialized');
1074
+ return this.manager.createPlayer({
1075
+ guildId,
1076
+ voiceChannelId: channelId
1077
+ });
1078
+ }
1079
+ async stop() {
1080
+ // Stop all?
1081
+ }
1082
+ destroy() {
1083
+ if (this.manager) {
1084
+ // @ts-ignore
1085
+ if (this.manager.destroy)
1086
+ this.manager.destroy();
1087
+ }
1088
+ }
1089
+ _formatTrack(lavalinkTrack) {
1090
+ const info = lavalinkTrack.info;
1091
+ return new Track(info.uri, {
1092
+ id: lavalinkTrack.track,
1093
+ title: info.title,
1094
+ artist: info.author,
1095
+ duration: Math.floor(info.length / 1000),
1096
+ thumbnail: info.artworkUrl,
1097
+ source: 'lavalink',
1098
+ metadata: {
1099
+ lavalinkTrack: lavalinkTrack.track,
1100
+ identifier: info.identifier
1101
+ }
1102
+ });
1211
1103
  }
1212
- return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1213
- }
1214
-
1215
- /**
1216
- * Parse time string to seconds
1217
- * @param {string} timeStr - Time string like "1:23" or "01:23:45"
1218
- * @returns {number} Time in seconds
1219
- */
1220
- static parse(timeStr) {
1221
- if (!timeStr || typeof timeStr !== 'string') return 0;
1222
-
1223
- const parts = timeStr.split(':').map(Number);
1224
- if (parts.length === 2) {
1225
- return parts[0] * 60 + parts[1];
1226
- } else if (parts.length === 3) {
1227
- return parts[0] * 3600 + parts[1] * 60 + parts[2];
1228
- }
1229
- return 0;
1230
- }
1231
-
1232
- /**
1233
- * Get current timestamp in milliseconds
1234
- * @returns {number} Current time
1235
- */
1236
- static now() {
1237
- return Date.now();
1238
- }
1239
-
1240
- /**
1241
- * Calculate duration between two timestamps
1242
- * @param {number} start - Start time
1243
- * @param {number} end - End time
1244
- * @returns {number} Duration in milliseconds
1245
- */
1246
- static duration(start, end) {
1247
- return end - start;
1248
- }
1249
1104
  }
1250
1105
 
1251
1106
  /**
1252
- * Audio probing utilities
1107
+ * Plugin manager for loading and managing plugins
1253
1108
  */
1254
- class ProbeUtils {
1255
- /**
1256
- * Probe audio file/stream for basic info
1257
- * @param {string|Buffer|ReadableStream} source - Audio source
1258
- * @returns {Promise<Object>} Probe result
1259
- */
1260
- static async probe(source) {
1261
- // In a real implementation, this would use ffprobe or similar
1262
- // For now, return basic mock data
1263
- return {
1264
- duration: null, // seconds
1265
- format: null, // e.g., 'mp3', 'wav'
1266
- bitrate: null, // kbps
1267
- sampleRate: null, // Hz
1268
- channels: null // 1 or 2
1269
- };
1270
- }
1271
-
1272
- /**
1273
- * Check if source is a valid audio URL
1274
- * @param {string} url - URL to check
1275
- * @returns {boolean} Is valid audio URL
1276
- */
1277
- static isValidAudioUrl(url) {
1278
- if (!url || typeof url !== 'string') return false;
1279
-
1280
- try {
1281
- const parsed = new URL(url);
1282
- const audioExtensions = ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a'];
1283
- const path = parsed.pathname.toLowerCase();
1284
-
1285
- return audioExtensions.some(ext => path.endsWith(ext)) ||
1286
- url.includes('youtube.com') ||
1287
- url.includes('youtu.be') ||
1288
- url.includes('soundcloud.com');
1289
- } catch {
1290
- return false;
1291
- }
1292
- }
1293
-
1294
- /**
1295
- * Get audio format from URL or buffer
1296
- * @param {string|Buffer} source - Audio source
1297
- * @returns {string|null} Audio format
1298
- */
1299
- static getFormat(source) {
1300
- if (typeof source === 'string') {
1301
- const extensions = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
1302
- for (const ext of extensions) {
1303
- if (source.toLowerCase().includes(`.${ext}`)) {
1304
- return ext;
1305
- }
1306
- }
1307
- }
1308
- // For buffer, would need to check headers
1309
- return null;
1310
- }
1109
+ class PluginManager {
1110
+ constructor(engine) {
1111
+ this.engine = engine;
1112
+ this.plugins = new Map();
1113
+ }
1114
+ /**
1115
+ * Load a plugin
1116
+ * @param plugin - Plugin instance
1117
+ */
1118
+ load(plugin) {
1119
+ if (!plugin || typeof plugin.onLoad !== 'function') {
1120
+ throw new Error('Invalid plugin instance');
1121
+ }
1122
+ try {
1123
+ plugin.onLoad(this.engine);
1124
+ this.plugins.set(plugin.name, plugin);
1125
+ }
1126
+ catch (error) {
1127
+ console.error(`Failed to load plugin ${plugin.name}:`, error);
1128
+ }
1129
+ }
1130
+ /**
1131
+ * Enable a plugin
1132
+ * @param name - Plugin name
1133
+ */
1134
+ enable(name) {
1135
+ const plugin = this.plugins.get(name);
1136
+ if (plugin && !plugin.onEnable)
1137
+ return; // Should have onEnable from interface
1138
+ // Check if we track enabled state in plugin interface?
1139
+ // Interface has onEnable methods.
1140
+ // Plugin implementations usually track their own state or we assume onEnable does it.
1141
+ // Check if plugin object has 'enabled' property (generic check)
1142
+ if (plugin && plugin.enabled === false) {
1143
+ plugin.onEnable();
1144
+ }
1145
+ }
1146
+ /**
1147
+ * Disable a plugin
1148
+ * @param name - Plugin name
1149
+ */
1150
+ disable(name) {
1151
+ const plugin = this.plugins.get(name);
1152
+ if (plugin) {
1153
+ plugin.onDisable();
1154
+ }
1155
+ }
1156
+ /**
1157
+ * Unload a plugin
1158
+ * @param name - Plugin name
1159
+ */
1160
+ unload(name) {
1161
+ const plugin = this.plugins.get(name);
1162
+ if (plugin) {
1163
+ plugin.onUnload();
1164
+ this.plugins.delete(name);
1165
+ }
1166
+ }
1167
+ /**
1168
+ * Get plugin by name
1169
+ * @param name - Plugin name
1170
+ * @returns Plugin instance
1171
+ */
1172
+ get(name) {
1173
+ return this.plugins.get(name);
1174
+ }
1175
+ /**
1176
+ * Get all plugins
1177
+ * @returns Map of plugins
1178
+ */
1179
+ getAll() {
1180
+ return new Map(this.plugins);
1181
+ }
1182
+ /**
1183
+ * Get enabled plugins
1184
+ * @returns Array of enabled plugins
1185
+ */
1186
+ getEnabled() {
1187
+ // We assume plugins that are loaded are potential candidates,
1188
+ // but the IPlugin interface doesn't enforce an 'enabled' property reading.
1189
+ // However, the Base Plugin class does.
1190
+ // We'll filter by checking 'enabled' property if it exists, or assume true?
1191
+ // Safer to check property.
1192
+ return Array.from(this.plugins.values()).filter(plugin => plugin.enabled === true);
1193
+ }
1194
+ /**
1195
+ * Call hook on all enabled plugins
1196
+ * @param hook - Hook name
1197
+ * @param args - Arguments to pass
1198
+ */
1199
+ callHook(hook, ...args) {
1200
+ this.getEnabled().forEach(plugin => {
1201
+ if (typeof plugin[hook] === 'function') {
1202
+ try {
1203
+ plugin[hook](...args);
1204
+ }
1205
+ catch (error) {
1206
+ console.error(`Error in plugin ${plugin.name} hook ${hook}:`, error);
1207
+ }
1208
+ }
1209
+ });
1210
+ }
1311
1211
  }
1312
1212
 
1313
1213
  /**
1314
- * YouTube provider for basic info fetching
1214
+ * Main audio engine class
1315
1215
  */
1316
- class YouTubeProvider {
1317
- /**
1318
- * Check if URL is a valid YouTube URL
1319
- * @param {string} url - URL to check
1320
- * @returns {boolean} Is valid YouTube URL
1321
- */
1322
- static isValidUrl(url) {
1323
- return url.includes('youtube.com/watch') || url.includes('youtu.be/');
1324
- }
1325
-
1326
- /**
1327
- * Extract video ID from YouTube URL
1328
- * @param {string} url - YouTube URL
1329
- * @returns {string|null} Video ID
1330
- */
1331
- static extractVideoId(url) {
1332
- try {
1333
- const urlObj = new URL(url);
1334
- if (urlObj.hostname === 'youtu.be') {
1335
- return urlObj.pathname.slice(1);
1336
- }
1337
- return urlObj.searchParams.get('v');
1338
- } catch {
1339
- return null;
1340
- }
1341
- }
1342
-
1343
- /**
1344
- * Get basic track info from YouTube URL
1345
- * @param {string} url - YouTube URL
1346
- * @returns {Promise<Object>} Track info
1347
- */
1348
- static async getInfo(url) {
1349
- const videoId = this.extractVideoId(url);
1350
- if (!videoId) {
1351
- throw new Error('Invalid YouTube URL');
1216
+ class AudioEngine {
1217
+ constructor(options = {}) {
1218
+ this.options = options;
1219
+ this.queue = new Queue();
1220
+ this.eventBus = new EventBus();
1221
+ this.providers = new ProviderRegistry();
1222
+ this.isReady = false;
1223
+ this.player = new Player(this);
1224
+ // @ts-ignore - Accessing private audioContext from player for filters
1225
+ this.filters = new Filters(this.player.audioContext);
1226
+ this.plugins = new PluginManager(this);
1227
+ this.initialize();
1352
1228
  }
1229
+ /**
1230
+ * Initialize the audio engine
1231
+ */
1232
+ async initialize() {
1233
+ try {
1234
+ // Connect event buses
1235
+ this.queue.eventBus.on(EVENTS.TRACK_ADD, (track) => this.eventBus.emit(EVENTS.TRACK_ADD, track));
1236
+ this.queue.eventBus.on(EVENTS.TRACK_REMOVE, (track) => this.eventBus.emit(EVENTS.TRACK_REMOVE, track));
1237
+ this.queue.eventBus.on(EVENTS.QUEUE_UPDATE, (tracks) => {
1238
+ this.eventBus.emit(EVENTS.QUEUE_UPDATE, tracks);
1239
+ this.plugins.callHook('queueUpdate', this.queue);
1240
+ });
1241
+ this.player.eventBus.on(EVENTS.PLAY, (data) => this.eventBus.emit(EVENTS.PLAY, data));
1242
+ this.player.eventBus.on(EVENTS.PAUSE, () => this.eventBus.emit(EVENTS.PAUSE));
1243
+ this.player.eventBus.on(EVENTS.STOP, () => this.eventBus.emit(EVENTS.STOP));
1244
+ this.player.eventBus.on(EVENTS.ERROR, (error) => this.eventBus.emit(EVENTS.ERROR, error));
1245
+ this.player.eventBus.on(EVENTS.TRACK_START, (track) => {
1246
+ this.eventBus.emit(EVENTS.TRACK_START, track);
1247
+ this.plugins.callHook('afterPlay', track); // afterPlay usually means playback started
1248
+ });
1249
+ this.player.eventBus.on(EVENTS.TRACK_END, (track) => {
1250
+ this.eventBus.emit(EVENTS.TRACK_END, track);
1251
+ this.plugins.callHook('trackEnd', track);
1252
+ });
1253
+ // Register default providers
1254
+ await this.registerProvider(new LocalProvider());
1255
+ await this.registerProvider(new YouTubeProvider());
1256
+ if (this.options.spotify) {
1257
+ await this.registerProvider(new SpotifyProvider(this.options.spotify));
1258
+ }
1259
+ if (this.options.lavalink) {
1260
+ await this.registerProvider(new LavalinkProvider(this.options.lavalink));
1261
+ }
1262
+ this.isReady = true;
1263
+ this.eventBus.emit(EVENTS.READY);
1264
+ }
1265
+ catch (error) {
1266
+ this.eventBus.emit(EVENTS.ERROR, error);
1267
+ }
1268
+ }
1269
+ async registerProvider(provider) {
1270
+ await provider.initialize(this);
1271
+ this.providers.register(provider);
1272
+ }
1273
+ getProvider(name) {
1274
+ return this.providers.get(name);
1275
+ }
1276
+ /**
1277
+ * Play track or resume playback
1278
+ * @param track - Track to play or track identifier
1279
+ */
1280
+ async play(track) {
1281
+ if (!this.isReady)
1282
+ return;
1283
+ if (track) {
1284
+ let trackObj = null;
1285
+ if (typeof track === 'string') {
1286
+ trackObj = new Track(track);
1287
+ this.queue.add(trackObj);
1288
+ }
1289
+ else {
1290
+ trackObj = track;
1291
+ }
1292
+ this.plugins.callHook('beforePlay', trackObj);
1293
+ await this.player.play(trackObj);
1294
+ }
1295
+ else {
1296
+ this.player.resume();
1297
+ }
1298
+ }
1299
+ /**
1300
+ * Pause playback
1301
+ */
1302
+ pause() {
1303
+ this.player.pause();
1304
+ }
1305
+ /**
1306
+ * Stop playback
1307
+ */
1308
+ stop() {
1309
+ this.player.stop();
1310
+ }
1311
+ /**
1312
+ * Seek to position
1313
+ * @param time - Time in seconds
1314
+ */
1315
+ seek(time) {
1316
+ this.player.seek(time);
1317
+ }
1318
+ /**
1319
+ * Set volume
1320
+ * @param volume - Volume level (0-1)
1321
+ */
1322
+ setVolume(volume) {
1323
+ this.player.setVolume(volume);
1324
+ }
1325
+ /**
1326
+ * Add track(s) to queue
1327
+ * @param tracks - Track(s) to add
1328
+ */
1329
+ add(tracks) {
1330
+ this.queue.add(tracks);
1331
+ }
1332
+ /**
1333
+ * Remove track from queue
1334
+ * @param identifier - Track index or ID
1335
+ */
1336
+ remove(identifier) {
1337
+ return this.queue.remove(identifier);
1338
+ }
1339
+ /**
1340
+ * Skip to next track
1341
+ */
1342
+ next() {
1343
+ const nextTrack = this.queue.next(false);
1344
+ if (nextTrack) {
1345
+ this.play(nextTrack);
1346
+ }
1347
+ else {
1348
+ this.stop();
1349
+ }
1350
+ }
1351
+ /**
1352
+ * Go to previous track
1353
+ */
1354
+ previous() {
1355
+ const prevTrack = this.queue.previous(false);
1356
+ if (prevTrack) {
1357
+ this.play(prevTrack);
1358
+ }
1359
+ }
1360
+ /**
1361
+ * Shuffle queue
1362
+ */
1363
+ shuffle() {
1364
+ this.queue.shuffle();
1365
+ }
1366
+ /**
1367
+ * Clear queue
1368
+ */
1369
+ clear() {
1370
+ this.queue.clear();
1371
+ }
1372
+ /**
1373
+ * Jump to track in queue
1374
+ * @param index - Track index
1375
+ */
1376
+ jump(index) {
1377
+ const track = this.queue.jump(index);
1378
+ if (track) {
1379
+ this.play(track);
1380
+ }
1381
+ }
1382
+ /**
1383
+ * Apply audio filter
1384
+ * @param type - Filter type
1385
+ * @param options - Filter options
1386
+ */
1387
+ applyFilter(type, options) {
1388
+ this.filters.apply(type, options);
1389
+ this.eventBus.emit(EVENTS.FILTER_APPLIED, { type, options });
1390
+ }
1391
+ /**
1392
+ * Remove audio filter
1393
+ * @param type - Filter type
1394
+ */
1395
+ removeFilter(type) {
1396
+ this.filters.remove(type);
1397
+ }
1398
+ /**
1399
+ * Set loop mode
1400
+ * @param mode - Loop mode
1401
+ */
1402
+ setLoopMode(mode) {
1403
+ this.player.setLoopMode(mode);
1404
+ }
1405
+ /**
1406
+ * Get current state
1407
+ * @returns Engine state
1408
+ */
1409
+ getState() {
1410
+ return {
1411
+ isReady: this.isReady,
1412
+ ...this.player.getState(),
1413
+ currentTrack: this.queue.getCurrent(),
1414
+ queue: this.queue.getTracks(),
1415
+ filters: this.filters.getEnabled()
1416
+ };
1417
+ }
1418
+ /**
1419
+ * Destroy the engine
1420
+ */
1421
+ destroy() {
1422
+ this.filters.clear();
1423
+ this.player.stop();
1424
+ this.providers.getAll().forEach(p => p.destroy());
1425
+ this.plugins.getAll().forEach(p => p.onUnload());
1426
+ // @ts-ignore
1427
+ if (this.player.audioContext && this.player.audioContext.state !== 'closed') {
1428
+ // @ts-ignore
1429
+ this.player.audioContext.close();
1430
+ }
1431
+ }
1432
+ }
1353
1433
 
1354
- // In a real implementation, this would call YouTube API
1355
- // For now, return mock data
1356
- return {
1357
- title: `YouTube Video ${videoId}`,
1358
- artist: null,
1359
- duration: null,
1360
- thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
1361
- url: url,
1362
- source: 'youtube',
1363
- videoId: videoId
1364
- };
1365
- }
1366
-
1367
- /**
1368
- * Get stream URL (not implemented without dependencies)
1369
- * @param {string} url - YouTube URL
1370
- * @returns {Promise<string>} Stream URL
1371
- */
1372
- static async getStreamUrl(url) {
1373
- // Would require ytdl-core or similar
1374
- throw new Error('Stream URL extraction requires additional dependencies');
1375
- }
1434
+ class Plugin {
1435
+ constructor(name, version = '1.0.0') {
1436
+ this.enabled = false;
1437
+ this.loaded = false;
1438
+ this.name = name;
1439
+ this.version = version;
1440
+ }
1441
+ onLoad(engine) {
1442
+ this.engine = engine;
1443
+ this.loaded = true;
1444
+ }
1445
+ onUnload() {
1446
+ this.loaded = false;
1447
+ this.engine = undefined;
1448
+ }
1449
+ onEnable() {
1450
+ this.enabled = true;
1451
+ }
1452
+ onDisable() {
1453
+ this.enabled = false;
1454
+ }
1455
+ // Optional Hooks used by PluginManager.callHook
1456
+ beforePlay(track) { }
1457
+ afterPlay(track) { }
1458
+ trackEnd(track) { }
1459
+ queueUpdate(queue) { }
1376
1460
  }
1377
1461
 
1378
1462
  /**
1379
- * SoundCloud provider for basic info fetching
1463
+ * Logger utility with different levels
1380
1464
  */
1381
- class SoundCloudProvider {
1382
- /**
1383
- * Check if URL is a valid SoundCloud URL
1384
- * @param {string} url - URL to check
1385
- * @returns {boolean} Is valid SoundCloud URL
1386
- */
1387
- static isValidUrl(url) {
1388
- return url.includes('soundcloud.com/');
1389
- }
1390
-
1391
- /**
1392
- * Get basic track info from SoundCloud URL
1393
- * @param {string} url - SoundCloud URL
1394
- * @returns {Promise<Object>} Track info
1395
- */
1396
- static async getInfo(url) {
1397
- // In a real implementation, this would call SoundCloud API
1398
- // For now, return mock data
1399
- return {
1400
- title: 'SoundCloud Track',
1401
- artist: null,
1402
- duration: null,
1403
- thumbnail: null,
1404
- url: url,
1405
- source: 'soundcloud'
1406
- };
1407
- }
1408
-
1409
- /**
1410
- * Get stream URL (not implemented without dependencies)
1411
- * @param {string} url - SoundCloud URL
1412
- * @returns {Promise<string>} Stream URL
1413
- */
1414
- static async getStreamUrl(url) {
1415
- // Would require soundcloud-scraper or similar
1416
- throw new Error('Stream URL extraction requires additional dependencies');
1417
- }
1465
+ class Logger {
1466
+ constructor(level = 'info') {
1467
+ this.levels = {
1468
+ debug: 0,
1469
+ info: 1,
1470
+ warn: 2,
1471
+ error: 3
1472
+ };
1473
+ this.currentLevel = this.levels[level];
1474
+ }
1475
+ /**
1476
+ * Set log level
1477
+ * @param level - Log level (debug, info, warn, error)
1478
+ */
1479
+ setLevel(level) {
1480
+ this.currentLevel = this.levels[level] || this.levels.info;
1481
+ }
1482
+ /**
1483
+ * Debug log
1484
+ * @param args - Arguments to log
1485
+ */
1486
+ debug(...args) {
1487
+ if (this.currentLevel <= this.levels.debug) {
1488
+ console.debug('[DEBUG]', ...args);
1489
+ }
1490
+ }
1491
+ /**
1492
+ * Info log
1493
+ * @param args - Arguments to log
1494
+ */
1495
+ info(...args) {
1496
+ if (this.currentLevel <= this.levels.info) {
1497
+ console.info('[INFO]', ...args);
1498
+ }
1499
+ }
1500
+ /**
1501
+ * Warning log
1502
+ * @param args - Arguments to log
1503
+ */
1504
+ warn(...args) {
1505
+ if (this.currentLevel <= this.levels.warn) {
1506
+ console.warn('[WARN]', ...args);
1507
+ }
1508
+ }
1509
+ /**
1510
+ * Error log
1511
+ * @param args - Arguments to log
1512
+ */
1513
+ error(...args) {
1514
+ if (this.currentLevel <= this.levels.error) {
1515
+ console.error('[ERROR]', ...args);
1516
+ }
1517
+ }
1418
1518
  }
1519
+ // Default logger instance
1520
+ const logger = new Logger();
1419
1521
 
1420
1522
  /**
1421
- * Local file provider for Node.js
1523
+ * Time formatting utilities
1422
1524
  */
1423
- class LocalProvider {
1424
- /**
1425
- * Check if path is a valid local audio file
1426
- * @param {string} path - File path
1427
- * @returns {boolean} Is valid audio file
1428
- */
1429
- static isValidPath(path) {
1430
- const audioExtensions = ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a'];
1431
- return audioExtensions.includes(extname(path).toLowerCase());
1432
- }
1433
-
1434
- /**
1435
- * Get track info from local file
1436
- * @param {string} path - File path
1437
- * @returns {Promise<Object>} Track info
1438
- */
1439
- static async getInfo(path) {
1440
- try {
1441
- const stats = await promises.stat(path);
1442
- if (!stats.isFile()) {
1443
- throw new Error('Path is not a file');
1444
- }
1445
-
1446
- return {
1447
- title: path.split('/').pop().replace(extname(path), ''),
1448
- artist: null,
1449
- duration: null, // Would need audio parsing library
1450
- thumbnail: null,
1451
- url: `file://${path}`,
1452
- source: 'local',
1453
- size: stats.size,
1454
- modified: stats.mtime
1455
- };
1456
- } catch (error) {
1457
- throw new Error(`Failed to get file info: ${error.message}`);
1458
- }
1459
- }
1460
-
1461
- /**
1462
- * Check if file exists
1463
- * @param {string} path - File path
1464
- * @returns {Promise<boolean>} File exists
1465
- */
1466
- static async exists(path) {
1467
- try {
1468
- await promises.access(path);
1469
- return true;
1470
- } catch {
1471
- return false;
1472
- }
1473
- }
1525
+ class TimeUtils {
1526
+ /**
1527
+ * Format seconds to MM:SS or HH:MM:SS
1528
+ * @param seconds - Time in seconds
1529
+ * @returns Formatted time string
1530
+ */
1531
+ static format(seconds) {
1532
+ if (!Number.isFinite(seconds) || seconds < 0)
1533
+ return '00:00';
1534
+ const hours = Math.floor(seconds / 3600);
1535
+ const minutes = Math.floor((seconds % 3600) / 60);
1536
+ const secs = Math.floor(seconds % 60);
1537
+ if (hours > 0) {
1538
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1539
+ }
1540
+ return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1541
+ }
1542
+ /**
1543
+ * Parse time string to seconds
1544
+ * @param timeStr - Time string like "1:23" or "01:23:45"
1545
+ * @returns Time in seconds
1546
+ */
1547
+ static parse(timeStr) {
1548
+ if (!timeStr)
1549
+ return 0;
1550
+ const parts = timeStr.split(':').map(Number);
1551
+ if (parts.length === 2) {
1552
+ return parts[0] * 60 + parts[1];
1553
+ }
1554
+ else if (parts.length === 3) {
1555
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
1556
+ }
1557
+ return 0;
1558
+ }
1559
+ /**
1560
+ * Get current timestamp in milliseconds
1561
+ * @returns Current time
1562
+ */
1563
+ static now() {
1564
+ return Date.now();
1565
+ }
1566
+ /**
1567
+ * Calculate duration between two timestamps
1568
+ * @param start - Start time
1569
+ * @param end - End time
1570
+ * @returns Duration in milliseconds
1571
+ */
1572
+ static duration(start, end) {
1573
+ return end - start;
1574
+ }
1474
1575
  }
1475
1576
 
1476
- export { AudioEngine, EVENTS, EventBus, FILTER_TYPES, LOOP_MODES$1 as LOOP_MODES, LocalProvider, Logger, MetadataUtils, PluginManager, ProbeUtils, Queue, REPEAT_MODES, SoundCloudProvider, TimeUtils, Track, YouTubeProvider };
1577
+ export { AudioEngine, EVENTS, EventBus, FILTER_TYPES, Filters as FilterManager, Filters, LOOP_MODES, LavalinkProvider, LocalProvider, Logger, MetadataUtils, PLAYER_STATES, Plugin, PluginManager, Queue, SpotifyProvider, TimeUtils, Track, YouTubeProvider, logger };
1477
1578
  //# sourceMappingURL=index.js.map