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/docs/quick-start.md
DELETED
|
@@ -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
|
-
```
|
package/docs/sponsorblock.md
DELETED
|
@@ -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
|
package/tests/cache.test.js
DELETED
|
@@ -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
|
-
});
|
package/tests/config.test.js
DELETED
|
@@ -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
|
-
});
|