@students-dev/audify-js 1.0.1 → 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 (48) hide show
  1. package/README.md +92 -441
  2. package/dist/AudioEngine.js +232 -0
  3. package/dist/cjs/index.js +1478 -1746
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/constants/index.js +35 -0
  6. package/dist/engine/Filters.js +137 -0
  7. package/dist/engine/MockAudioContext.js +53 -0
  8. package/dist/engine/Player.js +209 -0
  9. package/dist/esm/index.js +1474 -1744
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/events/EventBus.js +61 -0
  12. package/dist/index.js +18 -0
  13. package/dist/interfaces/index.js +1 -0
  14. package/dist/plugins/Plugin.js +27 -0
  15. package/dist/plugins/PluginManager.js +106 -0
  16. package/dist/providers/LavalinkProvider.js +81 -0
  17. package/dist/providers/LocalProvider.js +70 -0
  18. package/dist/providers/ProviderRegistry.js +20 -0
  19. package/dist/providers/SpotifyProvider.js +59 -0
  20. package/dist/providers/YouTubeProvider.js +48 -0
  21. package/dist/queue/Queue.js +186 -0
  22. package/dist/queue/Track.js +54 -0
  23. package/dist/types/AudioEngine.d.ts +107 -0
  24. package/dist/types/constants/index.d.ts +39 -0
  25. package/dist/types/engine/Filters.d.ts +25 -24
  26. package/dist/types/engine/MockAudioContext.d.ts +43 -0
  27. package/dist/types/engine/Player.d.ts +25 -21
  28. package/dist/types/events/EventBus.d.ts +17 -15
  29. package/dist/types/index.d.ts +17 -15
  30. package/dist/types/interfaces/index.d.ts +31 -0
  31. package/dist/types/plugins/Plugin.d.ts +11 -43
  32. package/dist/types/plugins/PluginManager.d.ts +19 -19
  33. package/dist/types/providers/LavalinkProvider.d.ts +15 -46
  34. package/dist/types/providers/LocalProvider.d.ts +11 -22
  35. package/dist/types/providers/ProviderRegistry.d.ts +10 -0
  36. package/dist/types/providers/SpotifyProvider.d.ts +12 -52
  37. package/dist/types/providers/YouTubeProvider.d.ts +11 -28
  38. package/dist/types/queue/Queue.d.ts +28 -22
  39. package/dist/types/queue/Track.d.ts +18 -16
  40. package/dist/types/utils/Logger.d.ts +12 -16
  41. package/dist/types/utils/Metadata.d.ts +16 -15
  42. package/dist/types/utils/Probe.d.ts +7 -7
  43. package/dist/types/utils/Time.d.ts +9 -9
  44. package/dist/utils/Logger.js +59 -0
  45. package/dist/utils/Metadata.js +90 -0
  46. package/dist/utils/Probe.js +59 -0
  47. package/dist/utils/Time.js +54 -0
  48. package/package.json +14 -8
package/dist/esm/index.js CHANGED
@@ -1,1848 +1,1578 @@
1
- import SpotifyWebApi from 'spotify-web-api-node';
2
- import { LavalinkManager } from 'lavalink-client';
3
1
  import { promises } from 'fs';
4
2
  import { extname } from 'path';
3
+ import SpotifyWebApi from 'spotify-web-api-node';
4
+ import { LavalinkManager } from 'lavalink-client';
5
5
 
6
- /**
7
- * Loop modes for playback
8
- */
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'
20
+ };
9
21
  const LOOP_MODES = {
10
- OFF: 'off',
11
- TRACK: 'track',
12
- QUEUE: 'queue'
22
+ OFF: 'off',
23
+ TRACK: 'track',
24
+ QUEUE: 'queue'
13
25
  };
14
-
15
- /**
16
- * Repeat modes (alias for loop modes)
17
- */
18
- const REPEAT_MODES = LOOP_MODES;
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'
26
+ const PLAYER_STATES = {
27
+ IDLE: 'idle',
28
+ PLAYING: 'playing',
29
+ PAUSED: 'paused',
30
+ BUFFERING: 'buffering'
32
31
  };
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'
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'
51
40
  };
52
41
 
53
42
  /**
54
43
  * Simple event emitter for handling events
55
44
  */
56
45
  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
- }
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
+ }
106
103
 
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
- }
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
+ }
115
156
  }
116
157
 
117
158
  /**
118
159
  * Audio player with playback controls
119
160
  */
120
161
  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.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.TRACK:
243
- // Replay current track
244
- break;
245
- case LOOP_MODES.QUEUE:
246
- // Play next in queue
247
- this.audioEngine.queue.getNext();
248
- break;
249
- case LOOP_MODES.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
- }
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
+ }
266
363
  }
267
364
 
268
365
  /**
269
366
  * Audio filters and effects
270
367
  */
271
368
  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;
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);
308
499
  }
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
500
  }
420
501
 
421
502
  /**
422
503
  * Metadata parsing utilities
423
504
  */
424
505
  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);
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;
443
533
  }
444
-
445
- // For SoundCloud URLs
446
- if (source.includes('soundcloud.com')) {
447
- 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';
448
556
  }
449
-
450
- // For local files
451
- if (!source.startsWith('http')) {
452
- 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
+ };
453
567
  }
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
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
+ };
477
590
  }
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
591
  }
530
592
 
531
593
  /**
532
594
  * Represents an audio track
533
595
  */
534
596
  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
- }
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
+ }
580
645
  }
581
646
 
582
647
  /**
583
648
  * Audio queue management
584
649
  */
585
650
  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);
651
+ constructor() {
652
+ this.tracks = [];
653
+ this.currentIndex = -1;
654
+ this.eventBus = new EventBus();
611
655
  }
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);
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);
629
683
  }
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;
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;
639
709
  }
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]];
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);
655
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;
828
+ }
829
+ }
656
830
 
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
- }
831
+ class ProviderRegistry {
832
+ constructor() {
833
+ this.providers = new Map();
834
+ }
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);
849
+ }
850
+ }
724
851
 
725
- /**
726
- * Check if queue is empty
727
- * @returns {boolean} Is empty
728
- */
729
- isEmpty() {
730
- return this.tracks.length === 0;
731
- }
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
+ }
918
+ }
732
919
 
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
- }
920
+ class YouTubeProvider {
921
+ constructor() {
922
+ this.name = 'youtube';
923
+ this.version = '1.0.0';
924
+ this.engine = null;
925
+ }
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) {
955
+ try {
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;
964
+ }
965
+ }
741
966
  }
742
967
 
743
- /**
744
- * Spotify provider for client-side API integration
745
- */
746
968
  class SpotifyProvider {
747
- constructor(options = {}) {
748
- this.spotifyApi = new SpotifyWebApi({
749
- clientId: options.clientId,
750
- clientSecret: options.clientSecret,
751
- redirectUri: options.redirectUri,
752
- accessToken: options.accessToken,
753
- refreshToken: options.refreshToken
754
- });
755
- }
756
-
757
- /**
758
- * Set access token
759
- * @param {string} token - OAuth access token
760
- */
761
- setAccessToken(token) {
762
- this.spotifyApi.setAccessToken(token);
763
- }
764
-
765
- /**
766
- * Set refresh token
767
- * @param {string} token - OAuth refresh token
768
- */
769
- setRefreshToken(token) {
770
- this.spotifyApi.setRefreshToken(token);
771
- }
772
-
773
- /**
774
- * Refresh access token
775
- * @returns {Promise<Object>} Token response
776
- */
777
- async refreshAccessToken() {
778
- try {
779
- const data = await this.spotifyApi.refreshAccessToken();
780
- this.spotifyApi.setAccessToken(data.body.access_token);
781
- return data.body;
782
- } catch (error) {
783
- throw new Error(`Failed to refresh token: ${error.message}`);
784
- }
785
- }
786
-
787
- /**
788
- * Search tracks
789
- * @param {string} query - Search query
790
- * @param {Object} options - Search options
791
- * @returns {Promise<Array>} Array of track objects
792
- */
793
- async searchTracks(query, options = {}) {
794
- try {
795
- const data = await this.spotifyApi.searchTracks(query, {
796
- limit: options.limit || 20,
797
- offset: options.offset || 0
798
- });
799
-
800
- return data.body.tracks.items.map(track => this._formatTrack(track));
801
- } catch (error) {
802
- throw new Error(`Failed to search tracks: ${error.message}`);
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
+ });
803
980
  }
804
- }
805
-
806
- /**
807
- * Get track by ID
808
- * @param {string} trackId - Spotify track ID
809
- * @returns {Promise<Object>} Track object
810
- */
811
- async getTrack(trackId) {
812
- try {
813
- const data = await this.spotifyApi.getTrack(trackId);
814
- return this._formatTrack(data.body);
815
- } catch (error) {
816
- throw new Error(`Failed to get track: ${error.message}`);
817
- }
818
- }
819
-
820
- /**
821
- * Get tracks by IDs
822
- * @param {Array<string>} trackIds - Array of Spotify track IDs
823
- * @returns {Promise<Array>} Array of track objects
824
- */
825
- async getTracks(trackIds) {
826
- try {
827
- const data = await this.spotifyApi.getTracks(trackIds);
828
- return data.body.tracks.map(track => this._formatTrack(track));
829
- } catch (error) {
830
- throw new Error(`Failed to get tracks: ${error.message}`);
831
- }
832
- }
833
-
834
- /**
835
- * Get audio features for track
836
- * @param {string} trackId - Spotify track ID
837
- * @returns {Promise<Object>} Audio features
838
- */
839
- async getAudioFeatures(trackId) {
840
- try {
841
- const data = await this.spotifyApi.getAudioFeaturesForTrack(trackId);
842
- return data.body;
843
- } catch (error) {
844
- throw new Error(`Failed to get audio features: ${error.message}`);
845
- }
846
- }
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
+ }
1024
+ }
847
1025
 
848
- /**
849
- * Format Spotify track to internal format
850
- * @param {Object} spotifyTrack - Spotify track object
851
- * @returns {Object} Formatted track
852
- * @private
853
- */
854
- _formatTrack(spotifyTrack) {
855
- return {
856
- id: spotifyTrack.id,
857
- title: spotifyTrack.name,
858
- artist: spotifyTrack.artists.map(artist => artist.name).join(', '),
859
- duration: Math.floor(spotifyTrack.duration_ms / 1000),
860
- thumbnail: spotifyTrack.album.images[0]?.url,
861
- url: spotifyTrack.external_urls.spotify,
862
- source: 'spotify',
863
- album: spotifyTrack.album.name,
864
- popularity: spotifyTrack.popularity,
865
- preview_url: spotifyTrack.preview_url,
866
- metadata: {
867
- spotifyId: spotifyTrack.id,
868
- artists: spotifyTrack.artists,
869
- album: spotifyTrack.album
870
- }
871
- };
872
- }
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
+ });
1103
+ }
873
1104
  }
874
1105
 
875
1106
  /**
876
- * Lavalink provider for connecting to Lavalink server
1107
+ * Plugin manager for loading and managing plugins
877
1108
  */
878
- class LavalinkProvider {
879
- constructor(options = {}) {
880
- this.host = options.host || 'localhost';
881
- this.port = options.port || 2333;
882
- this.password = options.password || 'youshallnotpass';
883
- this.secure = options.secure || false;
884
- this.manager = null;
885
- this.node = null;
886
- this.isConnected = false;
887
- }
888
-
889
- /**
890
- * Connect to Lavalink server
891
- * @returns {Promise<void>}
892
- */
893
- async connect() {
894
- try {
895
- this.manager = new LavalinkManager({
896
- nodes: [{
897
- host: this.host,
898
- port: this.port,
899
- password: this.password,
900
- secure: this.secure,
901
- id: 'main'
902
- }],
903
- sendToShard: (guildId, payload) => {
904
- // This would need to be implemented to send to Discord gateway
905
- // For now, this is a placeholder
906
- console.log('Send to shard:', guildId, payload);
907
- }
908
- });
909
-
910
- await this.manager.connect();
911
- this.node = this.manager.nodes.get('main');
912
- this.isConnected = true;
913
- } catch (error) {
914
- throw new Error(`Failed to connect to Lavalink: ${error.message}`);
1109
+ class PluginManager {
1110
+ constructor(engine) {
1111
+ this.engine = engine;
1112
+ this.plugins = new Map();
915
1113
  }
916
- }
917
-
918
- /**
919
- * Disconnect from Lavalink server
920
- */
921
- disconnect() {
922
- if (this.manager) {
923
- this.manager.destroy();
924
- this.isConnected = false;
925
- }
926
- }
927
-
928
- /**
929
- * Create a player for a guild/channel
930
- * @param {string} guildId - Guild ID
931
- * @param {string} channelId - Voice channel ID
932
- * @returns {Object} Player instance
933
- */
934
- createPlayer(guildId, channelId) {
935
- if (!this.isConnected) {
936
- throw new Error('Not connected to Lavalink');
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
+ }
937
1129
  }
938
-
939
- return this.manager.createPlayer({
940
- guildId,
941
- voiceChannelId: channelId,
942
- textChannelId: channelId, // Optional
943
- selfDeaf: false,
944
- selfMute: false
945
- });
946
- }
947
-
948
- /**
949
- * Load track from Lavalink
950
- * @param {string} identifier - Track identifier (URL or search query)
951
- * @returns {Promise<Object>} Track info
952
- */
953
- async loadTrack(identifier) {
954
- if (!this.isConnected) {
955
- throw new Error('Not connected to Lavalink');
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
+ }
956
1145
  }
957
-
958
- try {
959
- const result = await this.node.rest.loadTracks(identifier);
960
-
961
- if (result.loadType === 'TRACK_LOADED') {
962
- return this._formatTrack(result.tracks[0]);
963
- } else if (result.loadType === 'PLAYLIST_LOADED') {
964
- return result.tracks.map(track => this._formatTrack(track));
965
- } else if (result.loadType === 'SEARCH_RESULT') {
966
- return result.tracks.map(track => this._formatTrack(track));
967
- } else {
968
- throw new Error('No tracks found');
969
- }
970
- } catch (error) {
971
- throw new Error(`Failed to load track: ${error.message}`);
972
- }
973
- }
974
-
975
- /**
976
- * Format Lavalink track to internal format
977
- * @param {Object} lavalinkTrack - Lavalink track object
978
- * @returns {Object} Formatted track
979
- * @private
980
- */
981
- _formatTrack(lavalinkTrack) {
982
- const info = lavalinkTrack.info;
983
- return {
984
- id: lavalinkTrack.track,
985
- title: info.title,
986
- artist: info.author,
987
- duration: Math.floor(info.length / 1000),
988
- thumbnail: info.artworkUrl,
989
- url: info.uri,
990
- source: 'lavalink',
991
- isrc: info.isrc,
992
- metadata: {
993
- lavalinkTrack: lavalinkTrack.track,
994
- identifier: info.identifier,
995
- sourceName: info.sourceName
996
- }
997
- };
998
- }
999
-
1000
- /**
1001
- * Get node stats
1002
- * @returns {Promise<Object>} Node stats
1003
- */
1004
- async getStats() {
1005
- if (!this.isConnected) {
1006
- throw new Error('Not connected to Lavalink');
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
+ });
1007
1210
  }
1008
-
1009
- return await this.node.rest.getStats();
1010
- }
1011
1211
  }
1012
1212
 
1013
1213
  /**
1014
1214
  * Main audio engine class
1015
1215
  */
1016
1216
  class AudioEngine {
1017
- constructor(options = {}) {
1018
- this.options = options;
1019
- this.audioContext = null;
1020
- this.player = null;
1021
- this.filters = null;
1022
- this.queue = new Queue();
1023
- this.eventBus = new EventBus();
1024
- this.spotifyProvider = null;
1025
- this.lavalinkProvider = null;
1026
- this.isReady = false;
1027
-
1028
- this.initialize();
1029
- }
1030
-
1031
- /**
1032
- * Initialize the audio engine
1033
- */
1034
- async initialize() {
1035
- try {
1036
- // Create AudioContext
1037
- this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
1038
-
1039
- // Create components
1040
- this.filters = new Filters(this.audioContext);
1041
- this.player = new Player(this);
1042
-
1043
- // Connect event buses
1044
- this.queue.eventBus.on(EVENTS.TRACK_ADD, (track) => this.eventBus.emit(EVENTS.TRACK_ADD, track));
1045
- this.queue.eventBus.on(EVENTS.TRACK_REMOVE, (track) => this.eventBus.emit(EVENTS.TRACK_REMOVE, track));
1046
- this.player.eventBus.on(EVENTS.PLAY, (data) => this.eventBus.emit(EVENTS.PLAY, data));
1047
- this.player.eventBus.on(EVENTS.PAUSE, () => this.eventBus.emit(EVENTS.PAUSE));
1048
- this.player.eventBus.on(EVENTS.STOP, () => this.eventBus.emit(EVENTS.STOP));
1049
- this.player.eventBus.on(EVENTS.ERROR, (error) => this.eventBus.emit(EVENTS.ERROR, error));
1050
- this.player.eventBus.on(EVENTS.TRACK_START, (track) => this.eventBus.emit(EVENTS.TRACK_START, track));
1051
- this.player.eventBus.on(EVENTS.TRACK_END, (track) => this.eventBus.emit(EVENTS.TRACK_END, track));
1052
-
1053
- this.isReady = true;
1054
- this.eventBus.emit(EVENTS.READY);
1055
- } catch (error) {
1056
- this.eventBus.emit(EVENTS.ERROR, error);
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();
1057
1228
  }
1058
- }
1059
-
1060
- /**
1061
- * Play track or resume playback
1062
- * @param {Track|string} track - Track to play or track identifier
1063
- */
1064
- async play(track) {
1065
- if (!this.isReady) return;
1066
-
1067
- if (track) {
1068
- const trackObj = typeof track === 'string' ? this.queue.getCurrent() : track;
1069
- await this.player.play(trackObj);
1070
- } else {
1071
- this.player.resume();
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
+ }
1072
1268
  }
1073
- }
1074
-
1075
- /**
1076
- * Pause playback
1077
- */
1078
- pause() {
1079
- this.player.pause();
1080
- }
1081
-
1082
- /**
1083
- * Stop playback
1084
- */
1085
- stop() {
1086
- this.player.stop();
1087
- }
1088
-
1089
- /**
1090
- * Seek to position
1091
- * @param {number} time - Time in seconds
1092
- */
1093
- seek(time) {
1094
- this.player.seek(time);
1095
- }
1096
-
1097
- /**
1098
- * Set volume
1099
- * @param {number} volume - Volume level (0-1)
1100
- */
1101
- setVolume(volume) {
1102
- this.player.setVolume(volume);
1103
- }
1104
-
1105
- /**
1106
- * Add track(s) to queue
1107
- * @param {Track|Track[]|string|string[]} tracks - Track(s) to add
1108
- */
1109
- add(tracks) {
1110
- this.queue.add(tracks);
1111
- }
1112
-
1113
- /**
1114
- * Remove track from queue
1115
- * @param {number|string} identifier - Track index or ID
1116
- */
1117
- remove(identifier) {
1118
- return this.queue.remove(identifier);
1119
- }
1120
-
1121
- /**
1122
- * Skip to next track
1123
- */
1124
- next() {
1125
- const nextTrack = this.queue.getNext();
1126
- if (nextTrack) {
1127
- this.play(nextTrack);
1128
- }
1129
- }
1130
-
1131
- /**
1132
- * Go to previous track
1133
- */
1134
- previous() {
1135
- const prevTrack = this.queue.getPrevious();
1136
- if (prevTrack) {
1137
- this.play(prevTrack);
1138
- }
1139
- }
1140
-
1141
- /**
1142
- * Shuffle queue
1143
- */
1144
- shuffle() {
1145
- this.queue.shuffle();
1146
- }
1147
-
1148
- /**
1149
- * Clear queue
1150
- */
1151
- clear() {
1152
- this.queue.clear();
1153
- }
1154
-
1155
- /**
1156
- * Jump to track in queue
1157
- * @param {number} index - Track index
1158
- */
1159
- jump(index) {
1160
- const track = this.queue.jump(index);
1161
- if (track) {
1162
- this.play(track);
1163
- }
1164
- }
1165
-
1166
- /**
1167
- * Apply audio filter
1168
- * @param {string} type - Filter type
1169
- * @param {Object} options - Filter options
1170
- */
1171
- applyFilter(type, options) {
1172
- this.filters.apply(type, options);
1173
- this.eventBus.emit(EVENTS.FILTER_APPLIED, { type, options });
1174
- }
1175
-
1176
- /**
1177
- * Remove audio filter
1178
- * @param {string} type - Filter type
1179
- */
1180
- removeFilter(type) {
1181
- this.filters.remove(type);
1182
- }
1183
-
1184
- /**
1185
- * Set loop mode
1186
- * @param {string} mode - Loop mode
1187
- */
1188
- setLoopMode(mode) {
1189
- this.player.setLoopMode(mode);
1190
- }
1191
-
1192
- /**
1193
- * Get current state
1194
- * @returns {Object} Engine state
1195
- */
1196
- getState() {
1197
- return {
1198
- isReady: this.isReady,
1199
- isPlaying: this.player ? this.player.isPlaying : false,
1200
- currentTrack: this.queue.getCurrent(),
1201
- queue: this.queue.getTracks(),
1202
- volume: this.player ? this.player.volume : 1,
1203
- loopMode: this.player ? this.player.loopMode : LOOP_MODES.OFF,
1204
- filters: this.filters ? this.filters.getEnabled() : new Set()
1205
- };
1206
- }
1207
-
1208
- /**
1209
- * Initialize Spotify provider
1210
- * @param {Object} options - Spotify options
1211
- */
1212
- initSpotify(options = {}) {
1213
- this.spotifyProvider = new SpotifyProvider(options);
1214
- }
1215
-
1216
- /**
1217
- * Load Spotify track and add to queue
1218
- * @param {string} trackId - Spotify track ID
1219
- * @param {Object} options - Options including token
1220
- * @returns {Promise<Track>} Added track
1221
- */
1222
- async loadSpotifyTrack(trackId, options = {}) {
1223
- if (!this.spotifyProvider) {
1224
- throw new Error('Spotify provider not initialized');
1269
+ async registerProvider(provider) {
1270
+ await provider.initialize(this);
1271
+ this.providers.register(provider);
1225
1272
  }
1226
-
1227
- if (options.token) {
1228
- this.spotifyProvider.setAccessToken(options.token);
1273
+ getProvider(name) {
1274
+ return this.providers.get(name);
1229
1275
  }
1230
-
1231
- const trackData = await this.spotifyProvider.getTrack(trackId);
1232
- const track = new Track(trackData.url, trackData);
1233
- this.add(track);
1234
- return track;
1235
- }
1236
-
1237
- /**
1238
- * Search Spotify tracks
1239
- * @param {string} query - Search query
1240
- * @param {Object} options - Search options
1241
- * @returns {Promise<Array>} Search results
1242
- */
1243
- async searchSpotifyTracks(query, options = {}) {
1244
- if (!this.spotifyProvider) {
1245
- throw new Error('Spotify provider not initialized');
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
+ }
1246
1298
  }
1247
-
1248
- if (options.token) {
1249
- this.spotifyProvider.setAccessToken(options.token);
1299
+ /**
1300
+ * Pause playback
1301
+ */
1302
+ pause() {
1303
+ this.player.pause();
1250
1304
  }
1251
-
1252
- return await this.spotifyProvider.searchTracks(query, options);
1253
- }
1254
-
1255
- /**
1256
- * Connect to Lavalink server
1257
- * @param {Object} options - Lavalink connection options
1258
- * @returns {Promise<void>}
1259
- */
1260
- async connectLavalink(options = {}) {
1261
- this.lavalinkProvider = new LavalinkProvider(options);
1262
- await this.lavalinkProvider.connect();
1263
- }
1264
-
1265
- /**
1266
- * Load Lavalink track and add to queue
1267
- * @param {string} identifier - Track identifier
1268
- * @returns {Promise<Track|Array<Track>>} Added track(s)
1269
- */
1270
- async loadLavalinkTrack(identifier) {
1271
- if (!this.lavalinkProvider) {
1272
- throw new Error('Lavalink provider not connected');
1305
+ /**
1306
+ * Stop playback
1307
+ */
1308
+ stop() {
1309
+ this.player.stop();
1273
1310
  }
1274
-
1275
- const trackData = await this.lavalinkProvider.loadTrack(identifier);
1276
-
1277
- if (Array.isArray(trackData)) {
1278
- const tracks = trackData.map(data => new Track(data.url, data));
1279
- this.add(tracks);
1280
- return tracks;
1281
- } else {
1282
- const track = new Track(trackData.url, trackData);
1283
- this.add(track);
1284
- return track;
1285
- }
1286
- }
1287
-
1288
- /**
1289
- * Get Lavalink player for guild/channel
1290
- * @param {string} guildId - Guild ID
1291
- * @param {string} channelId - Voice channel ID
1292
- * @returns {Object} Lavalink player
1293
- */
1294
- getLavalinkPlayer(guildId, channelId) {
1295
- if (!this.lavalinkProvider) {
1296
- throw new Error('Lavalink provider not connected');
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
+ }
1297
1431
  }
1298
-
1299
- return this.lavalinkProvider.createPlayer(guildId, channelId);
1300
- }
1301
-
1302
- /**
1303
- * Destroy the engine
1304
- */
1305
- destroy() {
1306
- if (this.audioContext) {
1307
- this.audioContext.close();
1308
- }
1309
- this.filters.clear();
1310
- this.player.stop();
1311
- if (this.lavalinkProvider) {
1312
- this.lavalinkProvider.disconnect();
1313
- }
1314
- }
1315
1432
  }
1316
1433
 
1317
- /**
1318
- * Base plugin class
1319
- */
1320
1434
  class Plugin {
1321
- constructor(name, version = '1.0.0') {
1322
- this.name = name;
1323
- this.version = version;
1324
- this.enabled = false;
1325
- this.loaded = false;
1326
- }
1327
-
1328
- /**
1329
- * Called when plugin is loaded
1330
- * @param {AudioEngine} engine - Audio engine instance
1331
- */
1332
- onLoad(engine) {
1333
- this.engine = engine;
1334
- this.loaded = true;
1335
- }
1336
-
1337
- /**
1338
- * Called when plugin is enabled
1339
- */
1340
- onEnable() {
1341
- this.enabled = true;
1342
- }
1343
-
1344
- /**
1345
- * Called when plugin is disabled
1346
- */
1347
- onDisable() {
1348
- this.enabled = false;
1349
- }
1350
-
1351
- /**
1352
- * Hook called before play
1353
- * @param {Track} track - Track being played
1354
- */
1355
- beforePlay(track) {
1356
- // Override in subclass
1357
- }
1358
-
1359
- /**
1360
- * Hook called after play
1361
- * @param {Track} track - Track being played
1362
- */
1363
- afterPlay(track) {
1364
- // Override in subclass
1365
- }
1366
-
1367
- /**
1368
- * Hook called when track ends
1369
- * @param {Track} track - Track that ended
1370
- */
1371
- trackEnd(track) {
1372
- // Override in subclass
1373
- }
1374
-
1375
- /**
1376
- * Hook called when queue updates
1377
- * @param {Queue} queue - Updated queue
1378
- */
1379
- queueUpdate(queue) {
1380
- // Override in subclass
1381
- }
1382
-
1383
- /**
1384
- * Get plugin info
1385
- * @returns {Object} Plugin information
1386
- */
1387
- getInfo() {
1388
- return {
1389
- name: this.name,
1390
- version: this.version,
1391
- enabled: this.enabled,
1392
- loaded: this.loaded
1393
- };
1394
- }
1395
- }
1396
-
1397
- /**
1398
- * Plugin manager for loading and managing plugins
1399
- */
1400
- class PluginManager {
1401
- constructor(audioEngine) {
1402
- this.engine = audioEngine;
1403
- this.plugins = new Map();
1404
- }
1405
-
1406
- /**
1407
- * Load a plugin
1408
- * @param {Plugin} plugin - Plugin instance
1409
- */
1410
- load(plugin) {
1411
- if (!(plugin instanceof Plugin)) {
1412
- throw new Error('Invalid plugin instance');
1435
+ constructor(name, version = '1.0.0') {
1436
+ this.enabled = false;
1437
+ this.loaded = false;
1438
+ this.name = name;
1439
+ this.version = version;
1413
1440
  }
1414
-
1415
- plugin.onLoad(this.engine);
1416
- this.plugins.set(plugin.name, plugin);
1417
- }
1418
-
1419
- /**
1420
- * Enable a plugin
1421
- * @param {string} name - Plugin name
1422
- */
1423
- enable(name) {
1424
- const plugin = this.plugins.get(name);
1425
- if (plugin && !plugin.enabled) {
1426
- plugin.onEnable();
1427
- }
1428
- }
1429
-
1430
- /**
1431
- * Disable a plugin
1432
- * @param {string} name - Plugin name
1433
- */
1434
- disable(name) {
1435
- const plugin = this.plugins.get(name);
1436
- if (plugin && plugin.enabled) {
1437
- plugin.onDisable();
1438
- }
1439
- }
1440
-
1441
- /**
1442
- * Unload a plugin
1443
- * @param {string} name - Plugin name
1444
- */
1445
- unload(name) {
1446
- const plugin = this.plugins.get(name);
1447
- if (plugin) {
1448
- if (plugin.enabled) {
1449
- plugin.onDisable();
1450
- }
1451
- this.plugins.delete(name);
1452
- }
1453
- }
1454
-
1455
- /**
1456
- * Get plugin by name
1457
- * @param {string} name - Plugin name
1458
- * @returns {Plugin|null} Plugin instance
1459
- */
1460
- get(name) {
1461
- return this.plugins.get(name) || null;
1462
- }
1463
-
1464
- /**
1465
- * Get all plugins
1466
- * @returns {Map} Map of plugins
1467
- */
1468
- getAll() {
1469
- return new Map(this.plugins);
1470
- }
1471
-
1472
- /**
1473
- * Get enabled plugins
1474
- * @returns {Plugin[]} Array of enabled plugins
1475
- */
1476
- getEnabled() {
1477
- return Array.from(this.plugins.values()).filter(plugin => plugin.enabled);
1478
- }
1479
-
1480
- /**
1481
- * Call hook on all enabled plugins
1482
- * @param {string} hook - Hook name
1483
- * @param {...*} args - Arguments to pass
1484
- */
1485
- callHook(hook, ...args) {
1486
- this.getEnabled().forEach(plugin => {
1487
- if (typeof plugin[hook] === 'function') {
1488
- try {
1489
- plugin[hook](...args);
1490
- } catch (error) {
1491
- console.error(`Error in plugin ${plugin.name} hook ${hook}:`, error);
1492
- }
1493
- }
1494
- });
1495
- }
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) { }
1496
1460
  }
1497
1461
 
1498
1462
  /**
1499
1463
  * Logger utility with different levels
1500
1464
  */
1501
1465
  class Logger {
1502
- constructor(level = 'info') {
1503
- this.levels = {
1504
- debug: 0,
1505
- info: 1,
1506
- warn: 2,
1507
- error: 3
1508
- };
1509
- this.currentLevel = this.levels[level] || this.levels.info;
1510
- }
1511
-
1512
- /**
1513
- * Set log level
1514
- * @param {string} level - Log level (debug, info, warn, error)
1515
- */
1516
- setLevel(level) {
1517
- this.currentLevel = this.levels[level] || this.levels.info;
1518
- }
1519
-
1520
- /**
1521
- * Debug log
1522
- * @param {...*} args - Arguments to log
1523
- */
1524
- debug(...args) {
1525
- if (this.currentLevel <= this.levels.debug) {
1526
- console.debug('[DEBUG]', ...args);
1527
- }
1528
- }
1529
-
1530
- /**
1531
- * Info log
1532
- * @param {...*} args - Arguments to log
1533
- */
1534
- info(...args) {
1535
- if (this.currentLevel <= this.levels.info) {
1536
- console.info('[INFO]', ...args);
1537
- }
1538
- }
1539
-
1540
- /**
1541
- * Warning log
1542
- * @param {...*} args - Arguments to log
1543
- */
1544
- warn(...args) {
1545
- if (this.currentLevel <= this.levels.warn) {
1546
- console.warn('[WARN]', ...args);
1547
- }
1548
- }
1549
-
1550
- /**
1551
- * Error log
1552
- * @param {...*} args - Arguments to log
1553
- */
1554
- error(...args) {
1555
- if (this.currentLevel <= this.levels.error) {
1556
- console.error('[ERROR]', ...args);
1557
- }
1558
- }
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
+ }
1559
1518
  }
1560
-
1561
1519
  // Default logger instance
1562
- new Logger();
1520
+ const logger = new Logger();
1563
1521
 
1564
1522
  /**
1565
1523
  * Time formatting utilities
1566
1524
  */
1567
1525
  class TimeUtils {
1568
- /**
1569
- * Format seconds to MM:SS or HH:MM:SS
1570
- * @param {number} seconds - Time in seconds
1571
- * @returns {string} Formatted time string
1572
- */
1573
- static format(seconds) {
1574
- if (!Number.isFinite(seconds) || seconds < 0) return '00:00';
1575
-
1576
- const hours = Math.floor(seconds / 3600);
1577
- const minutes = Math.floor((seconds % 3600) / 60);
1578
- const secs = Math.floor(seconds % 60);
1579
-
1580
- if (hours > 0) {
1581
- return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
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')}`;
1582
1541
  }
1583
- return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1584
- }
1585
-
1586
- /**
1587
- * Parse time string to seconds
1588
- * @param {string} timeStr - Time string like "1:23" or "01:23:45"
1589
- * @returns {number} Time in seconds
1590
- */
1591
- static parse(timeStr) {
1592
- if (!timeStr || typeof timeStr !== 'string') return 0;
1593
-
1594
- const parts = timeStr.split(':').map(Number);
1595
- if (parts.length === 2) {
1596
- return parts[0] * 60 + parts[1];
1597
- } else if (parts.length === 3) {
1598
- return parts[0] * 3600 + parts[1] * 60 + parts[2];
1599
- }
1600
- return 0;
1601
- }
1602
-
1603
- /**
1604
- * Get current timestamp in milliseconds
1605
- * @returns {number} Current time
1606
- */
1607
- static now() {
1608
- return Date.now();
1609
- }
1610
-
1611
- /**
1612
- * Calculate duration between two timestamps
1613
- * @param {number} start - Start time
1614
- * @param {number} end - End time
1615
- * @returns {number} Duration in milliseconds
1616
- */
1617
- static duration(start, end) {
1618
- return end - start;
1619
- }
1620
- }
1621
-
1622
- /**
1623
- * Audio probing utilities
1624
- */
1625
- class ProbeUtils {
1626
- /**
1627
- * Probe audio file/stream for basic info
1628
- * @param {string|Buffer|ReadableStream} source - Audio source
1629
- * @returns {Promise<Object>} Probe result
1630
- */
1631
- static async probe(source) {
1632
- // In a real implementation, this would use ffprobe or similar
1633
- // For now, return basic mock data
1634
- return {
1635
- duration: null, // seconds
1636
- format: null, // e.g., 'mp3', 'wav'
1637
- bitrate: null, // kbps
1638
- sampleRate: null, // Hz
1639
- channels: null // 1 or 2
1640
- };
1641
- }
1642
-
1643
- /**
1644
- * Check if source is a valid audio URL
1645
- * @param {string} url - URL to check
1646
- * @returns {boolean} Is valid audio URL
1647
- */
1648
- static isValidAudioUrl(url) {
1649
- if (!url || typeof url !== 'string') return false;
1650
-
1651
- try {
1652
- const parsed = new URL(url);
1653
- const audioExtensions = ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a'];
1654
- const path = parsed.pathname.toLowerCase();
1655
-
1656
- return audioExtensions.some(ext => path.endsWith(ext)) ||
1657
- url.includes('youtube.com') ||
1658
- url.includes('youtu.be') ||
1659
- url.includes('soundcloud.com');
1660
- } catch {
1661
- return false;
1662
- }
1663
- }
1664
-
1665
- /**
1666
- * Get audio format from URL or buffer
1667
- * @param {string|Buffer} source - Audio source
1668
- * @returns {string|null} Audio format
1669
- */
1670
- static getFormat(source) {
1671
- if (typeof source === 'string') {
1672
- const extensions = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
1673
- for (const ext of extensions) {
1674
- if (source.toLowerCase().includes(`.${ext}`)) {
1675
- return ext;
1676
- }
1677
- }
1678
- }
1679
- // For buffer, would need to check headers
1680
- return null;
1681
- }
1682
- }
1683
-
1684
- /**
1685
- * YouTube provider for basic info fetching
1686
- */
1687
- class YouTubeProvider {
1688
- /**
1689
- * Check if URL is a valid YouTube URL
1690
- * @param {string} url - URL to check
1691
- * @returns {boolean} Is valid YouTube URL
1692
- */
1693
- static isValidUrl(url) {
1694
- return url.includes('youtube.com/watch') || url.includes('youtu.be/');
1695
- }
1696
-
1697
- /**
1698
- * Extract video ID from YouTube URL
1699
- * @param {string} url - YouTube URL
1700
- * @returns {string|null} Video ID
1701
- */
1702
- static extractVideoId(url) {
1703
- try {
1704
- const urlObj = new URL(url);
1705
- if (urlObj.hostname === 'youtu.be') {
1706
- return urlObj.pathname.slice(1);
1707
- }
1708
- return urlObj.searchParams.get('v');
1709
- } catch {
1710
- return null;
1711
- }
1712
- }
1713
-
1714
- /**
1715
- * Get basic track info from YouTube URL
1716
- * @param {string} url - YouTube URL
1717
- * @returns {Promise<Object>} Track info
1718
- */
1719
- static async getInfo(url) {
1720
- const videoId = this.extractVideoId(url);
1721
- if (!videoId) {
1722
- throw new Error('Invalid YouTube URL');
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;
1723
1574
  }
1724
-
1725
- // In a real implementation, this would call YouTube API
1726
- // For now, return mock data
1727
- return {
1728
- title: `YouTube Video ${videoId}`,
1729
- artist: null,
1730
- duration: null,
1731
- thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
1732
- url: url,
1733
- source: 'youtube',
1734
- videoId: videoId
1735
- };
1736
- }
1737
-
1738
- /**
1739
- * Get stream URL (not implemented without dependencies)
1740
- * @param {string} url - YouTube URL
1741
- * @returns {Promise<string>} Stream URL
1742
- */
1743
- static async getStreamUrl(url) {
1744
- // Would require ytdl-core or similar
1745
- throw new Error('Stream URL extraction requires additional dependencies');
1746
- }
1747
- }
1748
-
1749
- /**
1750
- * SoundCloud provider for basic info fetching
1751
- */
1752
- class SoundCloudProvider {
1753
- /**
1754
- * Check if URL is a valid SoundCloud URL
1755
- * @param {string} url - URL to check
1756
- * @returns {boolean} Is valid SoundCloud URL
1757
- */
1758
- static isValidUrl(url) {
1759
- return url.includes('soundcloud.com/');
1760
- }
1761
-
1762
- /**
1763
- * Get basic track info from SoundCloud URL
1764
- * @param {string} url - SoundCloud URL
1765
- * @returns {Promise<Object>} Track info
1766
- */
1767
- static async getInfo(url) {
1768
- // In a real implementation, this would call SoundCloud API
1769
- // For now, return mock data
1770
- return {
1771
- title: 'SoundCloud Track',
1772
- artist: null,
1773
- duration: null,
1774
- thumbnail: null,
1775
- url: url,
1776
- source: 'soundcloud'
1777
- };
1778
- }
1779
-
1780
- /**
1781
- * Get stream URL (not implemented without dependencies)
1782
- * @param {string} url - SoundCloud URL
1783
- * @returns {Promise<string>} Stream URL
1784
- */
1785
- static async getStreamUrl(url) {
1786
- // Would require soundcloud-scraper or similar
1787
- throw new Error('Stream URL extraction requires additional dependencies');
1788
- }
1789
- }
1790
-
1791
- /**
1792
- * Local file provider for Node.js
1793
- */
1794
- class LocalProvider {
1795
- /**
1796
- * Check if path is a valid local audio file
1797
- * @param {string} path - File path
1798
- * @returns {boolean} Is valid audio file
1799
- */
1800
- static isValidPath(path) {
1801
- const audioExtensions = ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a'];
1802
- return audioExtensions.includes(extname(path).toLowerCase());
1803
- }
1804
-
1805
- /**
1806
- * Get track info from local file
1807
- * @param {string} path - File path
1808
- * @returns {Promise<Object>} Track info
1809
- */
1810
- static async getInfo(path) {
1811
- try {
1812
- const stats = await promises.stat(path);
1813
- if (!stats.isFile()) {
1814
- throw new Error('Path is not a file');
1815
- }
1816
-
1817
- return {
1818
- title: path.split('/').pop().replace(extname(path), ''),
1819
- artist: null,
1820
- duration: null, // Would need audio parsing library
1821
- thumbnail: null,
1822
- url: `file://${path}`,
1823
- source: 'local',
1824
- size: stats.size,
1825
- modified: stats.mtime
1826
- };
1827
- } catch (error) {
1828
- throw new Error(`Failed to get file info: ${error.message}`);
1829
- }
1830
- }
1831
-
1832
- /**
1833
- * Check if file exists
1834
- * @param {string} path - File path
1835
- * @returns {Promise<boolean>} File exists
1836
- */
1837
- static async exists(path) {
1838
- try {
1839
- await promises.access(path);
1840
- return true;
1841
- } catch {
1842
- return false;
1843
- }
1844
- }
1845
1575
  }
1846
1576
 
1847
- export { AudioEngine, EVENTS, EventBus, FILTER_TYPES, LOOP_MODES, LavalinkProvider, LocalProvider, Logger, MetadataUtils, PluginManager, ProbeUtils, Queue, REPEAT_MODES, SoundCloudProvider, SpotifyProvider, 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 };
1848
1578
  //# sourceMappingURL=index.js.map