streamify-audio 2.3.1 → 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/package.json +1 -1
- 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
package/tests/local.test.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
const test = require('node:test');
|
|
2
|
-
const assert = require('node:assert');
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const local = require('../src/providers/local');
|
|
6
|
-
const { loadConfig } = require('../src/config');
|
|
7
|
-
|
|
8
|
-
test('local provider: getInfo', async (t) => {
|
|
9
|
-
const config = loadConfig();
|
|
10
|
-
const testFile = path.resolve(__dirname, 'test-audio.mp3');
|
|
11
|
-
|
|
12
|
-
// Create a dummy file for testing
|
|
13
|
-
fs.writeFileSync(testFile, 'dummy audio content');
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
await t.test('should return correct info for a local file', async () => {
|
|
17
|
-
const info = await local.getInfo(testFile, config);
|
|
18
|
-
|
|
19
|
-
assert.strictEqual(info.source, 'local');
|
|
20
|
-
assert.strictEqual(info.title, 'test-audio.mp3');
|
|
21
|
-
assert.ok(info.absolutePath.endsWith('tests/test-audio.mp3'));
|
|
22
|
-
assert.strictEqual(info.uri, `file://${info.absolutePath}`);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
await t.test('should throw error if file does not exist', async () => {
|
|
26
|
-
await assert.rejects(
|
|
27
|
-
local.getInfo('non-existent-file.mp3', config),
|
|
28
|
-
{ message: /File not found/ }
|
|
29
|
-
);
|
|
30
|
-
});
|
|
31
|
-
} finally {
|
|
32
|
-
// Cleanup
|
|
33
|
-
if (fs.existsSync(testFile)) {
|
|
34
|
-
fs.unlinkSync(testFile);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
package/tests/queue.test.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
const test = require('node:test');
|
|
2
|
-
const assert = require('node:assert');
|
|
3
|
-
const Queue = require('../src/discord/Queue');
|
|
4
|
-
|
|
5
|
-
test('Queue: management', async (t) => {
|
|
6
|
-
await t.test('should add tracks', () => {
|
|
7
|
-
const queue = new Queue();
|
|
8
|
-
queue.add({ id: '1', title: 'Track 1' });
|
|
9
|
-
queue.add({ id: '2', title: 'Track 2' });
|
|
10
|
-
assert.strictEqual(queue.size, 2);
|
|
11
|
-
assert.strictEqual(queue.tracks[0].id, '1');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
await t.test('should add many tracks', () => {
|
|
15
|
-
const queue = new Queue();
|
|
16
|
-
queue.addMany([{ id: '1' }, { id: '2' }]);
|
|
17
|
-
assert.strictEqual(queue.size, 2);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
await t.test('should remove tracks', () => {
|
|
21
|
-
const queue = new Queue();
|
|
22
|
-
queue.addMany([{ id: '1' }, { id: '2' }]);
|
|
23
|
-
const removed = queue.remove(0);
|
|
24
|
-
assert.strictEqual(removed.id, '1');
|
|
25
|
-
assert.strictEqual(queue.size, 1);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
await t.test('should move tracks', () => {
|
|
29
|
-
const queue = new Queue();
|
|
30
|
-
queue.addMany([{ id: '1' }, { id: '2' }, { id: '3' }]);
|
|
31
|
-
queue.move(2, 0); // Move '3' to front
|
|
32
|
-
assert.strictEqual(queue.tracks[0].id, '3');
|
|
33
|
-
assert.strictEqual(queue.tracks[1].id, '1');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
await t.test('should shift tracks and track history', () => {
|
|
37
|
-
const queue = new Queue({ maxPreviousTracks: 2 });
|
|
38
|
-
const t1 = { id: '1' };
|
|
39
|
-
const t2 = { id: '2' };
|
|
40
|
-
const t3 = { id: '3' };
|
|
41
|
-
|
|
42
|
-
queue.addMany([t1, t2, t3]);
|
|
43
|
-
|
|
44
|
-
assert.strictEqual(queue.shift().id, '1');
|
|
45
|
-
assert.strictEqual(queue.current.id, '1');
|
|
46
|
-
|
|
47
|
-
assert.strictEqual(queue.shift().id, '2');
|
|
48
|
-
assert.strictEqual(queue.previous[0].id, '1');
|
|
49
|
-
|
|
50
|
-
assert.strictEqual(queue.shift().id, '3');
|
|
51
|
-
assert.strictEqual(queue.previous[0].id, '2');
|
|
52
|
-
assert.strictEqual(queue.previous[1].id, '1');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
await t.test('should handle repeat track mode', () => {
|
|
56
|
-
const queue = new Queue();
|
|
57
|
-
const t1 = { id: '1' };
|
|
58
|
-
queue.add(t1);
|
|
59
|
-
queue.shift(); // current = t1
|
|
60
|
-
|
|
61
|
-
queue.setRepeatMode('track');
|
|
62
|
-
assert.strictEqual(queue.shift().id, '1');
|
|
63
|
-
assert.strictEqual(queue.shift().id, '1');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
await t.test('should handle repeat queue mode', () => {
|
|
67
|
-
const queue = new Queue();
|
|
68
|
-
const t1 = { id: '1' };
|
|
69
|
-
const t2 = { id: '2' };
|
|
70
|
-
queue.addMany([t1, t2]);
|
|
71
|
-
|
|
72
|
-
queue.setRepeatMode('queue');
|
|
73
|
-
queue.shift(); // current = t1
|
|
74
|
-
queue.shift(); // current = t2
|
|
75
|
-
|
|
76
|
-
assert.strictEqual(queue.shift().id, '1');
|
|
77
|
-
assert.strictEqual(queue.current.id, '1');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
await t.test('should unshift (previous)', () => {
|
|
81
|
-
const queue = new Queue();
|
|
82
|
-
const t1 = { id: '1' };
|
|
83
|
-
const t2 = { id: '2' };
|
|
84
|
-
queue.addMany([t1, t2]);
|
|
85
|
-
|
|
86
|
-
queue.shift(); // current = t1
|
|
87
|
-
queue.shift(); // current = t2
|
|
88
|
-
|
|
89
|
-
const prev = queue.unshift();
|
|
90
|
-
assert.strictEqual(prev.id, '1');
|
|
91
|
-
assert.strictEqual(queue.current.id, '1');
|
|
92
|
-
assert.strictEqual(queue.tracks[0].id, '2');
|
|
93
|
-
});
|
|
94
|
-
});
|
package/tests/spotify.test.js
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
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
|
-
});
|
package/tests/stream.test.js
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
const { describe, it, beforeEach, afterEach, mock } = require('node:test');
|
|
2
|
-
const assert = require('node:assert');
|
|
3
|
-
const { EventEmitter } = require('events');
|
|
4
|
-
|
|
5
|
-
const { createStream, StreamController } = require('../src/discord/Stream');
|
|
6
|
-
|
|
7
|
-
describe('StreamController', () => {
|
|
8
|
-
const mockConfig = {
|
|
9
|
-
ytdlpPath: 'yt-dlp',
|
|
10
|
-
ffmpegPath: 'ffmpeg',
|
|
11
|
-
cookiesPath: null,
|
|
12
|
-
ytdlp: {
|
|
13
|
-
format: 'bestaudio/best',
|
|
14
|
-
additionalArgs: []
|
|
15
|
-
},
|
|
16
|
-
audio: {
|
|
17
|
-
bitrate: '128k',
|
|
18
|
-
format: 'opus'
|
|
19
|
-
},
|
|
20
|
-
sponsorblock: {
|
|
21
|
-
enabled: false
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
describe('constructor', () => {
|
|
26
|
-
it('should initialize with default values', () => {
|
|
27
|
-
const track = { id: 'test123', title: 'Test Track', source: 'youtube' };
|
|
28
|
-
const stream = createStream(track, {}, mockConfig);
|
|
29
|
-
|
|
30
|
-
assert.strictEqual(stream.track, track);
|
|
31
|
-
assert.deepStrictEqual(stream.filters, {});
|
|
32
|
-
assert.strictEqual(stream.destroyed, false);
|
|
33
|
-
assert.strictEqual(stream.ytdlp, null);
|
|
34
|
-
assert.strictEqual(stream.ffmpeg, null);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should accept filters', () => {
|
|
38
|
-
const track = { id: 'test123', title: 'Test Track', source: 'youtube' };
|
|
39
|
-
const filters = { bass: 10, speed: 1.25 };
|
|
40
|
-
const stream = createStream(track, filters, mockConfig);
|
|
41
|
-
|
|
42
|
-
assert.deepStrictEqual(stream.filters, filters);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should handle null filters', () => {
|
|
46
|
-
const track = { id: 'test123', title: 'Test Track', source: 'youtube' };
|
|
47
|
-
const stream = createStream(track, null, mockConfig);
|
|
48
|
-
|
|
49
|
-
assert.deepStrictEqual(stream.filters, {});
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('destroy', () => {
|
|
54
|
-
it('should set destroyed flag', () => {
|
|
55
|
-
const track = { id: 'test123', title: 'Test Track', source: 'youtube' };
|
|
56
|
-
const stream = createStream(track, {}, mockConfig);
|
|
57
|
-
|
|
58
|
-
stream.destroy();
|
|
59
|
-
|
|
60
|
-
assert.strictEqual(stream.destroyed, true);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should be idempotent', () => {
|
|
64
|
-
const track = { id: 'test123', title: 'Test Track', source: 'youtube' };
|
|
65
|
-
const stream = createStream(track, {}, mockConfig);
|
|
66
|
-
|
|
67
|
-
stream.destroy();
|
|
68
|
-
stream.destroy();
|
|
69
|
-
stream.destroy();
|
|
70
|
-
|
|
71
|
-
assert.strictEqual(stream.destroyed, true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should nullify resources', () => {
|
|
75
|
-
const track = { id: 'test123', title: 'Test Track', source: 'youtube' };
|
|
76
|
-
const stream = createStream(track, {}, mockConfig);
|
|
77
|
-
|
|
78
|
-
stream.destroy();
|
|
79
|
-
|
|
80
|
-
assert.strictEqual(stream.ytdlp, null);
|
|
81
|
-
assert.strictEqual(stream.ffmpeg, null);
|
|
82
|
-
assert.strictEqual(stream.resource, null);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('create - edge cases', () => {
|
|
87
|
-
it('should throw if already destroyed', async () => {
|
|
88
|
-
const track = { id: 'test123', title: 'Test Track', source: 'youtube' };
|
|
89
|
-
const stream = createStream(track, {}, mockConfig);
|
|
90
|
-
|
|
91
|
-
stream.destroy();
|
|
92
|
-
|
|
93
|
-
await assert.rejects(
|
|
94
|
-
() => stream.create(),
|
|
95
|
-
{ message: 'Stream already destroyed' }
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should throw for invalid track ID', async () => {
|
|
100
|
-
const track = { id: undefined, title: 'Test Track', source: 'youtube' };
|
|
101
|
-
const stream = createStream(track, {}, mockConfig);
|
|
102
|
-
|
|
103
|
-
await assert.rejects(
|
|
104
|
-
() => stream.create(),
|
|
105
|
-
/Invalid track ID/
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should throw for null track ID', async () => {
|
|
110
|
-
const track = { id: null, title: 'Test Track', source: 'youtube' };
|
|
111
|
-
const stream = createStream(track, {}, mockConfig);
|
|
112
|
-
|
|
113
|
-
await assert.rejects(
|
|
114
|
-
() => stream.create(),
|
|
115
|
-
/Invalid track ID/
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should throw for "undefined" string track ID', async () => {
|
|
120
|
-
const track = { id: 'undefined', title: 'Test Track', source: 'youtube' };
|
|
121
|
-
const stream = createStream(track, {}, mockConfig);
|
|
122
|
-
|
|
123
|
-
await assert.rejects(
|
|
124
|
-
() => stream.create(),
|
|
125
|
-
/Invalid track ID/
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('source detection', () => {
|
|
131
|
-
it('should handle youtube source', () => {
|
|
132
|
-
const track = { id: 'dQw4w9WgXcQ', title: 'Test', source: 'youtube' };
|
|
133
|
-
const stream = createStream(track, {}, mockConfig);
|
|
134
|
-
assert.strictEqual(stream.track.source, 'youtube');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should handle spotify source', () => {
|
|
138
|
-
const track = { id: '4uLU6hMCjMI75M1A2tKUQC', title: 'Test', source: 'spotify' };
|
|
139
|
-
const stream = createStream(track, {}, mockConfig);
|
|
140
|
-
assert.strictEqual(stream.track.source, 'spotify');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should handle soundcloud source', () => {
|
|
144
|
-
const track = { id: '123456', title: 'Test', source: 'soundcloud', uri: 'https://soundcloud.com/test/track' };
|
|
145
|
-
const stream = createStream(track, {}, mockConfig);
|
|
146
|
-
assert.strictEqual(stream.track.source, 'soundcloud');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should default to youtube for unknown source', () => {
|
|
150
|
-
const track = { id: 'test123', title: 'Test' };
|
|
151
|
-
const stream = createStream(track, {}, mockConfig);
|
|
152
|
-
assert.strictEqual(stream.track.source || 'youtube', 'youtube');
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe('metrics tracking', () => {
|
|
157
|
-
it('should initialize metrics to zero', () => {
|
|
158
|
-
const track = { id: 'test123', title: 'Test', source: 'youtube' };
|
|
159
|
-
const stream = createStream(track, {}, mockConfig);
|
|
160
|
-
|
|
161
|
-
assert.deepStrictEqual(stream.metrics, {
|
|
162
|
-
metadata: 0,
|
|
163
|
-
spawn: 0,
|
|
164
|
-
firstByte: 0,
|
|
165
|
-
total: 0
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('resolved ID handling', () => {
|
|
171
|
-
it('should use _resolvedId if available', () => {
|
|
172
|
-
const track = {
|
|
173
|
-
id: 'spotify123',
|
|
174
|
-
title: 'Test',
|
|
175
|
-
source: 'spotify',
|
|
176
|
-
_resolvedId: 'youtube456'
|
|
177
|
-
};
|
|
178
|
-
const stream = createStream(track, {}, mockConfig);
|
|
179
|
-
assert.strictEqual(stream.track._resolvedId, 'youtube456');
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('live stream handling', () => {
|
|
184
|
-
it('should detect live stream from isLive flag', () => {
|
|
185
|
-
const track = { id: 'live123', title: 'Live Stream', source: 'youtube', isLive: true };
|
|
186
|
-
const stream = createStream(track, {}, mockConfig);
|
|
187
|
-
assert.strictEqual(stream.track.isLive, true);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should detect live stream from zero duration', () => {
|
|
191
|
-
const track = { id: 'live123', title: 'Live Stream', source: 'youtube', duration: 0 };
|
|
192
|
-
const stream = createStream(track, {}, mockConfig);
|
|
193
|
-
assert.strictEqual(stream.track.duration, 0);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe('createStream factory', () => {
|
|
199
|
-
const mockConfig = {
|
|
200
|
-
ytdlpPath: 'yt-dlp',
|
|
201
|
-
ffmpegPath: 'ffmpeg',
|
|
202
|
-
ytdlp: { format: 'bestaudio/best', additionalArgs: [] },
|
|
203
|
-
audio: { bitrate: '128k', format: 'opus' }
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
it('should return a StreamController instance', () => {
|
|
207
|
-
const track = { id: 'test', title: 'Test', source: 'youtube' };
|
|
208
|
-
const stream = createStream(track, {}, mockConfig);
|
|
209
|
-
|
|
210
|
-
assert(stream instanceof StreamController);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('should handle empty track object', () => {
|
|
214
|
-
const stream = createStream({}, {}, mockConfig);
|
|
215
|
-
assert(stream instanceof StreamController);
|
|
216
|
-
});
|
|
217
|
-
});
|
package/tests/twitch.test.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
const test = require('node:test');
|
|
2
|
-
const assert = require('node:assert');
|
|
3
|
-
const twitch = require('../src/providers/twitch');
|
|
4
|
-
const { loadConfig } = require('../src/config');
|
|
5
|
-
|
|
6
|
-
test('twitch provider: getInfo', async (t) => {
|
|
7
|
-
const config = loadConfig();
|
|
8
|
-
|
|
9
|
-
// We'll use a real URL but it might fail if yt-dlp has issues or network is restricted.
|
|
10
|
-
// However, it's better to test the real integration if possible.
|
|
11
|
-
const url = 'https://www.twitch.tv/monstercat';
|
|
12
|
-
|
|
13
|
-
await t.test('should return info for a twitch stream', async () => {
|
|
14
|
-
try {
|
|
15
|
-
const info = await twitch.getInfo(url, config);
|
|
16
|
-
|
|
17
|
-
assert.strictEqual(info.source, 'twitch');
|
|
18
|
-
assert.ok(info.title);
|
|
19
|
-
assert.ok(info.author);
|
|
20
|
-
assert.strictEqual(info.isLive, true);
|
|
21
|
-
} catch (error) {
|
|
22
|
-
// If it fails because channel is not live, that's expected for this test URL sometimes
|
|
23
|
-
if (error.message.includes('The channel is not currently live')) {
|
|
24
|
-
console.warn('Twitch test: channel is offline, skipping validation');
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
// If it fails due to network/yt-dlp issues, we skip it but log it
|
|
28
|
-
if (error.message.includes('yt-dlp exited with code') || error.message.includes('ERROR:')) {
|
|
29
|
-
console.warn('Twitch test failed (likely network/yt-dlp issue):', error.message);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
await t.test('should reject on invalid URL', async () => {
|
|
37
|
-
await assert.rejects(
|
|
38
|
-
twitch.getInfo('https://not-a-twitch-url.com/abc', config),
|
|
39
|
-
{ message: /ERROR:|yt-dlp exited with code/ }
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
});
|
package/tests/utils.test.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
const test = require('node:test');
|
|
2
|
-
const assert = require('node:assert');
|
|
3
|
-
const {
|
|
4
|
-
registerStream,
|
|
5
|
-
unregisterStream,
|
|
6
|
-
getActiveStreams,
|
|
7
|
-
getStreamPosition,
|
|
8
|
-
updateStreamFilters,
|
|
9
|
-
setEventEmitter
|
|
10
|
-
} = require('../src/utils/stream');
|
|
11
|
-
const EventEmitter = require('events');
|
|
12
|
-
|
|
13
|
-
test('Utils: Stream Management', async (t) => {
|
|
14
|
-
const emitter = new EventEmitter();
|
|
15
|
-
setEventEmitter(emitter);
|
|
16
|
-
|
|
17
|
-
await t.test('should register a stream and emit event', () => {
|
|
18
|
-
let eventEmitted = false;
|
|
19
|
-
emitter.once('streamStart', (data) => {
|
|
20
|
-
assert.strictEqual(data.id, 'test-stream');
|
|
21
|
-
eventEmitted = true;
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
registerStream('test-stream', { source: 'youtube', videoId: 'abc' });
|
|
25
|
-
|
|
26
|
-
const active = getActiveStreams();
|
|
27
|
-
assert.ok(active.has('test-stream'));
|
|
28
|
-
assert.ok(eventEmitted);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
await t.test('should update stream filters', () => {
|
|
32
|
-
updateStreamFilters('test-stream', { volume: 80 });
|
|
33
|
-
const stream = getActiveStreams().get('test-stream');
|
|
34
|
-
assert.strictEqual(stream.filters.volume, 80);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
await t.test('should get stream position', async () => {
|
|
38
|
-
const pos1 = getStreamPosition('test-stream');
|
|
39
|
-
assert.ok(typeof pos1 === 'number');
|
|
40
|
-
|
|
41
|
-
await new Promise(r => setTimeout(r, 100));
|
|
42
|
-
|
|
43
|
-
const pos2 = getStreamPosition('test-stream');
|
|
44
|
-
assert.ok(pos2 > pos1);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
await t.test('should unregister a stream and emit event', () => {
|
|
48
|
-
let eventEmitted = false;
|
|
49
|
-
emitter.once('streamEnd', (data) => {
|
|
50
|
-
assert.strictEqual(data.id, 'test-stream');
|
|
51
|
-
eventEmitted = true;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
unregisterStream('test-stream');
|
|
55
|
-
|
|
56
|
-
const active = getActiveStreams();
|
|
57
|
-
assert.strictEqual(active.has('test-stream'), false);
|
|
58
|
-
assert.ok(eventEmitted);
|
|
59
|
-
});
|
|
60
|
-
});
|