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