streamify-audio 2.1.14 → 2.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamify-audio",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
4
4
  "description": "Dual-mode audio library: HTTP streaming proxy + Discord player (Lavalink alternative). Supports YouTube, Spotify, SoundCloud with audio filters.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -29,7 +29,8 @@
29
29
  "author": "Lucas",
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
- "express": "^4.18.2"
32
+ "express": "^4.18.2",
33
+ "opusscript": "^0.0.8"
33
34
  },
34
35
  "optionalDependencies": {
35
36
  "@discordjs/opus": "^0.9.0",
@@ -217,7 +217,7 @@ class Manager extends EventEmitter {
217
217
  if (!this._isProviderEnabled('youtube')) {
218
218
  throw new Error('YouTube provider is disabled');
219
219
  }
220
- result = await youtube.search(query, limit, this.config);
220
+ result = await youtube.search(query, limit, this.config, options);
221
221
  break;
222
222
 
223
223
  case 'spotify':
@@ -218,6 +218,10 @@ class Player extends EventEmitter {
218
218
  });
219
219
 
220
220
  this.audioPlayer.on('error', (error) => {
221
+ if (this._manualSkip || this._changingStream) {
222
+ return;
223
+ }
224
+
221
225
  if (error.message === 'Premature close' || error.message.includes('EPIPE')) {
222
226
  return;
223
227
  }
@@ -234,7 +238,8 @@ class Player extends EventEmitter {
234
238
 
235
239
  const next = this.queue.shift();
236
240
  if (next) {
237
- this._playTrack(next);
241
+ // Use setImmediate to break recursion stack on consecutive failures
242
+ setImmediate(() => this._playTrack(next));
238
243
  } else {
239
244
  this.emit('queueEnd');
240
245
  }
@@ -309,7 +314,7 @@ class Player extends EventEmitter {
309
314
  };
310
315
 
311
316
  if (startPosition === 0 && this._prefetchedTrack?.id === track.id && this._prefetchedStream) {
312
- log.info('PLAYER', `Using prefetched stream for ${track.id}`);
317
+ log.debug('PLAYER', `Using prefetched stream for ${track.id}`);
313
318
  this.stream = this._prefetchedStream;
314
319
  this._prefetchedStream = null;
315
320
  this._prefetchedTrack = null;
@@ -332,10 +337,15 @@ class Player extends EventEmitter {
332
337
  } catch (error) {
333
338
  log.error('PLAYER', `Failed to play track: ${error.message}`);
334
339
  this.emit('trackError', track, error);
340
+
341
+ // If manual skip is active, we just return so the skip function handles it
342
+ if (this._manualSkip) return;
335
343
 
336
344
  const next = this.queue.shift();
337
345
  if (next) {
338
- return this._playTrack(next);
346
+ // Use setImmediate to break recursion stack on consecutive failures
347
+ setImmediate(() => this._playTrack(next));
348
+ return;
339
349
  } else {
340
350
  this.emit('queueEnd');
341
351
  }
@@ -351,7 +361,7 @@ class Player extends EventEmitter {
351
361
  this._prefetching = true;
352
362
  this._clearPrefetch();
353
363
 
354
- log.info('PLAYER', `Prefetching: ${nextTrack.title} (${nextTrack.id})`);
364
+ log.debug('PLAYER', `Prefetching: ${nextTrack.title} (${nextTrack.id})`);
355
365
 
356
366
  try {
357
367
  const filtersWithVolume = {
@@ -363,7 +373,7 @@ class Player extends EventEmitter {
363
373
  this._prefetchedStream = createStream(nextTrack, filtersWithVolume, this.config);
364
374
  await this._prefetchedStream.create();
365
375
 
366
- log.info('PLAYER', `Prefetch ready: ${nextTrack.id}`);
376
+ log.debug('PLAYER', `Prefetch ready: ${nextTrack.id}`);
367
377
  } catch (error) {
368
378
  log.debug('PLAYER', `Prefetch failed: ${error.message}`);
369
379
  this._clearPrefetch();
@@ -429,7 +439,7 @@ class Player extends EventEmitter {
429
439
  }
430
440
 
431
441
  this.audioPlayer.stop(true);
432
- log.info('PLAYER', `Paused at ${Math.floor(this._positionMs / 1000)}s (stream destroyed)`);
442
+ log.info('PLAYER', `Paused playback at ${Math.floor(this._positionMs / 1000)}s (Reason: User Request)`);
433
443
 
434
444
  return true;
435
445
  }
@@ -438,7 +448,7 @@ class Player extends EventEmitter {
438
448
  if (!this._playing || !this._paused) return false;
439
449
 
440
450
  if (!this.stream && this.queue.current) {
441
- log.info('PLAYER', `Resuming from ${Math.floor(this._positionMs / 1000)}s (recreating stream)`);
451
+ log.info('PLAYER', `Resuming playback from ${Math.floor(this._positionMs / 1000)}s`);
442
452
 
443
453
  this._changingStream = true;
444
454
  const track = this.queue.current;
@@ -488,12 +498,17 @@ class Player extends EventEmitter {
488
498
  const next = this.queue.shift();
489
499
  this.audioPlayer.stop();
490
500
 
491
- if (next) {
492
- await this._playTrack(next);
501
+ log.info('PLAYER', `Skipping track (Next: ${next ? next.title : 'End of queue'})`);
502
+
503
+ try {
504
+ if (next) {
505
+ await this._playTrack(next);
506
+ }
507
+ } finally {
508
+ this._manualSkip = false;
493
509
  }
494
510
 
495
- this._manualSkip = false;
496
- return next;
511
+ return this.queue.current || next;
497
512
  }
498
513
 
499
514
  async previous() {
@@ -508,9 +523,16 @@ class Player extends EventEmitter {
508
523
 
509
524
  this._manualSkip = true;
510
525
  this.audioPlayer.stop();
511
- const result = await this._playTrack(prev);
512
- this._manualSkip = false;
513
- return result;
526
+
527
+ log.info('PLAYER', `Rewinding to previous track: ${prev.title}`);
528
+
529
+ try {
530
+ await this._playTrack(prev);
531
+ } finally {
532
+ this._manualSkip = false;
533
+ }
534
+
535
+ return this.queue.current || prev;
514
536
  }
515
537
 
516
538
  stop() {
@@ -527,11 +549,16 @@ class Player extends EventEmitter {
527
549
  this._positionMs = 0;
528
550
  this.queue.clear();
529
551
  this.queue.setCurrent(null);
552
+
553
+ log.info('PLAYER', 'Stopped playback and cleared queue');
554
+
530
555
  return true;
531
556
  }
532
557
 
533
558
  setVolume(volume) {
559
+ const oldVolume = this._volume;
534
560
  this._volume = Math.max(0, Math.min(200, volume));
561
+ log.info('PLAYER', `Volume adjusted: ${oldVolume}% -> ${this._volume}%`);
535
562
  return this._volume;
536
563
  }
537
564
 
@@ -550,6 +577,8 @@ class Player extends EventEmitter {
550
577
  this.stream.destroy();
551
578
  }
552
579
 
580
+ log.info('PLAYER', `Seeking to ${Math.floor(positionMs / 1000)}s`);
581
+
553
582
  try {
554
583
  this.stream = createStream(track, filtersWithVolume, this.config);
555
584
  const resource = await this.stream.create(positionMs);
@@ -565,6 +594,7 @@ class Player extends EventEmitter {
565
594
  }
566
595
 
567
596
  setLoop(mode) {
597
+ log.info('PLAYER', `Loop mode changed to: ${mode.toUpperCase()}`);
568
598
  return this.queue.setRepeatMode(mode);
569
599
  }
570
600
 
@@ -602,6 +632,7 @@ class Player extends EventEmitter {
602
632
 
603
633
  async clearFilters() {
604
634
  this._filters = {};
635
+ log.info('PLAYER', 'All audio filters cleared');
605
636
  if (this._playing && this.queue.current) {
606
637
  return this.setFilter('_trigger', null);
607
638
  }
@@ -612,6 +643,7 @@ class Player extends EventEmitter {
612
643
  if (!Array.isArray(bands) || bands.length !== 15) {
613
644
  throw new Error('EQ must be an array of 15 band gains (-0.25 to 1.0)');
614
645
  }
646
+ log.info('PLAYER', 'Applying custom 15-band Equalizer');
615
647
  return this.setFilter('equalizer', bands);
616
648
  }
617
649
 
@@ -621,12 +653,14 @@ class Player extends EventEmitter {
621
653
  throw new Error(`Unknown preset: ${presetName}. Available: ${Object.keys(PRESETS).join(', ')}`);
622
654
  }
623
655
  delete this._filters.equalizer;
656
+ log.info('PLAYER', `Applying EQ preset: ${presetName.toUpperCase()}`);
624
657
  return this.setFilter('preset', presetName);
625
658
  }
626
659
 
627
660
  async clearEQ() {
628
661
  delete this._filters.equalizer;
629
662
  delete this._filters.preset;
663
+ log.info('PLAYER', 'Equalizer cleared');
630
664
  if (this._playing && this.queue.current) {
631
665
  return this.setFilter('_trigger', null);
632
666
  }
@@ -644,7 +678,6 @@ class Player extends EventEmitter {
644
678
  }
645
679
 
646
680
  const replace = options.replace ?? false;
647
- const appliedFilters = {};
648
681
  const newPresets = [];
649
682
 
650
683
  for (const preset of presets) {
@@ -655,17 +688,11 @@ class Player extends EventEmitter {
655
688
  log.warn('PLAYER', `Unknown effect preset: ${name}`);
656
689
  continue;
657
690
  }
658
-
659
- const filters = applyEffectPreset(name, intensity);
660
- if (filters) {
661
- Object.assign(appliedFilters, filters);
662
- newPresets.push({ name, intensity });
663
- }
691
+ newPresets.push({ name, intensity });
664
692
  }
665
693
 
666
694
  if (replace) {
667
695
  this._effectPresets = newPresets;
668
- this._filters = appliedFilters;
669
696
  } else {
670
697
  const existingNames = this._effectPresets.map(p => p.name);
671
698
  for (const preset of newPresets) {
@@ -676,9 +703,19 @@ class Player extends EventEmitter {
676
703
  this._effectPresets.push(preset);
677
704
  }
678
705
  }
679
- this._filters = { ...this._filters, ...appliedFilters };
680
706
  }
681
707
 
708
+ // Re-calculate all filters from active presets
709
+ this._filters = {};
710
+ for (const preset of this._effectPresets) {
711
+ const filters = applyEffectPreset(preset.name, preset.intensity);
712
+ if (filters) {
713
+ Object.assign(this._filters, filters);
714
+ }
715
+ }
716
+
717
+ log.info('PLAYER', `Active effects: ${this._effectPresets.map(p => p.name).join(' + ') || 'NONE'}`);
718
+
682
719
  if (this._playing && this.queue.current) {
683
720
  return this.setFilter('_trigger', null);
684
721
  }
@@ -692,6 +729,7 @@ class Player extends EventEmitter {
692
729
  async clearEffectPresets() {
693
730
  this._effectPresets = [];
694
731
  this._filters = {};
732
+ log.info('PLAYER', 'All effect presets cleared');
695
733
  if (this._playing && this.queue.current) {
696
734
  return this.setFilter('_trigger', null);
697
735
  }
@@ -764,4 +802,4 @@ class Player extends EventEmitter {
764
802
  }
765
803
  }
766
804
 
767
- module.exports = Player;
805
+ module.exports = Player;
@@ -22,6 +22,17 @@ class StreamController {
22
22
  this.destroyed = false;
23
23
  this.startTime = null;
24
24
  this.bytesReceived = 0;
25
+ this.bytesSent = 0;
26
+ this.ytdlpError = '';
27
+ this.ffmpegError = '';
28
+
29
+ // Timing metrics
30
+ this.metrics = {
31
+ metadata: 0,
32
+ spawn: 0,
33
+ firstByte: 0,
34
+ total: 0
35
+ };
25
36
  }
26
37
 
27
38
  async create(seekPosition = 0) {
@@ -34,6 +45,7 @@ class StreamController {
34
45
  }
35
46
 
36
47
  this.startTime = Date.now();
48
+ const startTimestamp = this.startTime;
37
49
  const source = this.track.source || 'youtube';
38
50
 
39
51
  let videoId = this.track._resolvedId || this.track.id;
@@ -44,10 +56,13 @@ class StreamController {
44
56
  const spotify = require('../providers/spotify');
45
57
  videoId = await spotify.resolveToYouTube(this.track.id, this.config);
46
58
  this.track._resolvedId = videoId;
59
+ this.metrics.metadata = Date.now() - startTimestamp;
47
60
  } catch (error) {
48
61
  log.error('STREAM', `Spotify resolution failed: ${error.message}`);
49
62
  throw new Error(`Failed to resolve Spotify track: ${error.message}`);
50
63
  }
64
+ } else {
65
+ this.metrics.metadata = Date.now() - startTimestamp;
51
66
  }
52
67
 
53
68
  if (!videoId || videoId === 'undefined') {
@@ -55,6 +70,17 @@ class StreamController {
55
70
  }
56
71
 
57
72
  log.info('STREAM', `Creating stream for ${videoId} (${source})`);
73
+
74
+ // Log Filter Chain
75
+ const filterNames = Object.keys(this.filters).filter(k => k !== 'start' && k !== '_trigger');
76
+ if (filterNames.length > 0) {
77
+ const chain = filterNames.map(name => {
78
+ const val = this.filters[name];
79
+ const displayVal = typeof val === 'object' ? JSON.stringify(val) : val;
80
+ return `[${name} (${displayVal})]`;
81
+ }).join(' -> ');
82
+ log.debug('STREAM', `Filter Chain: ${chain}`);
83
+ }
58
84
 
59
85
  let url;
60
86
  if (source === 'soundcloud') {
@@ -106,35 +132,45 @@ class StreamController {
106
132
  ytdlpArgs.push('--sponsorblock-remove', categories.join(','));
107
133
  }
108
134
 
109
- this.ytdlp = spawn(this.config.ytdlpPath, ytdlpArgs, {
110
- env: { ...process.env, PATH: '/usr/local/bin:/root/.deno/bin:' + process.env.PATH }
111
- });
135
+ const env = { ...process.env, PATH: '/usr/local/bin:/root/.deno/bin:' + process.env.PATH };
112
136
 
113
- this._spawnTime = Date.now() - this.startTime;
137
+ const spawnStart = Date.now();
138
+ this.ytdlp = spawn(this.config.ytdlpPath, ytdlpArgs, { env });
114
139
 
115
140
  const ffmpegFilters = { ...this.filters };
116
- if (seekPosition > 0) {
117
- ffmpegFilters.start = seekPosition / 1000;
118
- }
119
-
120
141
  const ffmpegArgs = buildFfmpegArgs(ffmpegFilters, this.config);
121
- this.ffmpeg = spawn(this.config.ffmpegPath, ffmpegArgs);
142
+ this.ffmpeg = spawn(this.config.ffmpegPath, ffmpegArgs, { env });
143
+
144
+ this.metrics.spawn = Date.now() - spawnStart;
145
+
146
+ const { pipeline } = require('stream');
122
147
 
123
148
  this._firstDataTime = null;
149
+
124
150
  this.ytdlp.stdout.on('data', (chunk) => {
151
+ this.bytesReceived += chunk.length;
152
+ });
153
+
154
+ this.ffmpeg.stdout.on('data', (chunk) => {
125
155
  if (!this._firstDataTime) {
126
156
  this._firstDataTime = Date.now() - this.startTime;
157
+ this.metrics.firstByte = Date.now() - (startTimestamp + this.metrics.metadata + this.metrics.spawn);
127
158
  }
128
- this.bytesReceived += chunk.length;
159
+ this.bytesSent += chunk.length;
129
160
  });
130
161
 
131
- this.ytdlp.stdout.pipe(this.ffmpeg.stdin);
162
+ pipeline(this.ytdlp.stdout, this.ffmpeg.stdin, (err) => {
163
+ if (err && !this.destroyed) {
164
+ if (err.code !== 'ERR_STREAM_PREMATURE_CLOSE' && err.code !== 'EPIPE') {
165
+ log.debug('STREAM', `Pipeline error: ${err.message}`);
166
+ }
167
+ }
168
+ });
132
169
 
133
- let ytdlpError = '';
134
170
  this.ytdlp.stderr.on('data', (data) => {
135
171
  if (this.destroyed) return;
136
172
  const msg = data.toString();
137
- ytdlpError += msg;
173
+ this.ytdlpError += msg;
138
174
  if (msg.includes('ERROR:') && !msg.includes('Retrying') && !msg.includes('Broken pipe')) {
139
175
  log.error('YTDLP', msg.trim());
140
176
  } else if (!msg.includes('[download]') && !msg.includes('ETA') && !msg.includes('[youtube]') && !msg.includes('Retrying fragment') && !msg.includes('Got error')) {
@@ -145,6 +181,7 @@ class StreamController {
145
181
  this.ffmpeg.stderr.on('data', (data) => {
146
182
  if (this.destroyed) return;
147
183
  const msg = data.toString();
184
+ this.ffmpegError += msg;
148
185
  if ((msg.includes('Error') || msg.includes('error')) && !msg.includes('Connection reset') && !msg.includes('Broken pipe')) {
149
186
  log.error('FFMPEG', msg.trim());
150
187
  }
@@ -153,11 +190,11 @@ class StreamController {
153
190
  this.ytdlp.on('close', (code) => {
154
191
  if (code !== 0 && code !== null && !this.destroyed) {
155
192
  log.error('STREAM', `yt-dlp failed (code: ${code}) for ${videoId}`);
156
- if (ytdlpError) {
157
- log.error('STREAM', `yt-dlp stderr: ${ytdlpError.slice(-500)}`);
193
+ if (this.ytdlpError) {
194
+ log.error('STREAM', `yt-dlp stderr: ${this.ytdlpError.slice(-500)}`);
158
195
  }
159
196
  }
160
- if (this.ffmpeg && !this.ffmpeg.killed) {
197
+ if (this.ffmpeg && !this.ffmpeg.killed && this.ffmpeg.stdin) {
161
198
  this.ffmpeg.stdin.end();
162
199
  }
163
200
  });
@@ -199,9 +236,10 @@ class StreamController {
199
236
  inlineVolume: false
200
237
  });
201
238
 
202
- const elapsed = Date.now() - this.startTime;
203
- const firstData = this._firstDataTime || elapsed;
204
- log.info('STREAM', `Ready ${elapsed}ms | First byte: ${firstData}ms | Buffered: ${this.bytesReceived}`);
239
+ const elapsed = Date.now() - startTimestamp;
240
+ this.metrics.total = elapsed;
241
+
242
+ log.info('STREAM', `Ready ${elapsed}ms | Metrics: [Metadata: ${this.metrics.metadata}ms | Spawn: ${this.metrics.spawn}ms | FirstByte: ${this.metrics.firstByte}ms] | Buffered: ${this.bytesSent}b`);
205
243
 
206
244
  return this.resource;
207
245
  }
@@ -210,7 +248,7 @@ class StreamController {
210
248
  return new Promise((resolve, reject) => {
211
249
  const timeoutMs = isLive ? 30000 : 15000;
212
250
  const timeout = setTimeout(() => {
213
- log.warn('STREAM', `Timeout waiting for data, proceeding anyway (received: ${this.bytesReceived}, isLive: ${isLive})`);
251
+ log.warn('STREAM', `Timeout waiting for data, proceeding anyway (received: ${this.bytesSent}, isLive: ${isLive})`);
214
252
  resolve();
215
253
  }, timeoutMs);
216
254
 
@@ -221,7 +259,7 @@ class StreamController {
221
259
  clearInterval(checkInterval);
222
260
  return;
223
261
  }
224
- if (this.bytesReceived > 0) {
262
+ if (this.bytesSent > 0) {
225
263
  resolved = true;
226
264
  clearTimeout(timeout);
227
265
  clearInterval(checkInterval);
@@ -234,7 +272,7 @@ class StreamController {
234
272
  resolved = true;
235
273
  clearTimeout(timeout);
236
274
  clearInterval(checkInterval);
237
- reject(new Error('ffmpeg closed before producing data'));
275
+ reject(new Error(`ffmpeg closed before producing data. ffmpeg stderr: ${this.ffmpegError.slice(-200) || 'none'} | yt-dlp stderr: ${this.ytdlpError.slice(-200) || 'none'}`));
238
276
  }
239
277
  });
240
278
 
@@ -243,7 +281,7 @@ class StreamController {
243
281
  resolved = true;
244
282
  clearTimeout(timeout);
245
283
  clearInterval(checkInterval);
246
- reject(new Error(`yt-dlp failed with code ${code}`));
284
+ reject(new Error(`yt-dlp failed with code ${code}. stderr: ${this.ytdlpError.slice(-200) || 'none'}`));
247
285
  }
248
286
  });
249
287
  });
@@ -254,7 +292,7 @@ class StreamController {
254
292
  this.destroyed = true;
255
293
 
256
294
  const elapsed = this.startTime ? Date.now() - this.startTime : 0;
257
- log.info('STREAM', `Destroying stream | Duration: ${elapsed}ms | Received: ${this.bytesReceived}`);
295
+ log.info('STREAM', `Destroying stream | Duration: ${elapsed}ms | Data Out: ${(this.bytesSent / 1024 / 1024).toFixed(2)} MB`);
258
296
 
259
297
  try {
260
298
  if (this.ytdlp && this.ffmpeg) {
@@ -287,4 +325,4 @@ function createStream(track, filters, config) {
287
325
  return new StreamController(track, filters, config);
288
326
  }
289
327
 
290
- module.exports = { createStream, StreamController };
328
+ module.exports = { createStream, StreamController };
@@ -15,27 +15,38 @@ const PRESETS = {
15
15
  piano: [0.2, 0.15, 0.1, 0.05, 0, 0.05, 0.1, 0.15, 0.2, 0.2, 0.15, 0.1, 0.1, 0.15, 0.2],
16
16
  vocal: [-0.2, -0.15, -0.1, 0, 0.2, 0.35, 0.4, 0.4, 0.35, 0.2, 0, -0.1, -0.15, -0.15, -0.1],
17
17
  bass_heavy: [0.5, 0.45, 0.4, 0.3, 0.2, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
18
- treble_heavy: [0, 0, 0, 0, 0, 0, 0, 0, 0.1, 0.2, 0.3, 0.4, 0.45, 0.5, 0.5]
18
+ treble_heavy: [0, 0, 0, 0, 0, 0, 0, 0, 0.1, 0.2, 0.3, 0.4, 0.45, 0.5, 0.5],
19
+ extra_bass: [0.6, 0.55, 0.5, 0.4, 0.3, 0.2, 0.1, 0, 0, 0, 0, 0, 0, 0, 0],
20
+ crystal_clear: [-0.1, -0.1, -0.05, 0, 0.1, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.5, 0.5, 0.5]
19
21
  };
20
22
 
21
23
  const EFFECT_PRESETS = {
22
- bassboost: { filters: { bass: 10 }, description: 'Boost bass frequencies' },
24
+ bassboost: { filters: { bass: 12, equalizer: [0.4, 0.3, 0.2, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, description: 'Heavy bass boost' },
23
25
  nightcore: { filters: { speed: 1.25, pitch: 1.25 }, description: 'Speed up with higher pitch' },
24
26
  vaporwave: { filters: { speed: 0.8, pitch: 0.8 }, description: 'Slow down with lower pitch' },
25
- '8d': { filters: { rotation: { speed: 0.2 } }, description: '8D rotating audio effect' },
27
+ '8d': { filters: { rotation: { speed: 0.15 } }, description: '8D rotating audio effect' },
26
28
  karaoke: { filters: { karaoke: true }, description: 'Reduce vocals' },
27
29
  trebleboost: { filters: { treble: 10 }, description: 'Boost treble frequencies' },
28
- deep: { filters: { bass: 15, pitch: 0.9 }, description: 'Deep bass with lower pitch' },
29
- lofi: { filters: { lowpass: 3000, bass: 5 }, description: 'Lo-fi aesthetic' },
30
- radio: { filters: { highpass: 300, lowpass: 5000 }, description: 'Radio/telephone effect' },
31
- telephone: { filters: { highpass: 500, lowpass: 3500 }, description: 'Old telephone effect' },
32
- soft: { filters: { bass: -5, treble: -3, volume: 70 }, description: 'Softer, quieter sound' },
33
- loud: { filters: { bass: 5, treble: 3, compressor: true }, description: 'Louder, compressed sound' },
34
- chipmunk: { filters: { pitch: 1.5 }, description: 'High-pitched chipmunk voice' },
35
- darth: { filters: { pitch: 0.7 }, description: 'Deep Darth Vader voice' },
30
+ deep: { filters: { bass: 15, pitch: 0.85, equalizer: [0.5, 0.4, 0.3, 0.2, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, description: 'Deep bass with lower pitch' },
31
+ lofi: { filters: { lowpass: 2500, bass: 8, treble: -5 }, description: 'Lo-fi aesthetic' },
32
+ radio: { filters: { highpass: 400, lowpass: 4500, compressor: true }, description: 'Radio/telephone effect' },
33
+ telephone: { filters: { highpass: 600, lowpass: 3000, compressor: true }, description: 'Old telephone effect' },
34
+ soft: { filters: { bass: -3, treble: -5, volume: 80, compressor: true }, description: 'Softer, compressed sound' },
35
+ loud: { filters: { bass: 3, treble: 2, volume: 120, compressor: true, normalizer: true }, description: 'Louder, punchier sound' },
36
+ chipmunk: { filters: { pitch: 1.6 }, description: 'High-pitched chipmunk voice' },
37
+ darth: { filters: { pitch: 0.65 }, description: 'Deep Darth Vader voice' },
36
38
  echo: { filters: { echo: true }, description: 'Echo/reverb effect' },
37
- vibrato: { filters: { vibrato: { frequency: 5, depth: 0.5 } }, description: 'Vibrato effect' },
38
- tremolo: { filters: { tremolo: { frequency: 5, depth: 0.5 } }, description: 'Tremolo effect' }
39
+ vibrato: { filters: { vibrato: { frequency: 6, depth: 0.6 } }, description: 'Vibrato effect' },
40
+ tremolo: { filters: { tremolo: { frequency: 5, depth: 0.5 } }, description: 'Tremolo effect' },
41
+ reverb: { filters: { reverb: true }, description: 'Classic reverb effect' },
42
+ surround: { filters: { surround: true }, description: 'Surround sound effect' },
43
+ boost: { filters: { volume: 150, treble: 5, bass: 5, compressor: true }, description: 'Boost volume and clarity' },
44
+ subboost: { filters: { bass: 15, lowpass: 100 }, description: 'Extreme sub-woofer boost' },
45
+ pop: { filters: { preset: 'pop' }, description: 'Pop EQ preset' },
46
+ rock: { filters: { preset: 'rock' }, description: 'Rock EQ preset' },
47
+ electronic: { filters: { preset: 'electronic' }, description: 'Electronic EQ preset' },
48
+ jazz: { filters: { preset: 'jazz' }, description: 'Jazz EQ preset' },
49
+ classical: { filters: { preset: 'classical' }, description: 'Classical EQ preset' }
39
50
  };
40
51
 
41
52
  function buildEqualizer(bands) {
@@ -84,11 +95,7 @@ function buildFfmpegArgs(filters = {}, config = {}) {
84
95
  const speed = parseFloat(filters.speed);
85
96
  if (!isNaN(speed) && speed !== 1) {
86
97
  const clampedSpeed = Math.max(0.5, Math.min(2.0, speed));
87
- if (clampedSpeed < 1) {
88
- audioFilters.push(`atempo=${clampedSpeed}`);
89
- } else if (clampedSpeed <= 2) {
90
- audioFilters.push(`atempo=${clampedSpeed}`);
91
- }
98
+ audioFilters.push(`atempo=${clampedSpeed}`);
92
99
  }
93
100
 
94
101
  const pitch = parseFloat(filters.pitch);
@@ -205,18 +212,24 @@ function buildFfmpegArgs(filters = {}, config = {}) {
205
212
  audioFilters.push('loudnorm');
206
213
  }
207
214
 
215
+ if (filters.echo === 'true' || filters.echo === true) {
216
+ audioFilters.push('aecho=0.8:0.88:60:0.4');
217
+ }
218
+
219
+ if (filters.reverb === 'true' || filters.reverb === true) {
220
+ audioFilters.push('aecho=0.8:0.88:60:0.4,aecho=0.8:0.88:30:0.2');
221
+ }
222
+
208
223
  if (filters.nightcore === 'true' || filters.nightcore === true) {
209
- audioFilters.push('atempo=1.25');
210
- audioFilters.push('asetrate=48000*1.25,aresample=48000');
224
+ audioFilters.push('atempo=1.25,asetrate=48000*1.25,aresample=48000');
211
225
  }
212
226
 
213
227
  if (filters.vaporwave === 'true' || filters.vaporwave === true) {
214
- audioFilters.push('atempo=0.8');
215
- audioFilters.push('asetrate=48000*0.8,aresample=48000');
228
+ audioFilters.push('atempo=0.8,asetrate=48000*0.8,aresample=48000');
216
229
  }
217
230
 
218
231
  if (filters.bassboost === 'true' || filters.bassboost === true) {
219
- audioFilters.push('bass=g=10');
232
+ audioFilters.push('bass=g=10,equalizer=f=40:width_type=h:width=20:g=10');
220
233
  }
221
234
 
222
235
  if (filters['8d'] === 'true' || filters['8d'] === true) {
@@ -275,6 +288,8 @@ function getAvailableFilters() {
275
288
  nightcore: { type: 'boolean', description: 'Nightcore preset' },
276
289
  vaporwave: { type: 'boolean', description: 'Vaporwave preset' },
277
290
  bassboost: { type: 'boolean', description: 'Bass boost preset' },
291
+ echo: { type: 'boolean', description: 'Echo effect' },
292
+ reverb: { type: 'boolean', description: 'Reverb effect' },
278
293
  '8d': { type: 'boolean', description: '8D audio effect' }
279
294
  };
280
295
  }
@@ -306,14 +321,24 @@ function applyEffectPreset(name, intensity = 1.0) {
306
321
  const filters = {};
307
322
  for (const [key, value] of Object.entries(preset.filters)) {
308
323
  if (typeof value === 'number') {
309
- filters[key] = value * intensity;
324
+ if (key === 'speed' || key === 'pitch') {
325
+ // Scale relative to 1.0
326
+ filters[key] = 1.0 + (value - 1.0) * intensity;
327
+ } else {
328
+ filters[key] = value * intensity;
329
+ }
310
330
  } else if (typeof value === 'boolean') {
311
331
  filters[key] = value;
312
332
  } else if (typeof value === 'object') {
313
333
  filters[key] = { ...value };
314
334
  for (const [k, v] of Object.entries(filters[key])) {
315
335
  if (typeof v === 'number') {
316
- filters[key][k] = v * intensity;
336
+ if (k === 'speed' || k === 'pitch' || k === 'frequency') {
337
+ // Some frequency based ones might also need base values but speed/pitch are most critical
338
+ filters[key][k] = 1.0 + (v - 1.0) * intensity;
339
+ } else {
340
+ filters[key][k] = v * intensity;
341
+ }
317
342
  }
318
343
  }
319
344
  }
@@ -332,4 +357,4 @@ module.exports = {
332
357
  PRESETS,
333
358
  EQ_BANDS,
334
359
  EFFECT_PRESETS
335
- };
360
+ };
@@ -3,23 +3,42 @@ const { buildFfmpegArgs } = require('../filters/ffmpeg');
3
3
  const { registerStream, unregisterStream } = require('../utils/stream');
4
4
  const log = require('../utils/logger');
5
5
 
6
- async function search(query, limit, config) {
6
+ async function search(query, limit, config, options = {}) {
7
7
  const startTime = Date.now();
8
- log.info('YOUTUBE', `Searching: "${query}" (limit: ${limit})`);
8
+ log.info('YOUTUBE', `Searching: "${query}" (limit: ${limit}, type: ${options.type || 'all'}, sort: ${options.sort || 'relevance'})`);
9
9
 
10
10
  return new Promise((resolve, reject) => {
11
11
  const args = [
12
12
  '-q', '--no-warnings',
13
13
  '--flat-playlist',
14
14
  '--skip-download',
15
- '-J',
16
- `ytsearch${limit}:${query}`
15
+ '-J'
17
16
  ];
18
17
 
19
18
  if (config.cookiesPath) {
20
19
  args.unshift('--cookies', config.cookiesPath);
21
20
  }
22
21
 
22
+ let searchQuery = query;
23
+
24
+ // Handle sorting by modifying search query or using filters
25
+ if (options.sort === 'views' || options.sort === 'popular') {
26
+ searchQuery += ' most viewed';
27
+ } else if (options.sort === 'date' || options.sort === 'latest') {
28
+ searchQuery += ' new';
29
+ }
30
+
31
+ // Handle type filtering
32
+ if (options.type === 'live') {
33
+ args.push('--match-filter', 'live_status = is_live');
34
+ } else if (options.type === 'playlist') {
35
+ args.push('--match-filter', 'playlist_count > 0');
36
+ } else if (options.type === 'video') {
37
+ args.push('--match-filter', 'live_status != is_live');
38
+ }
39
+
40
+ args.push(`ytsearch${limit}:${searchQuery}`);
41
+
23
42
  const proc = spawn(config.ytdlpPath, args, {
24
43
  env: { ...process.env, PATH: '/usr/local/bin:/root/.deno/bin:' + process.env.PATH }
25
44
  });
package/src/server.js CHANGED
@@ -93,14 +93,14 @@ class Server {
93
93
  if (!this._isProviderEnabled('youtube')) {
94
94
  return res.status(400).json({ error: 'YouTube provider is disabled' });
95
95
  }
96
- const { q, limit = 10 } = req.query;
96
+ const { q, limit = 10, type, sort } = req.query;
97
97
  if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
98
98
 
99
- const cacheKey = `yt:search:${q}:${limit}`;
99
+ const cacheKey = `yt:search:${q}:${limit}:${type}:${sort}`;
100
100
  const cached = cache.get(cacheKey);
101
101
  if (cached) return res.json(cached);
102
102
 
103
- const results = await youtube.search(q, parseInt(limit), this.config);
103
+ const results = await youtube.search(q, parseInt(limit), this.config, { type, sort });
104
104
  cache.set(cacheKey, results, this.config.cache.searchTTL);
105
105
  res.json(results);
106
106
  } catch (error) {
@@ -228,7 +228,7 @@ class Server {
228
228
 
229
229
  this.app.get('/search', async (req, res) => {
230
230
  try {
231
- const { q, source = 'youtube', limit = 10 } = req.query;
231
+ const { q, source = 'youtube', limit = 10, type, sort } = req.query;
232
232
  if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
233
233
 
234
234
  let results;
@@ -252,7 +252,7 @@ class Server {
252
252
  if (!this._isProviderEnabled('youtube')) {
253
253
  return res.status(400).json({ error: 'YouTube provider is disabled' });
254
254
  }
255
- results = await youtube.search(q, parseInt(limit), this.config);
255
+ results = await youtube.search(q, parseInt(limit), this.config, { type, sort });
256
256
  }
257
257
  res.json(results);
258
258
  } catch (error) {
@@ -52,45 +52,67 @@ function formatTime() {
52
52
  }
53
53
 
54
54
  function formatTag(tag, color) {
55
- return colorize(color, `[${tag}]`);
55
+ return colorize(color, `[${tag.toUpperCase()}]`);
56
56
  }
57
57
 
58
58
  function debug(tag, ...args) {
59
59
  if (currentLevel < LOG_LEVELS.debug) return;
60
- console.log(formatTime(), formatTag(tag, 'gray'), colorize('dim', args.join(' ')));
60
+ console.log(formatTime(), '🐛', formatTag(tag, 'gray'), colorize('dim', args.join(' ')));
61
61
  }
62
62
 
63
63
  function info(tag, ...args) {
64
64
  if (currentLevel < LOG_LEVELS.info) return;
65
- console.log(formatTime(), formatTag(tag, 'cyan'), ...args);
65
+ const tagUpper = tag.toUpperCase();
66
+ const color = {
67
+ 'MANAGER': 'cyan',
68
+ 'PLAYER': 'blue',
69
+ 'YOUTUBE': 'red',
70
+ 'SPOTIFY': 'green',
71
+ 'SOUNDCLOUD': 'yellow',
72
+ 'STREAM': 'cyan'
73
+ }[tagUpper] || 'cyan';
74
+
75
+ const emojiMap = {
76
+ 'MANAGER': 'â„šī¸ ',
77
+ 'YOUTUBE': '🔴 ',
78
+ 'SPOTIFY': 'đŸŸĸ ',
79
+ 'SOUNDCLOUD': '🟠 ',
80
+ 'PLAYER': '🔹 ',
81
+ 'STREAM': 'đŸŽĩ '
82
+ };
83
+
84
+ const emoji = emojiMap[tagUpper] || ' ';
85
+ console.log(formatTime(), emoji, formatTag(tag, color), ...args);
66
86
  }
67
87
 
68
88
  function success(tag, ...args) {
69
89
  if (currentLevel < LOG_LEVELS.info) return;
70
- console.log(formatTime(), formatTag(tag, 'green'), colorize('green', args.join(' ')));
90
+ console.log(formatTime(), '✅ ', formatTag(tag, 'green'), colorize('green', args.join(' ')));
71
91
  }
72
92
 
73
93
  function warn(tag, ...args) {
74
94
  if (currentLevel < LOG_LEVELS.warn) return;
75
- console.warn(formatTime(), formatTag(tag, 'yellow'), colorize('yellow', args.join(' ')));
95
+ console.warn(formatTime(), 'âš ī¸ ', formatTag(tag, 'yellow'), colorize('yellow', args.join(' ')));
76
96
  }
77
97
 
78
98
  function error(tag, ...args) {
79
99
  if (currentLevel < LOG_LEVELS.error) return;
80
- console.error(formatTime(), formatTag(tag, 'red'), colorize('red', args.join(' ')));
100
+ console.error(formatTime(), '❌ ', formatTag(tag, 'red'), colorize('red', args.join(' ')));
81
101
  }
82
102
 
83
103
  function stream(source, id, message) {
84
104
  if (currentLevel < LOG_LEVELS.debug) return;
85
- const sourceColor = {
86
- youtube: 'red',
87
- spotify: 'green',
88
- soundcloud: 'yellow'
89
- }[source] || 'white';
105
+ const sourceMap = {
106
+ youtube: { color: 'red', emoji: '🔴 ' },
107
+ spotify: { color: 'green', emoji: 'đŸŸĸ ' },
108
+ soundcloud: { color: 'yellow', emoji: '🟠 ' }
109
+ };
110
+ const { color, emoji } = sourceMap[source.toLowerCase()] || { color: 'white', emoji: 'đŸŽĩ ' };
90
111
 
91
112
  console.log(
92
113
  formatTime(),
93
- formatTag('STREAM', sourceColor),
114
+ emoji,
115
+ colorize('cyan', '[STREAM]'),
94
116
  colorize('dim', `[${id}]`),
95
117
  message
96
118
  );