streamify-audio 2.3.0 → 2.3.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.
- package/index.d.ts +9 -0
- package/package.json +1 -1
- package/src/config.js +11 -0
- package/src/discord/Manager.js +1 -1
- package/src/discord/Stream.js +9 -12
- package/src/providers/spotify.js +2 -2
- package/docs/README.md +0 -31
- package/docs/automation.md +0 -186
- package/docs/configuration.md +0 -198
- package/docs/discord/events.md +0 -206
- package/docs/discord/manager.md +0 -195
- package/docs/discord/player.md +0 -263
- package/docs/discord/queue.md +0 -197
- package/docs/examples/advanced-bot.md +0 -391
- package/docs/examples/basic-bot.md +0 -182
- package/docs/examples/lavalink.md +0 -156
- package/docs/filters.md +0 -347
- package/docs/http/endpoints.md +0 -224
- package/docs/http/server.md +0 -174
- package/docs/plans/2026-02-22-stream-revamp-design.md +0 -88
- package/docs/plans/2026-02-22-stream-revamp-plan.md +0 -814
- package/docs/quick-start.md +0 -92
- package/docs/sources.md +0 -189
- package/docs/sponsorblock.md +0 -95
- package/tests/cache.test.js +0 -234
- package/tests/config.test.js +0 -44
- package/tests/error-handling.test.js +0 -318
- package/tests/ffmpeg.test.js +0 -66
- package/tests/filters-edge.test.js +0 -333
- package/tests/http.test.js +0 -24
- package/tests/integration.test.js +0 -325
- package/tests/local.test.js +0 -37
- package/tests/queue.test.js +0 -94
- package/tests/spotify.test.js +0 -238
- package/tests/stream.test.js +0 -217
- package/tests/twitch.test.js +0 -42
- package/tests/utils.test.js +0 -60
- package/tests/youtube.test.js +0 -219
- package/youtube-cookies.txt +0 -26
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
const { describe, it } = require('node:test');
|
|
2
|
-
const assert = require('node:assert');
|
|
3
|
-
|
|
4
|
-
const {
|
|
5
|
-
buildFfmpegArgs,
|
|
6
|
-
getAvailableFilters,
|
|
7
|
-
getPresets,
|
|
8
|
-
getEffectPresets,
|
|
9
|
-
applyEffectPreset,
|
|
10
|
-
PRESETS,
|
|
11
|
-
EFFECT_PRESETS
|
|
12
|
-
} = require('../src/filters/ffmpeg');
|
|
13
|
-
|
|
14
|
-
describe('FFmpeg Filters - Edge Cases', () => {
|
|
15
|
-
const mockConfig = {
|
|
16
|
-
audio: {
|
|
17
|
-
bitrate: '128k',
|
|
18
|
-
format: 'opus',
|
|
19
|
-
vbr: true,
|
|
20
|
-
compressionLevel: 10,
|
|
21
|
-
application: 'audio'
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
describe('buildFfmpegArgs boundary values', () => {
|
|
26
|
-
it('should clamp bass to -20', () => {
|
|
27
|
-
const args = buildFfmpegArgs({ bass: -50 }, mockConfig);
|
|
28
|
-
const afIndex = args.indexOf('-af');
|
|
29
|
-
const filterStr = args[afIndex + 1];
|
|
30
|
-
assert(filterStr.includes('bass=g=-20'), 'Should clamp to -20');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should clamp bass to 20', () => {
|
|
34
|
-
const args = buildFfmpegArgs({ bass: 100 }, mockConfig);
|
|
35
|
-
const afIndex = args.indexOf('-af');
|
|
36
|
-
const filterStr = args[afIndex + 1];
|
|
37
|
-
assert(filterStr.includes('bass=g=20'), 'Should clamp to 20');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should clamp treble to -20', () => {
|
|
41
|
-
const args = buildFfmpegArgs({ treble: -100 }, mockConfig);
|
|
42
|
-
const afIndex = args.indexOf('-af');
|
|
43
|
-
const filterStr = args[afIndex + 1];
|
|
44
|
-
assert(filterStr.includes('treble=g=-20'), 'Should clamp to -20');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should clamp treble to 20', () => {
|
|
48
|
-
const args = buildFfmpegArgs({ treble: 50 }, mockConfig);
|
|
49
|
-
const afIndex = args.indexOf('-af');
|
|
50
|
-
const filterStr = args[afIndex + 1];
|
|
51
|
-
assert(filterStr.includes('treble=g=20'), 'Should clamp to 20');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should clamp speed to 0.5', () => {
|
|
55
|
-
const args = buildFfmpegArgs({ speed: 0.1 }, mockConfig);
|
|
56
|
-
const afIndex = args.indexOf('-af');
|
|
57
|
-
const filterStr = args[afIndex + 1];
|
|
58
|
-
assert(filterStr.includes('atempo=0.5'), 'Should clamp to 0.5');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should clamp speed to 2.0', () => {
|
|
62
|
-
const args = buildFfmpegArgs({ speed: 5 }, mockConfig);
|
|
63
|
-
const afIndex = args.indexOf('-af');
|
|
64
|
-
const filterStr = args[afIndex + 1];
|
|
65
|
-
assert(filterStr.includes('atempo=2'), 'Should clamp to 2.0');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('should clamp pitch to 0.5', () => {
|
|
69
|
-
const args = buildFfmpegArgs({ pitch: 0.1 }, mockConfig);
|
|
70
|
-
const afIndex = args.indexOf('-af');
|
|
71
|
-
const filterStr = args[afIndex + 1];
|
|
72
|
-
assert(filterStr.includes('asetrate=48000*0.5'), 'Should clamp to 0.5');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should clamp pitch to 2.0', () => {
|
|
76
|
-
const args = buildFfmpegArgs({ pitch: 10 }, mockConfig);
|
|
77
|
-
const afIndex = args.indexOf('-af');
|
|
78
|
-
const filterStr = args[afIndex + 1];
|
|
79
|
-
assert(filterStr.includes('asetrate=48000*2'), 'Should clamp to 2.0');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should clamp volume to 0', () => {
|
|
83
|
-
const args = buildFfmpegArgs({ volume: -100 }, mockConfig);
|
|
84
|
-
const afIndex = args.indexOf('-af');
|
|
85
|
-
const filterStr = args[afIndex + 1];
|
|
86
|
-
assert(filterStr.includes('volume=0'), 'Should clamp to 0');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should clamp volume to 200', () => {
|
|
90
|
-
const args = buildFfmpegArgs({ volume: 500 }, mockConfig);
|
|
91
|
-
const afIndex = args.indexOf('-af');
|
|
92
|
-
const filterStr = args[afIndex + 1];
|
|
93
|
-
assert(filterStr.includes('volume=2'), 'Should clamp to 200% (2x)');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should clamp lowpass to 100 minimum', () => {
|
|
97
|
-
const args = buildFfmpegArgs({ lowpass: 10 }, mockConfig);
|
|
98
|
-
const afIndex = args.indexOf('-af');
|
|
99
|
-
const filterStr = args[afIndex + 1];
|
|
100
|
-
assert(filterStr.includes('lowpass=f=100'), 'Should clamp to 100');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should clamp lowpass to 20000 maximum', () => {
|
|
104
|
-
const args = buildFfmpegArgs({ lowpass: 50000 }, mockConfig);
|
|
105
|
-
const afIndex = args.indexOf('-af');
|
|
106
|
-
const filterStr = args[afIndex + 1];
|
|
107
|
-
assert(filterStr.includes('lowpass=f=20000'), 'Should clamp to 20000');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should clamp highpass to 20 minimum', () => {
|
|
111
|
-
const args = buildFfmpegArgs({ highpass: 5 }, mockConfig);
|
|
112
|
-
const afIndex = args.indexOf('-af');
|
|
113
|
-
const filterStr = args[afIndex + 1];
|
|
114
|
-
assert(filterStr.includes('highpass=f=20'), 'Should clamp to 20');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should clamp highpass to 10000 maximum', () => {
|
|
118
|
-
const args = buildFfmpegArgs({ highpass: 50000 }, mockConfig);
|
|
119
|
-
const afIndex = args.indexOf('-af');
|
|
120
|
-
const filterStr = args[afIndex + 1];
|
|
121
|
-
assert(filterStr.includes('highpass=f=10000'), 'Should clamp to 10000');
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('filter type coercion', () => {
|
|
126
|
-
it('should handle string numbers for bass', () => {
|
|
127
|
-
const args = buildFfmpegArgs({ bass: '10' }, mockConfig);
|
|
128
|
-
const afIndex = args.indexOf('-af');
|
|
129
|
-
assert(afIndex > -1, 'Should have -af flag');
|
|
130
|
-
assert(args[afIndex + 1].includes('bass=g=10'), 'Should parse string to number');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('should handle string numbers for volume', () => {
|
|
134
|
-
const args = buildFfmpegArgs({ volume: '150' }, mockConfig);
|
|
135
|
-
const afIndex = args.indexOf('-af');
|
|
136
|
-
assert(args[afIndex + 1].includes('volume=1.5'), 'Should parse string volume');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should handle string "true" for boolean filters', () => {
|
|
140
|
-
const args = buildFfmpegArgs({ karaoke: 'true' }, mockConfig);
|
|
141
|
-
const afIndex = args.indexOf('-af');
|
|
142
|
-
assert(args[afIndex + 1].includes('pan=stereo'), 'Should handle string true');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should ignore NaN values', () => {
|
|
146
|
-
const args = buildFfmpegArgs({ bass: 'notanumber' }, mockConfig);
|
|
147
|
-
const afIndex = args.indexOf('-af');
|
|
148
|
-
const filterStr = args[afIndex + 1];
|
|
149
|
-
assert(!filterStr.includes('bass=g=NaN'), 'Should not include NaN');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should ignore null values', () => {
|
|
153
|
-
const args = buildFfmpegArgs({ bass: null }, mockConfig);
|
|
154
|
-
const afIndex = args.indexOf('-af');
|
|
155
|
-
const filterStr = args[afIndex + 1];
|
|
156
|
-
assert(!filterStr.includes('bass'), 'Should skip null bass');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should ignore undefined values', () => {
|
|
160
|
-
const args = buildFfmpegArgs({ bass: undefined }, mockConfig);
|
|
161
|
-
const afIndex = args.indexOf('-af');
|
|
162
|
-
const filterStr = args[afIndex + 1];
|
|
163
|
-
assert(!filterStr.includes('bass'), 'Should skip undefined bass');
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('equalizer edge cases', () => {
|
|
168
|
-
it('should handle empty equalizer array', () => {
|
|
169
|
-
const args = buildFfmpegArgs({ equalizer: [] }, mockConfig);
|
|
170
|
-
assert(args, 'Should not crash');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('should handle partial equalizer array', () => {
|
|
174
|
-
const args = buildFfmpegArgs({ equalizer: [0.5, 0.3] }, mockConfig);
|
|
175
|
-
const afIndex = args.indexOf('-af');
|
|
176
|
-
assert(args[afIndex + 1].includes('equalizer'), 'Should apply partial eq');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should handle oversized equalizer array', () => {
|
|
180
|
-
const bigEq = Array(30).fill(0.5);
|
|
181
|
-
const args = buildFfmpegArgs({ equalizer: bigEq }, mockConfig);
|
|
182
|
-
assert(args, 'Should not crash with oversized array');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should clamp equalizer values to -0.25', () => {
|
|
186
|
-
const args = buildFfmpegArgs({ equalizer: [-1, -1, -1] }, mockConfig);
|
|
187
|
-
const afIndex = args.indexOf('-af');
|
|
188
|
-
const filterStr = args[afIndex + 1];
|
|
189
|
-
assert(filterStr.includes('g=-3'), 'Should clamp gain (12 * -0.25 = -3)');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should clamp equalizer values to 1.0', () => {
|
|
193
|
-
const args = buildFfmpegArgs({ equalizer: [2, 2, 2] }, mockConfig);
|
|
194
|
-
const afIndex = args.indexOf('-af');
|
|
195
|
-
const filterStr = args[afIndex + 1];
|
|
196
|
-
assert(filterStr.includes('g=12'), 'Should clamp gain (12 * 1.0 = 12)');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should skip zero equalizer values', () => {
|
|
200
|
-
const args = buildFfmpegArgs({ equalizer: [0, 0, 0, 0.5, 0] }, mockConfig);
|
|
201
|
-
const afIndex = args.indexOf('-af');
|
|
202
|
-
const filterStr = args[afIndex + 1];
|
|
203
|
-
const eqCount = (filterStr.match(/equalizer=/g) || []).length;
|
|
204
|
-
assert.strictEqual(eqCount, 1, 'Should only have one eq band');
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe('tremolo/vibrato edge cases', () => {
|
|
209
|
-
it('should clamp tremolo frequency', () => {
|
|
210
|
-
const args = buildFfmpegArgs({ tremolo: { frequency: 100, depth: 0.5 } }, mockConfig);
|
|
211
|
-
const afIndex = args.indexOf('-af');
|
|
212
|
-
assert(args[afIndex + 1].includes('tremolo=f=20'), 'Should clamp to 20');
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('should clamp tremolo depth', () => {
|
|
216
|
-
const args = buildFfmpegArgs({ tremolo: { frequency: 5, depth: 5 } }, mockConfig);
|
|
217
|
-
const afIndex = args.indexOf('-af');
|
|
218
|
-
assert(args[afIndex + 1].includes('d=1'), 'Should clamp to 1');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('should clamp vibrato frequency', () => {
|
|
222
|
-
const args = buildFfmpegArgs({ vibrato: { frequency: 100, depth: 0.5 } }, mockConfig);
|
|
223
|
-
const afIndex = args.indexOf('-af');
|
|
224
|
-
assert(args[afIndex + 1].includes('vibrato=f=14'), 'Should clamp to 14');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('should use defaults for missing tremolo values', () => {
|
|
228
|
-
const args = buildFfmpegArgs({ tremolo: {} }, mockConfig);
|
|
229
|
-
const afIndex = args.indexOf('-af');
|
|
230
|
-
assert(args[afIndex + 1].includes('tremolo=f=4:d=0.5'), 'Should use defaults');
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
describe('output format variations', () => {
|
|
235
|
-
it('should output opus by default', () => {
|
|
236
|
-
const args = buildFfmpegArgs({}, mockConfig);
|
|
237
|
-
assert(args.includes('libopus'), 'Should use libopus');
|
|
238
|
-
assert(args.includes('-f') && args[args.indexOf('-f') + 1] === 'ogg');
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('should output mp3 when configured', () => {
|
|
242
|
-
const mp3Config = { ...mockConfig, audio: { ...mockConfig.audio, format: 'mp3' } };
|
|
243
|
-
const args = buildFfmpegArgs({}, mp3Config);
|
|
244
|
-
assert(args.includes('libmp3lame'), 'Should use libmp3lame');
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('should output aac when configured', () => {
|
|
248
|
-
const aacConfig = { ...mockConfig, audio: { ...mockConfig.audio, format: 'aac' } };
|
|
249
|
-
const args = buildFfmpegArgs({}, aacConfig);
|
|
250
|
-
assert(args.includes('aac'), 'Should use aac');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should respect custom bitrate', () => {
|
|
254
|
-
const customConfig = { ...mockConfig, audio: { ...mockConfig.audio, bitrate: '320k' } };
|
|
255
|
-
const args = buildFfmpegArgs({}, customConfig);
|
|
256
|
-
assert(args.includes('320k'), 'Should use custom bitrate');
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
describe('multiple filters combined', () => {
|
|
261
|
-
it('should combine all filter types', () => {
|
|
262
|
-
const filters = {
|
|
263
|
-
bass: 5,
|
|
264
|
-
treble: 3,
|
|
265
|
-
speed: 1.2,
|
|
266
|
-
volume: 120,
|
|
267
|
-
karaoke: true,
|
|
268
|
-
echo: true,
|
|
269
|
-
compressor: true
|
|
270
|
-
};
|
|
271
|
-
const args = buildFfmpegArgs(filters, mockConfig);
|
|
272
|
-
const afIndex = args.indexOf('-af');
|
|
273
|
-
const filterStr = args[afIndex + 1];
|
|
274
|
-
|
|
275
|
-
assert(filterStr.includes('bass'), 'Should have bass');
|
|
276
|
-
assert(filterStr.includes('treble'), 'Should have treble');
|
|
277
|
-
assert(filterStr.includes('atempo'), 'Should have speed');
|
|
278
|
-
assert(filterStr.includes('volume'), 'Should have volume');
|
|
279
|
-
assert(filterStr.includes('pan=stereo'), 'Should have karaoke');
|
|
280
|
-
assert(filterStr.includes('aecho'), 'Should have echo');
|
|
281
|
-
assert(filterStr.includes('acompressor'), 'Should have compressor');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should maintain filter order', () => {
|
|
285
|
-
const args = buildFfmpegArgs({ bass: 5, treble: 5 }, mockConfig);
|
|
286
|
-
const afIndex = args.indexOf('-af');
|
|
287
|
-
const filterStr = args[afIndex + 1];
|
|
288
|
-
const bassPos = filterStr.indexOf('bass');
|
|
289
|
-
const treblePos = filterStr.indexOf('treble');
|
|
290
|
-
assert(bassPos < treblePos, 'Bass should come before treble');
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe('preset functions', () => {
|
|
295
|
-
it('should have all documented presets', () => {
|
|
296
|
-
const presets = getPresets();
|
|
297
|
-
const expected = ['flat', 'rock', 'pop', 'jazz', 'classical', 'electronic', 'hiphop', 'acoustic', 'rnb', 'latin', 'loudness', 'piano', 'vocal', 'bass_heavy', 'treble_heavy', 'extra_bass', 'crystal_clear'];
|
|
298
|
-
|
|
299
|
-
for (const name of expected) {
|
|
300
|
-
assert(presets[name], `Should have ${name} preset`);
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('should have all effect presets', () => {
|
|
305
|
-
const effects = getEffectPresets();
|
|
306
|
-
const expected = ['bassboost', 'nightcore', 'vaporwave', '8d', 'karaoke', 'trebleboost', 'deep', 'lofi', 'radio', 'telephone', 'soft', 'loud', 'chipmunk', 'darth', 'echo', 'vibrato', 'tremolo', 'reverb', 'surround', 'boost', 'subboost'];
|
|
307
|
-
|
|
308
|
-
for (const name of expected) {
|
|
309
|
-
assert(effects[name], `Should have ${name} effect`);
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('should apply effect preset with intensity', () => {
|
|
314
|
-
const result = applyEffectPreset('bassboost', 0.5);
|
|
315
|
-
assert(result, 'Should return filters');
|
|
316
|
-
assert(typeof result.bass === 'number', 'Should have bass');
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('should return null for unknown preset', () => {
|
|
320
|
-
const result = applyEffectPreset('nonexistent');
|
|
321
|
-
assert.strictEqual(result, null);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('should scale intensity correctly', () => {
|
|
325
|
-
const full = applyEffectPreset('bassboost', 1.0);
|
|
326
|
-
const half = applyEffectPreset('bassboost', 0.5);
|
|
327
|
-
|
|
328
|
-
if (full.bass && half.bass) {
|
|
329
|
-
assert(half.bass < full.bass, 'Half intensity should have less bass');
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
});
|
package/tests/http.test.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const test = require('node:test');
|
|
2
|
-
const assert = require('node:assert');
|
|
3
|
-
const httpProvider = require('../src/providers/http');
|
|
4
|
-
const { loadConfig } = require('../src/config');
|
|
5
|
-
|
|
6
|
-
test('http provider: getInfo', async (t) => {
|
|
7
|
-
const config = loadConfig();
|
|
8
|
-
const testUrl = 'https://example.com/music/track1.mp3?query=1';
|
|
9
|
-
|
|
10
|
-
await t.test('should return basic info for a direct URL', async () => {
|
|
11
|
-
const info = await httpProvider.getInfo(testUrl, config);
|
|
12
|
-
|
|
13
|
-
assert.strictEqual(info.source, 'http');
|
|
14
|
-
assert.strictEqual(info.title, 'track1.mp3');
|
|
15
|
-
assert.strictEqual(info.uri, testUrl);
|
|
16
|
-
assert.strictEqual(info.isLive, true);
|
|
17
|
-
assert.ok(info.id);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
await t.test('should handle URLs without clear filenames', async () => {
|
|
21
|
-
const info = await httpProvider.getInfo('https://example.com/stream', config);
|
|
22
|
-
assert.strictEqual(info.title, 'stream');
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
const { describe, it, before, after } = require('node:test');
|
|
2
|
-
const assert = require('node:assert');
|
|
3
|
-
|
|
4
|
-
const Streamify = require('../index');
|
|
5
|
-
|
|
6
|
-
describe('Streamify Integration', () => {
|
|
7
|
-
let streamify;
|
|
8
|
-
|
|
9
|
-
before(async () => {
|
|
10
|
-
streamify = new Streamify({
|
|
11
|
-
port: 3999,
|
|
12
|
-
host: '127.0.0.1'
|
|
13
|
-
});
|
|
14
|
-
await streamify.start();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
after(async () => {
|
|
18
|
-
if (streamify) {
|
|
19
|
-
await streamify.stop();
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('lifecycle', () => {
|
|
24
|
-
it('should start successfully', () => {
|
|
25
|
-
assert.strictEqual(streamify.running, true);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should have valid base URL', () => {
|
|
29
|
-
const url = streamify.getBaseUrl();
|
|
30
|
-
assert.strictEqual(url, 'http://127.0.0.1:3999');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should not start twice', async () => {
|
|
34
|
-
const result = await streamify.start();
|
|
35
|
-
assert.strictEqual(result, streamify);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe('detectSource', () => {
|
|
40
|
-
it('should detect YouTube URLs', () => {
|
|
41
|
-
const tests = [
|
|
42
|
-
{ input: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', expected: 'youtube' },
|
|
43
|
-
{ input: 'https://youtu.be/dQw4w9WgXcQ', expected: 'youtube' },
|
|
44
|
-
{ input: 'https://youtube.com/embed/dQw4w9WgXcQ', expected: 'youtube' }
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
for (const test of tests) {
|
|
48
|
-
const result = streamify.detectSource(test.input);
|
|
49
|
-
assert.strictEqual(result.source, test.expected, `Failed for ${test.input}`);
|
|
50
|
-
assert.strictEqual(result.id, 'dQw4w9WgXcQ');
|
|
51
|
-
assert.strictEqual(result.isUrl, true);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should detect raw YouTube IDs', () => {
|
|
56
|
-
const result = streamify.detectSource('dQw4w9WgXcQ');
|
|
57
|
-
assert.strictEqual(result.source, 'youtube');
|
|
58
|
-
assert.strictEqual(result.id, 'dQw4w9WgXcQ');
|
|
59
|
-
assert.strictEqual(result.isUrl, false);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should detect Spotify URLs', () => {
|
|
63
|
-
const tests = [
|
|
64
|
-
{ input: 'https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC', expected: 'spotify' },
|
|
65
|
-
{ input: 'spotify:track:4uLU6hMCjMI75M1A2tKUQC', expected: 'spotify' }
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
for (const test of tests) {
|
|
69
|
-
const result = streamify.detectSource(test.input);
|
|
70
|
-
assert.strictEqual(result.source, test.expected, `Failed for ${test.input}`);
|
|
71
|
-
assert.strictEqual(result.id, '4uLU6hMCjMI75M1A2tKUQC');
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should detect SoundCloud URLs', () => {
|
|
76
|
-
const result = streamify.detectSource('https://soundcloud.com/artist/track-name');
|
|
77
|
-
assert.strictEqual(result.source, 'soundcloud');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should return null for unknown sources', () => {
|
|
81
|
-
const result = streamify.detectSource('some random text');
|
|
82
|
-
assert.strictEqual(result.source, null);
|
|
83
|
-
assert.strictEqual(result.id, null);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should handle null input', () => {
|
|
87
|
-
const result = streamify.detectSource(null);
|
|
88
|
-
assert.strictEqual(result.source, null);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should handle undefined input', () => {
|
|
92
|
-
const result = streamify.detectSource(undefined);
|
|
93
|
-
assert.strictEqual(result.source, null);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should handle empty string', () => {
|
|
97
|
-
const result = streamify.detectSource('');
|
|
98
|
-
assert.strictEqual(result.source, null);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('getStreamUrl', () => {
|
|
103
|
-
it('should generate YouTube stream URL', () => {
|
|
104
|
-
const url = streamify.getStreamUrl('youtube', 'dQw4w9WgXcQ');
|
|
105
|
-
assert.strictEqual(url, 'http://127.0.0.1:3999/youtube/stream/dQw4w9WgXcQ');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should generate Spotify stream URL', () => {
|
|
109
|
-
const url = streamify.getStreamUrl('spotify', '4uLU6hMCjMI75M1A2tKUQC');
|
|
110
|
-
assert.strictEqual(url, 'http://127.0.0.1:3999/spotify/stream/4uLU6hMCjMI75M1A2tKUQC');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should generate SoundCloud stream URL', () => {
|
|
114
|
-
const url = streamify.getStreamUrl('soundcloud', '123456');
|
|
115
|
-
assert.strictEqual(url, 'http://127.0.0.1:3999/soundcloud/stream/123456');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should accept source aliases', () => {
|
|
119
|
-
const ytUrl = streamify.getStreamUrl('yt', 'test');
|
|
120
|
-
const spUrl = streamify.getStreamUrl('sp', 'test');
|
|
121
|
-
const scUrl = streamify.getStreamUrl('sc', 'test');
|
|
122
|
-
|
|
123
|
-
assert(ytUrl.includes('/youtube/'), 'Should alias yt to youtube');
|
|
124
|
-
assert(spUrl.includes('/spotify/'), 'Should alias sp to spotify');
|
|
125
|
-
assert(scUrl.includes('/soundcloud/'), 'Should alias sc to soundcloud');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should append filter parameters', () => {
|
|
129
|
-
const url = streamify.getStreamUrl('youtube', 'test', { bass: 10, speed: 1.25 });
|
|
130
|
-
assert(url.includes('bass=10'), 'Should include bass');
|
|
131
|
-
assert(url.includes('speed=1.25'), 'Should include speed');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should skip null/undefined filter values', () => {
|
|
135
|
-
const url = streamify.getStreamUrl('youtube', 'test', { bass: 10, treble: null, speed: undefined });
|
|
136
|
-
assert(url.includes('bass=10'), 'Should include bass');
|
|
137
|
-
assert(!url.includes('treble'), 'Should skip null');
|
|
138
|
-
assert(!url.includes('speed'), 'Should skip undefined');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should throw for unknown source', () => {
|
|
142
|
-
assert.throws(
|
|
143
|
-
() => streamify.getStreamUrl('unknown', 'test'),
|
|
144
|
-
/Unknown source/
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe('HTTP endpoints', () => {
|
|
150
|
-
it('should respond to health check', async () => {
|
|
151
|
-
const res = await fetch('http://127.0.0.1:3999/health');
|
|
152
|
-
const data = await res.json();
|
|
153
|
-
|
|
154
|
-
assert.strictEqual(res.status, 200);
|
|
155
|
-
assert.strictEqual(data.status, 'ok');
|
|
156
|
-
assert(typeof data.uptime === 'number');
|
|
157
|
-
assert(typeof data.activeStreams === 'number');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should respond to stats', async () => {
|
|
161
|
-
const res = await fetch('http://127.0.0.1:3999/stats');
|
|
162
|
-
const data = await res.json();
|
|
163
|
-
|
|
164
|
-
assert.strictEqual(res.status, 200);
|
|
165
|
-
assert(typeof data.uptime === 'number');
|
|
166
|
-
assert(data.memory);
|
|
167
|
-
assert(data.cache);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should respond to streams list', async () => {
|
|
171
|
-
const res = await fetch('http://127.0.0.1:3999/streams');
|
|
172
|
-
const data = await res.json();
|
|
173
|
-
|
|
174
|
-
assert.strictEqual(res.status, 200);
|
|
175
|
-
assert(Array.isArray(data.streams));
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('should return 404 for unknown stream ID', async () => {
|
|
179
|
-
const res = await fetch('http://127.0.0.1:3999/streams/nonexistent');
|
|
180
|
-
assert.strictEqual(res.status, 404);
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe('YouTube search endpoint', () => {
|
|
185
|
-
it('should search YouTube', async () => {
|
|
186
|
-
const res = await fetch('http://127.0.0.1:3999/youtube/search?q=test&limit=1');
|
|
187
|
-
const data = await res.json();
|
|
188
|
-
|
|
189
|
-
assert.strictEqual(res.status, 200);
|
|
190
|
-
assert(data.tracks);
|
|
191
|
-
assert(Array.isArray(data.tracks));
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should return 400 without query', async () => {
|
|
195
|
-
const res = await fetch('http://127.0.0.1:3999/youtube/search');
|
|
196
|
-
assert.strictEqual(res.status, 400);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe('YouTube info endpoint', () => {
|
|
201
|
-
it('should get video info', async () => {
|
|
202
|
-
const res = await fetch('http://127.0.0.1:3999/youtube/info/dQw4w9WgXcQ');
|
|
203
|
-
const data = await res.json();
|
|
204
|
-
|
|
205
|
-
assert.strictEqual(res.status, 200);
|
|
206
|
-
assert.strictEqual(data.id, 'dQw4w9WgXcQ');
|
|
207
|
-
assert(data.title);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('should return 500 for invalid video', async () => {
|
|
211
|
-
const res = await fetch('http://127.0.0.1:3999/youtube/info/invalidvideoid123');
|
|
212
|
-
assert.strictEqual(res.status, 500);
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe('generic search endpoint', () => {
|
|
217
|
-
it('should default to YouTube', async () => {
|
|
218
|
-
const res = await fetch('http://127.0.0.1:3999/search?q=test&limit=1');
|
|
219
|
-
const data = await res.json();
|
|
220
|
-
|
|
221
|
-
assert.strictEqual(res.status, 200);
|
|
222
|
-
assert(data.tracks);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('should accept source parameter', async () => {
|
|
226
|
-
const res = await fetch('http://127.0.0.1:3999/search?q=test&source=youtube&limit=1');
|
|
227
|
-
const data = await res.json();
|
|
228
|
-
|
|
229
|
-
assert.strictEqual(res.status, 200);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('should return 400 without query', async () => {
|
|
233
|
-
const res = await fetch('http://127.0.0.1:3999/search');
|
|
234
|
-
assert.strictEqual(res.status, 400);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe('active streams API', () => {
|
|
239
|
-
it('should return empty array initially', async () => {
|
|
240
|
-
const streams = await streamify.getActiveStreams();
|
|
241
|
-
assert(Array.isArray(streams.streams));
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe('resolve method', () => {
|
|
246
|
-
it('should resolve YouTube URL', async () => {
|
|
247
|
-
const result = await streamify.resolve('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
|
248
|
-
|
|
249
|
-
assert.strictEqual(result.source, 'youtube');
|
|
250
|
-
assert(result.tracks.length > 0);
|
|
251
|
-
assert.strictEqual(result.fromUrl, true);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('should resolve raw YouTube ID', async () => {
|
|
255
|
-
const result = await streamify.resolve('dQw4w9WgXcQ');
|
|
256
|
-
|
|
257
|
-
assert.strictEqual(result.source, 'youtube');
|
|
258
|
-
assert(result.tracks.length > 0);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('should search for plain text', async () => {
|
|
262
|
-
const result = await streamify.resolve('never gonna give you up', 1);
|
|
263
|
-
|
|
264
|
-
assert.strictEqual(result.source, 'youtube');
|
|
265
|
-
assert(result.tracks.length > 0);
|
|
266
|
-
assert.strictEqual(result.fromUrl, false);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('shorthand methods', () => {
|
|
271
|
-
it('should have youtube.search', async () => {
|
|
272
|
-
const result = await streamify.youtube.search('test', 1);
|
|
273
|
-
assert(result.tracks);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('should have youtube.getInfo', async () => {
|
|
277
|
-
const result = await streamify.youtube.getInfo('dQw4w9WgXcQ');
|
|
278
|
-
assert(result.id);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('should have youtube.getStreamUrl', () => {
|
|
282
|
-
const url = streamify.youtube.getStreamUrl('test');
|
|
283
|
-
assert(url.includes('/youtube/stream/'));
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
describe('error handling', () => {
|
|
288
|
-
it('should throw when not started', async () => {
|
|
289
|
-
const notStarted = new Streamify({ port: 3998 });
|
|
290
|
-
|
|
291
|
-
assert.throws(
|
|
292
|
-
() => notStarted.getBaseUrl(),
|
|
293
|
-
/not started/
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
await assert.rejects(
|
|
297
|
-
() => notStarted.search('youtube', 'test'),
|
|
298
|
-
/not started/
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
await assert.rejects(
|
|
302
|
-
() => notStarted.resolve('test'),
|
|
303
|
-
/not started/
|
|
304
|
-
);
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
describe('Streamify Not Started', () => {
|
|
310
|
-
it('should throw for getStreamUrl without start', () => {
|
|
311
|
-
const instance = new Streamify({ port: 3997 });
|
|
312
|
-
assert.throws(
|
|
313
|
-
() => instance.getStreamUrl('youtube', 'test'),
|
|
314
|
-
/not started/
|
|
315
|
-
);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('should throw for getInfo without start', async () => {
|
|
319
|
-
const instance = new Streamify({ port: 3996 });
|
|
320
|
-
await assert.rejects(
|
|
321
|
-
() => instance.getInfo('youtube', 'test'),
|
|
322
|
-
/not started/
|
|
323
|
-
);
|
|
324
|
-
});
|
|
325
|
-
});
|