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