streamify-audio 2.2.5 → 2.2.7

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.
@@ -0,0 +1,325 @@
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
+ });
@@ -0,0 +1,238 @@
1
+ const { describe, it, beforeEach } = require('node:test');
2
+ const assert = require('node:assert');
3
+
4
+ const spotify = require('../src/providers/spotify');
5
+
6
+ describe('Spotify Provider', () => {
7
+ const mockConfig = {
8
+ ytdlpPath: 'yt-dlp',
9
+ ffmpegPath: 'ffmpeg',
10
+ spotify: {
11
+ clientId: process.env.SPOTIFY_CLIENT_ID || null,
12
+ clientSecret: process.env.SPOTIFY_CLIENT_SECRET || null
13
+ },
14
+ cache: {
15
+ searchTTL: 300,
16
+ infoTTL: 600
17
+ }
18
+ };
19
+
20
+ const hasCredentials = mockConfig.spotify.clientId && mockConfig.spotify.clientSecret;
21
+
22
+ describe('search', () => {
23
+ it('should throw without credentials', async function() {
24
+ if (hasCredentials) this.skip();
25
+
26
+ const badConfig = { ...mockConfig, spotify: {} };
27
+ await assert.rejects(
28
+ () => spotify.search('test', 1, badConfig),
29
+ /credentials/i
30
+ );
31
+ });
32
+
33
+ it('should return tracks for valid query', async function() {
34
+ if (!hasCredentials) this.skip();
35
+
36
+ const results = await spotify.search('never gonna give you up', 1, mockConfig);
37
+
38
+ assert(results.tracks, 'Should have tracks');
39
+ assert(Array.isArray(results.tracks), 'Tracks should be array');
40
+ assert.strictEqual(results.source, 'spotify');
41
+ });
42
+
43
+ it('should respect limit', async function() {
44
+ if (!hasCredentials) this.skip();
45
+
46
+ const results = await spotify.search('music', 5, mockConfig);
47
+ assert(results.tracks.length <= 5, 'Should not exceed limit');
48
+ });
49
+
50
+ it('should return track with required fields', async function() {
51
+ if (!hasCredentials) this.skip();
52
+
53
+ const results = await spotify.search('test', 1, mockConfig);
54
+
55
+ if (results.tracks.length > 0) {
56
+ const track = results.tracks[0];
57
+ assert(track.id, 'Should have id');
58
+ assert(track.title, 'Should have title');
59
+ assert(track.author, 'Should have author');
60
+ assert(track.album, 'Should have album');
61
+ assert(typeof track.duration === 'number', 'Should have numeric duration');
62
+ assert(track.uri, 'Should have uri');
63
+ assert(track.streamUrl, 'Should have streamUrl');
64
+ assert.strictEqual(track.source, 'spotify');
65
+ }
66
+ });
67
+
68
+ it('should handle empty results', async function() {
69
+ if (!hasCredentials) this.skip();
70
+
71
+ const results = await spotify.search('xyznonexistenttrack12345', 1, mockConfig);
72
+ assert(Array.isArray(results.tracks), 'Should return empty array');
73
+ });
74
+
75
+ it('should handle special characters', async function() {
76
+ if (!hasCredentials) this.skip();
77
+
78
+ const results = await spotify.search('rock & roll "classics"', 1, mockConfig);
79
+ assert(results.tracks, 'Should handle special chars');
80
+ });
81
+ });
82
+
83
+ describe('getInfo', () => {
84
+ it('should throw without credentials', async function() {
85
+ if (hasCredentials) this.skip();
86
+
87
+ const badConfig = { ...mockConfig, spotify: {} };
88
+ await assert.rejects(
89
+ () => spotify.getInfo('4uLU6hMCjMI75M1A2tKUQC', badConfig),
90
+ /credentials/i
91
+ );
92
+ });
93
+
94
+ it('should return info for valid track ID', async function() {
95
+ if (!hasCredentials) this.skip();
96
+
97
+ const info = await spotify.getInfo('4uLU6hMCjMI75M1A2tKUQC', mockConfig);
98
+
99
+ assert(info.id, 'Should have id');
100
+ assert(info.title, 'Should have title');
101
+ assert(info.author, 'Should have author');
102
+ assert(info.album, 'Should have album');
103
+ assert.strictEqual(info.source, 'spotify');
104
+ });
105
+
106
+ it('should throw for invalid track ID', async function() {
107
+ if (!hasCredentials) this.skip();
108
+
109
+ await assert.rejects(
110
+ () => spotify.getInfo('invalidtrackid', mockConfig),
111
+ /Spotify API error/
112
+ );
113
+ });
114
+
115
+ it('should include duration in seconds', async function() {
116
+ if (!hasCredentials) this.skip();
117
+
118
+ const info = await spotify.getInfo('4uLU6hMCjMI75M1A2tKUQC', mockConfig);
119
+ assert(typeof info.duration === 'number', 'Duration should be number');
120
+ assert(info.duration > 0, 'Duration should be positive');
121
+ assert(info.duration < 3600, 'Duration should be in seconds not ms');
122
+ });
123
+ });
124
+
125
+ describe('resolveToYouTube', () => {
126
+ it('should resolve Spotify track to YouTube ID', async function() {
127
+ if (!hasCredentials) this.skip();
128
+
129
+ const youtubeId = await spotify.resolveToYouTube('4uLU6hMCjMI75M1A2tKUQC', mockConfig);
130
+
131
+ assert(youtubeId, 'Should return YouTube ID');
132
+ assert(typeof youtubeId === 'string', 'Should be string');
133
+ assert(youtubeId.length === 11, 'YouTube IDs are 11 characters');
134
+ });
135
+
136
+ it('should cache resolved IDs', async function() {
137
+ if (!hasCredentials) this.skip();
138
+
139
+ const trackId = '4uLU6hMCjMI75M1A2tKUQC';
140
+
141
+ const start1 = Date.now();
142
+ const id1 = await spotify.resolveToYouTube(trackId, mockConfig);
143
+ const time1 = Date.now() - start1;
144
+
145
+ const start2 = Date.now();
146
+ const id2 = await spotify.resolveToYouTube(trackId, mockConfig);
147
+ const time2 = Date.now() - start2;
148
+
149
+ assert.strictEqual(id1, id2, 'Should return same ID');
150
+ assert(time2 < time1 / 2, 'Cached lookup should be faster');
151
+ });
152
+
153
+ it('should throw for invalid track', async function() {
154
+ if (!hasCredentials) this.skip();
155
+
156
+ await assert.rejects(
157
+ () => spotify.resolveToYouTube('invalidtrack', mockConfig),
158
+ /Spotify API error/
159
+ );
160
+ });
161
+ });
162
+
163
+ describe('getPlaylist', () => {
164
+ it('should return playlist tracks', async function() {
165
+ if (!hasCredentials) this.skip();
166
+
167
+ const playlist = await spotify.getPlaylist('37i9dQZF1DXcBWIGoYBM5M', mockConfig);
168
+
169
+ assert(playlist.id, 'Should have id');
170
+ assert(playlist.title, 'Should have title');
171
+ assert(playlist.tracks, 'Should have tracks');
172
+ assert(Array.isArray(playlist.tracks), 'Tracks should be array');
173
+ assert.strictEqual(playlist.source, 'spotify');
174
+ });
175
+
176
+ it('should throw for invalid playlist', async function() {
177
+ if (!hasCredentials) this.skip();
178
+
179
+ await assert.rejects(
180
+ () => spotify.getPlaylist('invalidplaylist', mockConfig),
181
+ /Spotify API error/
182
+ );
183
+ });
184
+ });
185
+
186
+ describe('getAlbum', () => {
187
+ it('should return album tracks', async function() {
188
+ if (!hasCredentials) this.skip();
189
+
190
+ const album = await spotify.getAlbum('4aawyAB9vmqN3uQ7FjRGTy', mockConfig);
191
+
192
+ assert(album.id, 'Should have id');
193
+ assert(album.title, 'Should have title');
194
+ assert(album.author, 'Should have author');
195
+ assert(album.tracks, 'Should have tracks');
196
+ assert(Array.isArray(album.tracks), 'Tracks should be array');
197
+ assert.strictEqual(album.source, 'spotify');
198
+ });
199
+
200
+ it('should throw for invalid album', async function() {
201
+ if (!hasCredentials) this.skip();
202
+
203
+ await assert.rejects(
204
+ () => spotify.getAlbum('invalidalbum', mockConfig),
205
+ /Spotify API error/
206
+ );
207
+ });
208
+ });
209
+
210
+ describe('getRecommendations', () => {
211
+ it('should return recommended tracks', async function() {
212
+ if (!hasCredentials) this.skip();
213
+
214
+ const recs = await spotify.getRecommendations('4uLU6hMCjMI75M1A2tKUQC', 5, mockConfig);
215
+
216
+ assert(recs.tracks, 'Should have tracks');
217
+ assert(Array.isArray(recs.tracks), 'Tracks should be array');
218
+ assert.strictEqual(recs.source, 'spotify');
219
+ });
220
+
221
+ it('should mark tracks as autoplay', async function() {
222
+ if (!hasCredentials) this.skip();
223
+
224
+ const recs = await spotify.getRecommendations('4uLU6hMCjMI75M1A2tKUQC', 3, mockConfig);
225
+
226
+ if (recs.tracks.length > 0) {
227
+ assert.strictEqual(recs.tracks[0].isAutoplay, true);
228
+ }
229
+ });
230
+
231
+ it('should respect limit', async function() {
232
+ if (!hasCredentials) this.skip();
233
+
234
+ const recs = await spotify.getRecommendations('4uLU6hMCjMI75M1A2tKUQC', 3, mockConfig);
235
+ assert(recs.tracks.length <= 3, 'Should not exceed limit');
236
+ });
237
+ });
238
+ });