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.
@@ -1,92 +0,0 @@
1
- # Quick Start
2
-
3
- ## Installation
4
-
5
- ```bash
6
- npm install streamify-audio @discordjs/voice @discordjs/opus
7
- ```
8
-
9
- ## Requirements
10
-
11
- - Node.js 18+
12
- - [yt-dlp](https://github.com/yt-dlp/yt-dlp) — `pip install yt-dlp`
13
- - [ffmpeg](https://ffmpeg.org/) — `apt install ffmpeg`
14
-
15
- ## Discord Bot (5 minutes)
16
-
17
- ```javascript
18
- const { Client, GatewayIntentBits } = require('discord.js');
19
- const Streamify = require('streamify-audio');
20
-
21
- const client = new Client({
22
- intents: [
23
- GatewayIntentBits.Guilds,
24
- GatewayIntentBits.GuildVoiceStates,
25
- GatewayIntentBits.GuildMessages,
26
- GatewayIntentBits.MessageContent
27
- ]
28
- });
29
-
30
- const manager = new Streamify.Manager(client, {
31
- ytdlpPath: '/usr/local/bin/yt-dlp',
32
- ffmpegPath: '/usr/bin/ffmpeg'
33
- });
34
-
35
- client.on('messageCreate', async (message) => {
36
- if (!message.content.startsWith('!')) return;
37
-
38
- const [cmd, ...args] = message.content.slice(1).split(' ');
39
- const query = args.join(' ');
40
-
41
- if (cmd === 'play') {
42
- const vc = message.member.voice.channel;
43
- if (!vc) return message.reply('Join a voice channel first!');
44
-
45
- const result = await manager.search(query);
46
- if (!result.tracks.length) return message.reply('No results found.');
47
-
48
- const player = await manager.create(message.guild.id, vc.id, message.channel.id);
49
-
50
- player.on('trackStart', (track) => {
51
- message.channel.send(`Now playing: **${track.title}**`);
52
- });
53
-
54
- await player.play(result.tracks[0]);
55
- }
56
-
57
- if (cmd === 'skip') {
58
- const player = manager.get(message.guild.id);
59
- if (player) await player.skip();
60
- }
61
-
62
- if (cmd === 'stop') {
63
- const player = manager.get(message.guild.id);
64
- if (player) player.destroy();
65
- }
66
- });
67
-
68
- client.login(process.env.TOKEN);
69
- ```
70
-
71
- ## HTTP Server (3 minutes)
72
-
73
- ```javascript
74
- const Streamify = require('streamify-audio');
75
-
76
- const streamify = new Streamify({ port: 8787 });
77
- await streamify.start();
78
-
79
- // Search
80
- const results = await streamify.youtube.search('never gonna give you up');
81
- console.log(results.tracks[0].title);
82
-
83
- // Get stream URL
84
- const url = streamify.youtube.getStreamUrl(results.tracks[0].id);
85
- // http://127.0.0.1:8787/youtube/stream/dQw4w9WgXcQ
86
- ```
87
-
88
- ## Next Steps
89
-
90
- - [Configuration](./configuration.md) — Add Spotify, cookies, filters
91
- - [Discord Player](./discord/manager.md) — Full player documentation
92
- - [HTTP Server](./http/server.md) — API endpoints
package/docs/sources.md DELETED
@@ -1,189 +0,0 @@
1
- # Sources
2
-
3
- Streamify supports YouTube, Spotify, SoundCloud, Twitch, Mixcloud, Bandcamp, Local Files, and Direct URLs.
4
-
5
- ## Supported Features
6
-
7
- | Source | Search | Track URL | Playlist | Album |
8
- |--------|:------:|:---------:|:--------:|:-----:|
9
- | YouTube | ✅ | ✅ | ✅ | — |
10
- | Spotify | ✅ | ✅ | ✅ | ✅ |
11
- | SoundCloud | ✅ | ✅ | ❌ | — |
12
- | Twitch | ✅ | ✅ | — | — |
13
- | Mixcloud | ✅ | ✅ | — | — |
14
- | Bandcamp | ✅ | ✅ | — | ✅ |
15
- | Direct URL | ✅ | ✅ | — | — |
16
- | Local File | ✅ | ✅ | — | — |
17
-
18
- ## YouTube
19
-
20
- YouTube is the primary source. All audio is streamed directly from YouTube.
21
-
22
- ```javascript
23
- // Search
24
- const result = await manager.search('never gonna give you up');
25
- const result = await manager.search(query, { source: 'youtube' });
26
-
27
- // Direct URL
28
- const result = await manager.resolve('https://youtube.com/watch?v=dQw4w9WgXcQ');
29
- const result = await manager.resolve('https://youtu.be/dQw4w9WgXcQ');
30
- const result = await manager.resolve('https://music.youtube.com/watch?v=dQw4w9WgXcQ');
31
-
32
- // Playlist
33
- const result = await manager.loadPlaylist('https://youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf');
34
- ```
35
-
36
- ### Supported URL Formats
37
-
38
- - `youtube.com/watch?v=VIDEO_ID`
39
- - `youtu.be/VIDEO_ID`
40
- - `youtube.com/shorts/VIDEO_ID`
41
- - `music.youtube.com/watch?v=VIDEO_ID`
42
- - `youtube.com/playlist?list=PLAYLIST_ID`
43
-
44
- ### Cookies
45
-
46
- For age-restricted or region-locked videos, provide YouTube cookies:
47
-
48
- ```javascript
49
- const manager = new Streamify.Manager(client, {
50
- cookiesPath: './cookies.txt'
51
- });
52
- ```
53
-
54
- See [Configuration](./configuration.md#youtube-cookies) for setup instructions.
55
-
56
- ## Spotify
57
-
58
- Spotify tracks are resolved to YouTube for playback. Requires API credentials.
59
-
60
- ```javascript
61
- const manager = new Streamify.Manager(client, {
62
- spotify: {
63
- clientId: process.env.SPOTIFY_CLIENT_ID,
64
- clientSecret: process.env.SPOTIFY_CLIENT_SECRET
65
- }
66
- });
67
- ```
68
-
69
- ```javascript
70
- // Search
71
- const result = await manager.search('never gonna give you up', { source: 'spotify' });
72
-
73
- // Direct URL
74
- const result = await manager.resolve('https://open.spotify.com/track/4PTG3Z6ehGkBFwjybzWkR8');
75
-
76
- // Playlist
77
- const result = await manager.loadPlaylist('https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M');
78
-
79
- // Album
80
- const result = await manager.loadPlaylist('https://open.spotify.com/album/4LH4d3cOWNNsVw41Gqt2kv');
81
- ```
82
-
83
- ### Supported URL Formats
84
-
85
- - `open.spotify.com/track/TRACK_ID`
86
- - `open.spotify.com/playlist/PLAYLIST_ID`
87
- - `open.spotify.com/album/ALBUM_ID`
88
- - `spotify:track:TRACK_ID`
89
-
90
- ### How Resolution Works
91
-
92
- 1. Fetch track metadata from Spotify API
93
- 2. Search YouTube for `{artist} - {title}`
94
- 3. Use the first result for playback
95
-
96
- The resolved YouTube ID is cached for 5 minutes.
97
-
98
- ## SoundCloud
99
-
100
- SoundCloud tracks are streamed via yt-dlp.
101
-
102
- ```javascript
103
- // Search
104
- const result = await manager.search('never gonna give you up', { source: 'soundcloud' });
105
-
106
- // Direct URL
107
- const result = await manager.resolve('https://soundcloud.com/rick-astley-official/never-gonna-give-you-up');
108
- ```
109
-
110
- ### Supported URL Formats
111
-
112
- - `soundcloud.com/USER/TRACK`
113
-
114
- ## Twitch & Mixcloud
115
-
116
- Streaming support for live content and DJ sets.
117
-
118
- ```javascript
119
- // Twitch Live
120
- await manager.resolve('https://twitch.tv/monstercat');
121
-
122
- // Mixcloud Sets
123
- await manager.resolve('https://www.mixcloud.com/spinninrecords/spinnin-sessions-550/');
124
- ```
125
-
126
- ## Bandcamp
127
-
128
- Support for high-quality independent music.
129
-
130
- ```javascript
131
- await manager.resolve('https://monstercatmedia.bandcamp.com/track/the-governor');
132
- ```
133
-
134
- ## Direct URLs (HTTP)
135
-
136
- Play raw audio files from any public URL.
137
-
138
- ```javascript
139
- await manager.resolve('https://example.com/audio.mp3');
140
- await manager.resolve('https://cdn.discordapp.com/attachments/.../music.ogg');
141
- ```
142
-
143
- ## Local Files
144
-
145
- Play files directly from the host system.
146
-
147
- ```javascript
148
- // Absolute path
149
- await manager.resolve('/home/user/music/track.mp3');
150
-
151
- // Relative path
152
- await manager.resolve('./assets/sound-effect.wav');
153
-
154
- // File URI
155
- await manager.resolve('file:///var/lib/music/song.flac');
156
- ```
157
-
158
- ## Auto-Detection
159
-
160
- The `resolve()` method auto-detects the source from URLs:
161
-
162
- ```javascript
163
- // All of these work
164
- await manager.resolve('https://youtube.com/watch?v=dQw4w9WgXcQ');
165
- await manager.resolve('https://open.spotify.com/track/4PTG3Z6ehGkBFwjybzWkR8');
166
- await manager.resolve('https://soundcloud.com/rick-astley-official/never-gonna-give-you-up');
167
- await manager.resolve('https://twitch.tv/some-channel');
168
- await manager.resolve('/path/to/local/file.mp3');
169
- ```
170
-
171
- ## Track Object
172
-
173
- All sources return the same track structure:
174
-
175
- ```javascript
176
- {
177
- id: 'dQw4w9WgXcQ',
178
- title: 'Rick Astley - Never Gonna Give You Up',
179
- author: 'Rick Astley',
180
- duration: 213, // seconds
181
- thumbnail: 'https://...',
182
- uri: 'https://...', // Original URL
183
- source: 'youtube', // youtube, spotify, soundcloud
184
-
185
- // Spotify only
186
- album: 'Whenever You Need Somebody',
187
- _resolvedId: 'dQw4w9WgXcQ' // YouTube ID
188
- }
189
- ```
@@ -1,95 +0,0 @@
1
- # Sponsorblock
2
-
3
- Streamify integrates with [SponsorBlock](https://sponsor.ajay.app/) to automatically skip sponsored segments in YouTube videos.
4
-
5
- ## Configuration
6
-
7
- ```javascript
8
- const manager = new Streamify.Manager(client, {
9
- sponsorblock: {
10
- enabled: true,
11
- categories: ['sponsor', 'selfpromo', 'intro', 'outro']
12
- }
13
- });
14
- ```
15
-
16
- ## Categories
17
-
18
- | Category | Description |
19
- |----------|-------------|
20
- | `sponsor` | Paid promotion, sponsored content |
21
- | `selfpromo` | Self-promotion, merchandise, Patreon |
22
- | `intro` | Intro animation, opening sequence |
23
- | `outro` | Outro, end cards, credits |
24
- | `preview` | Preview of other videos |
25
- | `filler` | Tangents, jokes, off-topic |
26
- | `interaction` | Subscribe reminders, like requests |
27
- | `music_offtopic` | Non-music sections in music videos |
28
-
29
- ## Default Categories
30
-
31
- By default, Streamify skips:
32
- - `sponsor`
33
- - `selfpromo`
34
-
35
- ```javascript
36
- // Default behavior
37
- sponsorblock: {
38
- enabled: true,
39
- categories: ['sponsor', 'selfpromo']
40
- }
41
- ```
42
-
43
- ## Skip Everything
44
-
45
- ```javascript
46
- sponsorblock: {
47
- enabled: true,
48
- categories: [
49
- 'sponsor',
50
- 'selfpromo',
51
- 'intro',
52
- 'outro',
53
- 'preview',
54
- 'filler',
55
- 'interaction',
56
- 'music_offtopic'
57
- ]
58
- }
59
- ```
60
-
61
- ## Music Videos
62
-
63
- For music, you might want:
64
-
65
- ```javascript
66
- sponsorblock: {
67
- enabled: true,
68
- categories: ['sponsor', 'selfpromo', 'intro', 'outro', 'music_offtopic']
69
- }
70
- ```
71
-
72
- ## Disabling
73
-
74
- ```javascript
75
- sponsorblock: {
76
- enabled: false
77
- }
78
- ```
79
-
80
- Or omit the `sponsorblock` config entirely.
81
-
82
- ## How It Works
83
-
84
- 1. When a YouTube stream starts, yt-dlp queries the SponsorBlock API
85
- 2. Segment timestamps are fetched for the video
86
- 3. yt-dlp removes those segments during download
87
- 4. Audio plays seamlessly without the sponsored content
88
-
89
- ## Notes
90
-
91
- - Only works with YouTube (Spotify/SoundCloud unaffected)
92
- - Segments are crowdsourced and may not exist for all videos
93
- - Popular videos have more complete segment data
94
- - Occasionally segments may be slightly inaccurate
95
- - No additional latency - segments are fetched during stream initialization
@@ -1,234 +0,0 @@
1
- const { describe, it, beforeEach, afterEach } = require('node:test');
2
- const assert = require('node:assert');
3
-
4
- const cache = require('../src/cache');
5
-
6
- describe('Cache', () => {
7
- beforeEach(() => {
8
- cache.clear();
9
- });
10
-
11
- afterEach(() => {
12
- cache.clear();
13
- });
14
-
15
- describe('set and get', () => {
16
- it('should store and retrieve values', () => {
17
- cache.set('key1', 'value1');
18
- assert.strictEqual(cache.get('key1'), 'value1');
19
- });
20
-
21
- it('should store complex objects', () => {
22
- const obj = { id: 1, data: { nested: true }, arr: [1, 2, 3] };
23
- cache.set('obj', obj);
24
- assert.deepStrictEqual(cache.get('obj'), obj);
25
- });
26
-
27
- it('should store arrays', () => {
28
- const arr = [1, 2, 3, { a: 1 }];
29
- cache.set('arr', arr);
30
- assert.deepStrictEqual(cache.get('arr'), arr);
31
- });
32
-
33
- it('should store null values', () => {
34
- cache.set('null', null);
35
- assert.strictEqual(cache.get('null'), null);
36
- });
37
-
38
- it('should store zero', () => {
39
- cache.set('zero', 0);
40
- assert.strictEqual(cache.get('zero'), 0);
41
- });
42
-
43
- it('should store empty string', () => {
44
- cache.set('empty', '');
45
- assert.strictEqual(cache.get('empty'), '');
46
- });
47
-
48
- it('should store false', () => {
49
- cache.set('false', false);
50
- assert.strictEqual(cache.get('false'), false);
51
- });
52
-
53
- it('should overwrite existing values', () => {
54
- cache.set('key', 'value1');
55
- cache.set('key', 'value2');
56
- assert.strictEqual(cache.get('key'), 'value2');
57
- });
58
-
59
- it('should return null for non-existent keys', () => {
60
- assert.strictEqual(cache.get('nonexistent'), null);
61
- });
62
- });
63
-
64
- describe('TTL expiration', () => {
65
- it('should expire after TTL', async () => {
66
- cache.set('expiring', 'value', 0.1);
67
- assert.strictEqual(cache.get('expiring'), 'value');
68
-
69
- await new Promise(r => setTimeout(r, 150));
70
-
71
- assert.strictEqual(cache.get('expiring'), null);
72
- });
73
-
74
- it('should use default TTL of 300 seconds', () => {
75
- cache.set('default', 'value');
76
- const stats = cache.stats();
77
- assert.strictEqual(stats.valid, 1);
78
- });
79
-
80
- it('should handle zero TTL', async () => {
81
- cache.set('zero-ttl', 'value', 0);
82
-
83
- await new Promise(r => setTimeout(r, 10));
84
-
85
- assert.strictEqual(cache.get('zero-ttl'), null);
86
- });
87
-
88
- it('should handle negative TTL as immediate expiration', async () => {
89
- cache.set('negative-ttl', 'value', -1);
90
-
91
- await new Promise(r => setTimeout(r, 10));
92
-
93
- assert.strictEqual(cache.get('negative-ttl'), null);
94
- });
95
- });
96
-
97
- describe('del', () => {
98
- it('should delete existing keys', () => {
99
- cache.set('key', 'value');
100
- cache.del('key');
101
- assert.strictEqual(cache.get('key'), null);
102
- });
103
-
104
- it('should not throw for non-existent keys', () => {
105
- assert.doesNotThrow(() => cache.del('nonexistent'));
106
- });
107
- });
108
-
109
- describe('clear', () => {
110
- it('should remove all entries', () => {
111
- cache.set('key1', 'value1');
112
- cache.set('key2', 'value2');
113
- cache.set('key3', 'value3');
114
-
115
- cache.clear();
116
-
117
- assert.strictEqual(cache.get('key1'), null);
118
- assert.strictEqual(cache.get('key2'), null);
119
- assert.strictEqual(cache.get('key3'), null);
120
- });
121
-
122
- it('should reset stats', () => {
123
- cache.set('key1', 'value1');
124
- cache.clear();
125
-
126
- const stats = cache.stats();
127
- assert.strictEqual(stats.entries, 0);
128
- });
129
- });
130
-
131
- describe('stats', () => {
132
- it('should count valid entries', () => {
133
- cache.set('key1', 'value1');
134
- cache.set('key2', 'value2');
135
-
136
- const stats = cache.stats();
137
- assert.strictEqual(stats.entries, 2);
138
- assert.strictEqual(stats.valid, 2);
139
- assert.strictEqual(stats.expired, 0);
140
- });
141
-
142
- it('should count expired entries', async () => {
143
- cache.set('expiring', 'value', 0.05);
144
- cache.set('valid', 'value', 300);
145
-
146
- await new Promise(r => setTimeout(r, 100));
147
-
148
- const stats = cache.stats();
149
- assert.strictEqual(stats.entries, 2);
150
- assert.strictEqual(stats.valid, 1);
151
- assert.strictEqual(stats.expired, 1);
152
- });
153
-
154
- it('should return zero for empty cache', () => {
155
- const stats = cache.stats();
156
- assert.strictEqual(stats.entries, 0);
157
- assert.strictEqual(stats.valid, 0);
158
- assert.strictEqual(stats.expired, 0);
159
- });
160
- });
161
-
162
- describe('edge cases', () => {
163
- it('should handle special characters in keys', () => {
164
- cache.set('key:with:colons', 'value');
165
- cache.set('key/with/slashes', 'value');
166
- cache.set('key with spaces', 'value');
167
- cache.set('key\nwith\nnewlines', 'value');
168
-
169
- assert.strictEqual(cache.get('key:with:colons'), 'value');
170
- assert.strictEqual(cache.get('key/with/slashes'), 'value');
171
- assert.strictEqual(cache.get('key with spaces'), 'value');
172
- assert.strictEqual(cache.get('key\nwith\nnewlines'), 'value');
173
- });
174
-
175
- it('should handle unicode keys', () => {
176
- cache.set('日本語キー', 'value');
177
- cache.set('emoji🎵key', 'value');
178
-
179
- assert.strictEqual(cache.get('日本語キー'), 'value');
180
- assert.strictEqual(cache.get('emoji🎵key'), 'value');
181
- });
182
-
183
- it('should handle very long keys', () => {
184
- const longKey = 'a'.repeat(10000);
185
- cache.set(longKey, 'value');
186
- assert.strictEqual(cache.get(longKey), 'value');
187
- });
188
-
189
- it('should handle very large values', () => {
190
- const largeValue = { data: 'x'.repeat(100000) };
191
- cache.set('large', largeValue);
192
- assert.deepStrictEqual(cache.get('large'), largeValue);
193
- });
194
-
195
- it('should handle undefined key gracefully', () => {
196
- assert.strictEqual(cache.get(undefined), null);
197
- });
198
-
199
- it('should handle null key gracefully', () => {
200
- assert.strictEqual(cache.get(null), null);
201
- });
202
- });
203
-
204
- describe('concurrency simulation', () => {
205
- it('should handle rapid set/get operations', () => {
206
- for (let i = 0; i < 1000; i++) {
207
- cache.set(`key${i}`, `value${i}`);
208
- }
209
-
210
- for (let i = 0; i < 1000; i++) {
211
- assert.strictEqual(cache.get(`key${i}`), `value${i}`);
212
- }
213
-
214
- const stats = cache.stats();
215
- assert.strictEqual(stats.entries, 1000);
216
- });
217
-
218
- it('should handle interleaved set/get/del', () => {
219
- for (let i = 0; i < 100; i++) {
220
- cache.set(`key${i}`, `value${i}`);
221
- if (i % 2 === 0) {
222
- cache.del(`key${i}`);
223
- }
224
- }
225
-
226
- let found = 0;
227
- for (let i = 0; i < 100; i++) {
228
- if (cache.get(`key${i}`) !== null) found++;
229
- }
230
-
231
- assert.strictEqual(found, 50);
232
- });
233
- });
234
- });
@@ -1,44 +0,0 @@
1
- const test = require('node:test');
2
- const assert = require('node:assert');
3
- const { loadConfig, defaults } = require('../src/config');
4
- const fs = require('fs');
5
- const path = require('path');
6
-
7
- test('Config: loading', async (t) => {
8
- await t.test('should load default values', () => {
9
- const config = loadConfig({ ytdlpPath: 'yt-dlp', ffmpegPath: 'ffmpeg' }); // Provide paths to avoid check failure
10
- assert.strictEqual(config.port, defaults.port);
11
- assert.strictEqual(config.audio.bitrate, '128k');
12
- });
13
-
14
- await t.test('should respect environment variables', () => {
15
- process.env.PORT = '9999';
16
- process.env.SPOTIFY_CLIENT_ID = 'test-id';
17
-
18
- const config = loadConfig({ ytdlpPath: 'yt-dlp', ffmpegPath: 'ffmpeg' });
19
- assert.strictEqual(config.port, 9999);
20
- assert.strictEqual(config.spotify.clientId, 'test-id');
21
-
22
- // Cleanup
23
- delete process.env.PORT;
24
- delete process.env.SPOTIFY_CLIENT_ID;
25
- });
26
-
27
- await t.test('should load from custom config file', () => {
28
- const configPath = path.resolve(__dirname, 'temp-config.json');
29
- const customConfig = {
30
- port: 1234,
31
- audio: { bitrate: '320k' }
32
- };
33
-
34
- fs.writeFileSync(configPath, JSON.stringify(customConfig));
35
-
36
- try {
37
- const config = loadConfig({ configPath, ytdlpPath: 'yt-dlp', ffmpegPath: 'ffmpeg' });
38
- assert.strictEqual(config.port, 1234);
39
- assert.strictEqual(config.audio.bitrate, '320k');
40
- } finally {
41
- if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
42
- }
43
- });
44
- });