@stella_project/stellalib 1.0.2 → 1.1.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/README.md CHANGED
@@ -1,44 +1,151 @@
1
- # StellaLib
2
-
3
- A powerful, modern Lavalink client library for TypeScript/JavaScript with auto version detection (v3 + v4), session persistence, smart autoplay, and graceful shutdown.
4
-
5
- [![npm version](https://img.shields.io/npm/v/stellalib.svg)](https://www.npmjs.com/package/stellalib)
6
- [![License: OSL-3.0](https://img.shields.io/badge/License-OSL--3.0-blue.svg)](LICENSE)
7
-
8
- ## Features
9
-
10
- - **Lavalink v3 + v4** — Auto-detects server version and adapts protocol automatically
11
- - **Session Persistence** — Save/restore session IDs across bot restarts with `FileSessionStore`
12
- - **Smart Autoplay** — Auto-mix engine with transition scoring, multi-seed recommendations, and history tracking
13
- - **Graceful Shutdown** — Persist sessions, close nodes cleanly, and flush stores on SIGINT/SIGTERM
14
- - **Voice Readiness** — Promise-based voice connection waiting before playback
15
- - **Audio Filters** — Built-in presets: bassboost, nightcore, vaporwave, 8D, slowmo, and more
16
- - **Search Caching** — LRU cache with TTL for search results to reduce API calls
17
- - **Search Fallback** — Automatic fallback across platforms (Spotify → SoundCloud → YouTube)
18
- - **Node Selection** — Penalty-based, least-load, least-players, or priority-based node selection
19
- - **Heartbeat** — WebSocket ping/pong to detect dead connections and auto-reconnect
20
- - **REST Resilience** — Auto-retry on 429 rate limits, GET deduplication, request timeouts
21
- - **Reconnect** — Exponential backoff with jitter to prevent thundering herd
22
- - **Plugin System** — Extensible via plugins
23
- - **Typed Events** — Fully typed event emitter for all manager events
24
- - **Strict TypeScript** — Written with strict TypeScript
1
+ <p align="center">
2
+ <h1 align="center">StellaLib</h1>
3
+ <p align="center">A powerful Lavalink v3 + v4 client for TypeScript with auto version detection, session persistence, smart autoplay, and graceful shutdown.</p>
4
+ </p>
5
+
6
+ <p align="center">
7
+ <a href="https://www.npmjs.com/package/@stella_project/stellalib"><img src="https://img.shields.io/npm/v/@stella_project/stellalib.svg?style=flat-square&color=blue" alt="npm version" /></a>
8
+ <a href="https://www.npmjs.com/package/@stella_project/stellalib"><img src="https://img.shields.io/npm/dm/@stella_project/stellalib.svg?style=flat-square" alt="npm downloads" /></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-OSL--3.0-blue.svg?style=flat-square" alt="License" /></a>
10
+ <a href="https://github.com/Roki-Stella-Projects/StellaLib"><img src="https://img.shields.io/github/stars/Roki-Stella-Projects/StellaLib?style=flat-square" alt="GitHub stars" /></a>
11
+ </p>
12
+
13
+ ---
14
+
15
+ ## Table of Contents
16
+
17
+ - [What is StellaLib?](#what-is-stellalib)
18
+ - [How it Works](#how-it-works)
19
+ - [Architecture](#architecture)
20
+ - [Installation](#installation)
21
+ - [Quick Start](#quick-start)
22
+ - [Core Concepts](#core-concepts)
23
+ - [Manager](#manager)
24
+ - [Node](#node)
25
+ - [Player](#player)
26
+ - [Queue](#queue)
27
+ - [Rest](#rest)
28
+ - [Filters](#filters)
29
+ - [Multi-Version Support (v3 + v4)](#multi-version-support-v3--v4)
30
+ - [Session Persistence](#session-persistence)
31
+ - [Player State Persistence](#player-state-persistence)
32
+ - [Auto-Failover](#auto-failover)
33
+ - [Inactivity Timeout](#inactivity-timeout)
34
+ - [Queue Limits & Deduplication](#queue-limits--deduplication)
35
+ - [Node Health Monitoring](#node-health-monitoring)
36
+ - [Smart Autoplay](#smart-autoplay)
37
+ - [Search with Fallback](#search-with-fallback)
38
+ - [Audio Filters](#audio-filters)
39
+ - [Events Reference](#events-reference)
40
+ - [Configuration Reference](#configuration-reference)
41
+ - [Requirements](#requirements)
42
+ - [Documentation](#documentation)
43
+ - [Changelog](#changelog)
44
+ - [License](#license)
45
+
46
+ ---
47
+
48
+ ## What is StellaLib?
49
+
50
+ **StellaLib** is a TypeScript client library that connects your Discord bot to [Lavalink](https://github.com/lavalink-devs/Lavalink) — a standalone audio server that handles music playback, search, and streaming. StellaLib manages the entire lifecycle: connecting to Lavalink nodes, creating guild-level players, searching tracks, controlling playback, and handling events.
51
+
52
+ Unlike other Lavalink clients, StellaLib:
53
+
54
+ - **Auto-detects** whether your Lavalink server is v3 or v4 and adapts automatically
55
+ - **Persists sessions and player state** across bot restarts so music keeps playing — autoplay, queue, filters, and history all survive
56
+ - **Has a smart autoplay engine** that picks the best next track based on listening history
57
+ - **Auto-failover** — when a node dies, players move to healthy nodes automatically
58
+ - **Proactive health monitoring** — detects overloaded nodes and migrates players *before* they crash
59
+ - **Handles failures gracefully** with fast first reconnect, exponential backoff, rate limit retries, and search fallback
60
+
61
+ ## How it Works
62
+
63
+ ```
64
+ ┌──────────────┐ raw voice events ┌──────────────────┐ WebSocket/REST ┌──────────┐
65
+ │ Discord.js │ ──────────────────────► │ StellaManager │ ◄──────────────────► │ Lavalink │
66
+ │ (your bot) │ ◄────── send payloads ── │ │ │ Server │
67
+ └──────────────┘ └──────────────────┘ └──────────┘
68
+
69
+ ┌─────────────┼─────────────┐
70
+ ▼ ▼ ▼
71
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
72
+ │ Node 1 │ │ Node 2 │ │ Node N │
73
+ │ (v4 auto)│ │ (v3 auto)│ │ │
74
+ └──────────┘ └──────────┘ └──────────┘
75
+
76
+ ┌─────┼─────┐
77
+ ▼ ▼ ▼
78
+ ┌────────┐ ┌────────┐
79
+ │Player A│ │Player B│ (one per guild)
80
+ │ Queue │ │ Queue │
81
+ │Filters │ │Filters │
82
+ └────────┘ └────────┘
83
+ ```
84
+
85
+ **Flow:**
86
+
87
+ 1. Your bot receives raw Discord voice events and forwards them to `StellaManager`
88
+ 2. The Manager routes voice data to the correct `Node` (Lavalink server connection)
89
+ 3. Each Node auto-detects its Lavalink version (v3 or v4) and adapts its protocol
90
+ 4. `Player` instances (one per guild) handle playback, queue, volume, and filters
91
+ 5. The Node's `Rest` client handles track loading, player updates, and session management
92
+ 6. Events flow back from Lavalink → Node → Manager → your bot's event handlers
93
+
94
+ ## Architecture
95
+
96
+ StellaLib is composed of several core classes that work together:
97
+
98
+ | Class | What it does |
99
+ |---|---|
100
+ | **`StellaManager`** | The entry point. Manages all nodes and players, handles search, voice state updates, caching, and shutdown. You create one Manager per bot. |
101
+ | **`StellaNode`** | Represents a connection to a single Lavalink server. Handles WebSocket connection, heartbeat, reconnect, version detection, session resume, and autoplay logic. |
102
+ | **`StellaPlayer`** | One player per Discord guild. Controls playback (`play`, `pause`, `stop`, `seek`), manages the queue, applies filters, and handles voice readiness. |
103
+ | **`StellaQueue`** | Extends `Array` with music-specific methods: `add()`, `remove()`, `clear()`, `shuffle()`, repeat modes, and `current`/`previous` track tracking. |
104
+ | **`StellaRest`** | HTTP client for Lavalink's REST API. Version-aware (v3 vs v4 endpoints), with rate limit retry, request deduplication, and timeout handling. |
105
+ | **`StellaFilters`** | Manages audio filters and equalizer settings per player. Built-in presets for common effects. |
106
+ | **`LRUCache`** | Bounded least-recently-used cache with TTL expiry for search results. Reduces redundant API calls. |
107
+ | **`FileSessionStore`** | Persists Lavalink session IDs **and full player states** to a JSON file. Enables seamless resume after bot restarts — including autoplay, queue, and filters. |
108
+
109
+ ### Project Structure
110
+
111
+ ```
112
+ src/
113
+ Structures/
114
+ Manager.ts — Main hub: nodes, players, search, voice, cache, shutdown
115
+ Node.ts — Lavalink node: WS, heartbeat, reconnect, version detect, autoplay
116
+ Player.ts — Guild player: playback, queue, voice ready, filters, move node
117
+ Queue.ts — Queue: add/remove/shuffle, repeat modes, current/previous
118
+ Rest.ts — REST client: version-aware endpoints, retry, dedup, timeout
119
+ Filters.ts — Audio filter management and presets
120
+ LRUCache.ts — Bounded LRU cache with TTL and memory estimation
121
+ SessionStore.ts — FileSessionStore for session persistence
122
+ Types.ts — All TypeScript interfaces, types, and event definitions
123
+ Utils.ts — TrackUtils (build/validate tracks), Structure, Plugin system
124
+ Utils/
125
+ FiltersEqualizers.ts — Equalizer band presets for each filter
126
+ ManagerCheck.ts — Manager option validation
127
+ NodeCheck.ts — Node option validation
128
+ PlayerCheck.ts — Player option validation
129
+ index.ts — Re-exports everything
130
+ ```
25
131
 
26
132
  ## Installation
27
133
 
28
134
  ```bash
29
- npm install stellalib
135
+ npm install @stella_project/stellalib
30
136
  # or
31
- yarn add stellalib
137
+ yarn add @stella_project/stellalib
32
138
  # or
33
- bun add stellalib
139
+ bun add @stella_project/stellalib
34
140
  ```
35
141
 
36
142
  ## Quick Start
37
143
 
38
144
  ```ts
39
145
  import { Client, GatewayIntentBits } from "discord.js";
40
- import { StellaManager, FileSessionStore } from "stellalib";
146
+ import { StellaManager, FileSessionStore } from "@stella_project/stellalib";
41
147
 
148
+ // 1. Create your Discord client
42
149
  const client = new Client({
43
150
  intents: [
44
151
  GatewayIntentBits.Guilds,
@@ -48,38 +155,73 @@ const client = new Client({
48
155
  ],
49
156
  });
50
157
 
158
+ // 2. Create the StellaLib manager
51
159
  const manager = new StellaManager({
52
160
  nodes: [
53
161
  {
54
- identifier: "main",
55
- host: "localhost",
56
- port: 2333,
57
- password: "youshallnotpass",
58
- resumeStatus: true,
59
- resumeTimeout: 120,
60
- heartbeatInterval: 30000,
162
+ identifier: "main", // Unique name for this node
163
+ host: "localhost", // Lavalink server host
164
+ port: 2333, // Lavalink server port
165
+ password: "youshallnotpass",// Lavalink password
166
+ resumeStatus: true, // Enable session resuming
167
+ resumeTimeout: 120, // Seconds Lavalink waits for reconnect
168
+ heartbeatInterval: 30000, // Ping interval in ms
61
169
  },
62
170
  ],
63
- autoPlay: true,
64
- defaultSearchPlatform: "spotify",
65
- searchFallback: ["soundcloud", "youtube music", "youtube"],
66
- sessionStore: new FileSessionStore("./sessions.json"),
67
- caches: { enabled: true, time: 60000, maxSize: 200 },
171
+ autoPlay: true, // Enable autoplay when queue ends
172
+ defaultSearchPlatform: "spotify", // Default search source
173
+ searchFallback: ["soundcloud", "youtube"], // Fallback if primary fails
174
+ sessionStore: new FileSessionStore("./sessions.json"), // Persist sessions
175
+ caches: { enabled: true, time: 60000, maxSize: 200 }, // Search cache
68
176
  send(id, payload) {
177
+ // Required: how to send voice payloads to Discord
69
178
  const guild = client.guilds.cache.get(id);
70
179
  if (guild) guild.shard.send(payload);
71
180
  },
72
181
  });
73
182
 
74
- // Forward raw Discord events to StellaLib
183
+ // 3. Forward raw Discord events to StellaLib (required for voice)
75
184
  client.on("raw", (d) => manager.updateVoiceState(d));
76
185
 
186
+ // 4. Initialize manager when bot is ready
77
187
  client.on("ready", () => {
78
188
  console.log(`Bot ready as ${client.user?.tag}`);
79
189
  manager.init(client.user!.id);
80
190
  });
81
191
 
82
- // Graceful shutdown
192
+ // 5. Handle events
193
+ manager.on("NodeConnect", (node) => {
194
+ console.log(`Connected to ${node.options.identifier} (Lavalink v${node.version})`);
195
+ });
196
+
197
+ manager.on("TrackStart", (player, track) => {
198
+ console.log(`Now playing: ${track.title}`);
199
+ });
200
+
201
+ // 6. Play music (example in a command handler)
202
+ async function play(guildId: string, voiceChannelId: string, query: string) {
203
+ // Create or get player
204
+ let player = manager.players.get(guildId);
205
+ if (!player) {
206
+ player = manager.create({
207
+ guild: guildId,
208
+ voiceChannel: voiceChannelId,
209
+ textChannel: "TEXT_CHANNEL_ID",
210
+ volume: 50,
211
+ selfDeafen: true,
212
+ });
213
+ player.connect();
214
+ }
215
+
216
+ // Search and queue
217
+ const res = await manager.search(query, "USER_ID");
218
+ if (res.tracks.length) {
219
+ player.queue.add(res.tracks[0]);
220
+ if (!player.playing) player.play();
221
+ }
222
+ }
223
+
224
+ // 7. Graceful shutdown
83
225
  for (const sig of ["SIGINT", "SIGTERM"]) {
84
226
  process.on(sig, async () => {
85
227
  await manager.shutdown();
@@ -87,184 +229,561 @@ for (const sig of ["SIGINT", "SIGTERM"]) {
87
229
  });
88
230
  }
89
231
 
90
- // Play a track
91
- manager.on("NodeConnect", async () => {
92
- const player = manager.create({
93
- guild: "GUILD_ID",
94
- voiceChannel: "VOICE_CHANNEL_ID",
95
- textChannel: "TEXT_CHANNEL_ID",
96
- });
97
- player.connect();
232
+ client.login("YOUR_BOT_TOKEN");
233
+ ```
98
234
 
99
- const res = await manager.search("never gonna give you up");
100
- if (res.tracks.length) {
101
- player.queue.add(res.tracks[0]);
102
- player.play();
103
- }
235
+ ## Core Concepts
236
+
237
+ ### Manager
238
+
239
+ `StellaManager` is the central hub. You create **one instance** and it manages everything.
240
+
241
+ ```ts
242
+ const manager = new StellaManager({
243
+ nodes: [...], // Array of Lavalink node configs
244
+ send: (id, payload) => { ... }, // How to send to Discord gateway
245
+ autoPlay: true, // Auto-play next track when queue ends
246
+ defaultSearchPlatform: "spotify",
247
+ searchFallback: ["soundcloud", "youtube music"],
248
+ sessionStore: new FileSessionStore("./sessions.json"),
249
+ caches: { enabled: true, time: 60000, maxSize: 200 },
250
+ clientName: "StellaLib",
251
+ shards: 1,
104
252
  });
105
253
 
106
- client.login("YOUR_BOT_TOKEN");
254
+ // Initialize after Discord client is ready
255
+ manager.init(client.user!.id);
256
+ ```
257
+
258
+ **Key methods:**
259
+ - `manager.init(clientId)` — Connect all nodes
260
+ - `manager.create(options)` — Create a player for a guild
261
+ - `manager.get(guildId)` — Get existing player
262
+ - `manager.search(query, requester?)` — Search tracks with fallback
263
+ - `manager.updateVoiceState(data)` — Forward raw Discord voice events
264
+ - `manager.shutdown()` — Gracefully close everything
265
+ - `manager.getStats()` — Get node/player/cache statistics
266
+
267
+ ### Node
268
+
269
+ `StellaNode` represents a single Lavalink server connection. Nodes are created automatically from the `nodes` config.
270
+
271
+ **What it does automatically:**
272
+ - Detects Lavalink version (v3 or v4) before connecting
273
+ - Establishes WebSocket with the correct URL and headers
274
+ - Sends heartbeat pings to detect dead connections
275
+ - Reconnects with exponential backoff + jitter on disconnect
276
+ - Configures session resuming (v3: WS op, v4: REST PATCH)
277
+ - Syncs player state after resume (v4 only)
278
+ - Handles autoplay logic when queue ends
279
+
280
+ **Properties:**
281
+ - `node.version` — Detected Lavalink version (`3` or `4`)
282
+ - `node.connected` — Whether WebSocket is open
283
+ - `node.stats` — CPU, memory, players, uptime stats
284
+ - `node.info` — Cached Lavalink server info (plugins, sources)
285
+ - `node.penalties` — Calculated penalty score for load balancing
286
+
287
+ ### Player
288
+
289
+ `StellaPlayer` controls playback for **one Discord guild**. Created via `manager.create()`.
290
+
291
+ ```ts
292
+ const player = manager.create({
293
+ guild: "GUILD_ID",
294
+ voiceChannel: "VOICE_CHANNEL_ID",
295
+ textChannel: "TEXT_CHANNEL_ID",
296
+ volume: 50,
297
+ selfDeafen: true,
298
+ });
299
+
300
+ player.connect(); // Join voice channel
301
+ player.play(); // Play first track in queue
302
+ player.pause(true); // Pause
303
+ player.pause(false); // Resume
304
+ player.stop(); // Stop current track (plays next)
305
+ player.seek(30000); // Seek to 30 seconds
306
+ player.setVolume(80); // Set volume (0-100)
307
+ player.setTrackRepeat(true); // Repeat current track
308
+ player.setQueueRepeat(true); // Repeat entire queue
309
+ player.setAutoplay(true, botUser); // Enable smart autoplay
310
+ player.moveNode("other-node"); // Move to another Lavalink node
311
+ player.destroy(); // Leave channel and clean up
312
+ ```
313
+
314
+ ### Queue
315
+
316
+ `StellaQueue` extends JavaScript's `Array` with music-specific helpers:
317
+
318
+ ```ts
319
+ player.queue.add(track); // Add track(s) to end
320
+ player.queue.add([track1, track2]); // Add multiple
321
+ player.queue.remove(0); // Remove by index
322
+ player.queue.clear(); // Clear all queued tracks
323
+ player.queue.shuffle(); // Randomize order
324
+ player.queue.current; // Currently playing track
325
+ player.queue.previous; // Previously played track
326
+ player.queue.totalSize; // current + queued count
327
+ player.queue.size; // Queued count (excluding current)
328
+ ```
329
+
330
+ ### Rest
331
+
332
+ `StellaRest` handles all HTTP communication with Lavalink. It's version-aware — the same method call works for both v3 and v4.
333
+
334
+ | Method | v3 behavior | v4 behavior |
335
+ |---|---|---|
336
+ | `loadTracks(id)` | `GET /loadtracks` → normalized | `GET /v4/loadtracks` |
337
+ | `updatePlayer(opts)` | WS ops (`play`, `pause`, etc.) | `PATCH /v4/sessions/.../players/...` |
338
+ | `destroyPlayer(id)` | WS `destroy` op | `DELETE /v4/sessions/.../players/...` |
339
+ | `configureResume(t)` | WS `configureResuming` op | `PATCH /v4/sessions/...` |
340
+ | `getInfo()` | `GET /version` | `GET /v4/info` |
341
+ | `decodeTracks(arr)` | `POST /decodetracks` | `POST /v4/decodetracks` |
342
+
343
+ **Built-in resilience:**
344
+ - Auto-retry on 429 rate limits (up to 3 retries with `Retry-After`)
345
+ - GET request deduplication (concurrent identical GETs share one request)
346
+ - Configurable request timeout
347
+ - Request/failure counters
348
+
349
+ ### Filters
350
+
351
+ Built-in audio filter presets:
352
+
353
+ ```ts
354
+ await player.filters.setFilter("bassboost", true);
355
+ await player.filters.setFilter("nightcore", true);
356
+ await player.filters.setFilter("vaporwave", true);
357
+ await player.filters.setFilter("eightD", true);
358
+ await player.filters.setFilter("slowmo", true);
359
+ await player.filters.setFilter("soft", true);
360
+ await player.filters.setFilter("trebleBass", true);
361
+ await player.filters.setFilter("tv", true);
362
+ await player.filters.setFilter("distort", true);
363
+
364
+ await player.filters.clearFilters(); // Remove all
365
+ ```
366
+
367
+ Each preset applies specific equalizer bands, timescale, rotation, or other Lavalink audio parameters.
368
+
369
+ ## Multi-Version Support (v3 + v4)
370
+
371
+ StellaLib **automatically detects** your Lavalink server version before connecting. No configuration needed.
372
+
373
+ **How detection works:**
374
+ 1. Before WebSocket connect, the Node sends `GET /v4/info` to the server
375
+ 2. If it responds `200 OK` → **Lavalink v4** detected (server info is cached)
376
+ 3. If it fails, tries `GET /version` → **Lavalink v3** detected
377
+ 4. Falls back to v4 if both fail
378
+
379
+ **What adapts automatically:**
380
+
381
+ | Aspect | Lavalink v3 | Lavalink v4 |
382
+ |---|---|---|
383
+ | **WebSocket URL** | `ws://host:port` | `ws://host:port/v4/websocket` |
384
+ | **Player control** | WebSocket ops (`play`, `stop`, `pause`, `seek`, `volume`, `filters`) | REST `PATCH` |
385
+ | **Session resume** | `Resume-Key` header + WS `configureResuming` | `Session-Id` header + REST `PATCH` |
386
+ | **Track loading** | `/loadtracks` (response normalized to v4 format) | `/v4/loadtracks` |
387
+ | **Server info** | `/version` (returns version string) | `/v4/info` (returns full info JSON) |
388
+ | **Player sync** | Not available (v3 limitation) | Full player state sync on resume |
389
+ | **Track data** | `track` field → mapped to `encoded` | `encoded` field |
390
+ | **Load types** | `TRACK_LOADED` → `track`, `SEARCH_RESULT` → `search`, etc. | Already v4 format |
391
+
392
+ ```ts
393
+ manager.on("NodeConnect", (node) => {
394
+ console.log(`Lavalink v${node.version}`); // 3 or 4
395
+ });
107
396
  ```
108
397
 
109
398
  ## Session Persistence
110
399
 
111
- StellaLib can persist Lavalink session IDs across bot restarts, so players keep playing without interruption:
400
+ StellaLib persists session IDs so music **keeps playing** after bot restarts.
112
401
 
113
402
  ```ts
114
- import { FileSessionStore } from "stellalib";
403
+ import { FileSessionStore } from "@stella_project/stellalib";
115
404
 
116
405
  const manager = new StellaManager({
117
406
  sessionStore: new FileSessionStore("./sessions.json"),
118
407
  nodes: [{
119
- resumeStatus: true,
120
- resumeTimeout: 120, // seconds Lavalink waits for reconnect
408
+ resumeStatus: true, // Tell Lavalink to hold the session
409
+ resumeTimeout: 120, // Seconds to wait before destroying session
121
410
  // ...
122
411
  }],
123
412
  // ...
124
413
  });
125
-
126
- // On shutdown, sessions are saved automatically
127
- await manager.shutdown();
128
414
  ```
129
415
 
130
- You can also implement your own store (e.g., Redis) by implementing the `SessionStore` interface:
416
+ **How it works:**
417
+ 1. On connect, Node loads saved session ID from the store
418
+ 2. Sends it as `Session-Id` (v4) or `Resume-Key` (v3) header
419
+ 3. Lavalink resumes the session — players keep their state
420
+ 4. On disconnect/shutdown, session ID **and full player state** is persisted to the store
421
+ 5. On resume, autoplay state, queue, filters, history, and seed pool are all restored
422
+
423
+ **Custom stores** (e.g., Redis, database):
131
424
 
132
425
  ```ts
133
- interface SessionStore {
134
- get(nodeId: string): Promise<string | null> | string | null;
135
- set(nodeId: string, sessionId: string): Promise<void> | void;
136
- delete(nodeId: string): Promise<void> | void;
137
- }
426
+ const manager = new StellaManager({
427
+ sessionStore: {
428
+ async get(nodeId) { return await redis.get(`session:${nodeId}`); },
429
+ async set(nodeId, sessionId) { await redis.set(`session:${nodeId}`, sessionId); },
430
+ async delete(nodeId) { await redis.del(`session:${nodeId}`); },
431
+ },
432
+ // ...
433
+ });
138
434
  ```
139
435
 
140
- ## Smart Autoplay
436
+ ## Player State Persistence
141
437
 
142
- When the queue ends and autoplay is enabled, StellaLib's auto-mix engine finds the best next track:
438
+ StellaLib v1.1.0+ persists **full player state** — not just session IDs — across bot restarts. This means autoplay, queue, filters, repeat modes, and listening history all survive a restart.
143
439
 
144
440
  ```ts
145
- // Enable autoplay for a player
146
- player.setAutoplay(true, { id: user.id, tag: user.tag });
147
- ```
441
+ import { FileSessionStore } from "@stella_project/stellalib";
148
442
 
149
- The engine scores candidates based on:
150
- - **Duration similarity** Prefer tracks close in length to recent plays
151
- - **Author/title overlap** — Prioritize same artist or related keywords
152
- - **Source consistency** Stay on the same platform when possible
153
- - **Diversity** Avoid repeating the same artist too many times
154
- - **History tracking** — Never replay recently heard tracks (last 50)
443
+ // FileSessionStore automatically handles both session IDs and player states
444
+ const manager = new StellaManager({
445
+ sessionStore: new FileSessionStore("./sessions.json"),
446
+ // Player state store is auto-detected from FileSessionStore
447
+ // Or provide a custom one:
448
+ // playerStateStore: myCustomStore,
449
+ // ...
450
+ });
451
+ ```
155
452
 
156
- Uses multi-seed context from the last 5 tracks for Spotify recommendations, theme keyword extraction, and cross-artist transitions.
453
+ **What is persisted per player:**
454
+ - Autoplay on/off state and bot user ID
455
+ - Autoplay history (last 50 tracks) and seed pool
456
+ - Queue (all tracks with encoded data)
457
+ - Filter configuration and active preset flags
458
+ - Repeat modes (track, queue, dynamic)
459
+ - Volume, voice channel, text channel
157
460
 
158
- ## Search with Fallback
461
+ **Custom player state store** (e.g., Redis):
159
462
 
160
463
  ```ts
161
464
  const manager = new StellaManager({
162
- defaultSearchPlatform: "spotify",
163
- searchFallback: ["soundcloud", "youtube music", "youtube"],
465
+ playerStateStore: {
466
+ async getPlayerState(guildId) { return JSON.parse(await redis.get(`player:${guildId}`)); },
467
+ async setPlayerState(guildId, state) { await redis.set(`player:${guildId}`, JSON.stringify(state)); },
468
+ async deletePlayerState(guildId) { await redis.del(`player:${guildId}`); },
469
+ async getAllPlayerStates() { /* return all states */ },
470
+ },
164
471
  // ...
165
472
  });
473
+ ```
474
+
475
+ ## Auto-Failover
476
+
477
+ When a Lavalink node goes down **mid-playback**, StellaLib **immediately** moves all playing/paused players to a healthy node — audio continues at the exact same position with typically <150ms gap:
166
478
 
167
- // If Spotify returns empty, automatically tries SoundCloud, then YouTube Music, then YouTube
168
- const result = await manager.search("natori セレナーデ");
479
+ ```
480
+ Node A crashes! 💥
481
+
482
+ t=0ms WebSocket close fires
483
+ t=2ms attemptSeamlessFailover() starts
484
+ t=5ms Healthy nodes sorted by penalty score
485
+ t=50ms Voice state sent to Node B
486
+ t=100ms Track + position + filters sent
487
+ t=150ms Audio resumes on Node B ♪
169
488
  ```
170
489
 
171
- ## Audio Filters
490
+ ```
491
+ Node A (dies) Node B (healthy) Node C (healthy)
492
+ Player 1 ──────────────► Player 1 ♪
493
+ Player 2 ──────────────► Player 2 ♪ (load balanced)
494
+ Player 3 ────────────────────────────────────► Player 3 ♪
495
+ ```
496
+
497
+ ### Three Layers of Protection
498
+
499
+ | Layer | Trigger | Speed |
500
+ |---|---|---|
501
+ | **Seamless failover** | Node unexpectedly disconnects | Immediate (<150ms) |
502
+ | **Health monitoring** | CPU/frame deficit exceeds threshold | Proactive (before crash) |
503
+ | **Destroy failover** | Node explicitly removed from pool | Immediate |
504
+
505
+ ### PlayerFailover Event
172
506
 
173
507
  ```ts
174
- // Toggle filters
175
- await player.filters.setFilter("nightcore", true);
176
- await player.filters.setFilter("bassboost", true);
177
- await player.filters.setFilter("vaporwave", true);
178
- await player.filters.setFilter("eightD", true);
508
+ manager.on("PlayerFailover", (player, oldNode, newNode) => {
509
+ console.log(`Player ${player.guild} moved: ${oldNode} → ${newNode}`);
510
+ // Optionally notify the guild
511
+ });
512
+ ```
513
+
514
+ - Players are distributed across healthy nodes by **penalty score** (not all dumped on one node)
515
+ - If no healthy nodes exist, players wait for reconnect (fast 2s retry on first attempt)
516
+ - See [docs/13-seamless-failover.md](docs/13-seamless-failover.md) for full architecture details
517
+
518
+ ## Inactivity Timeout
519
+
520
+ Auto-disconnect the bot when it's alone in a voice channel:
521
+
522
+ ```ts
523
+ const player = manager.create({
524
+ guild: guildId,
525
+ voiceChannel: voiceChannelId,
526
+ inactivityTimeout: 300000, // 5 minutes
527
+ });
179
528
 
180
- // Clear all filters
181
- await player.filters.clearFilters();
529
+ // In your voiceStateUpdate handler:
530
+ client.on("voiceStateUpdate", (oldState, newState) => {
531
+ const player = manager.get(oldState.guild.id);
532
+ if (!player) return;
533
+
534
+ const channel = oldState.guild.channels.cache.get(player.voiceChannel!);
535
+ const members = channel?.members?.filter((m) => !m.user.bot).size ?? 0;
536
+
537
+ if (members === 0) {
538
+ player.startInactivityTimer(); // Start countdown
539
+ } else {
540
+ player.stopInactivityTimer(); // Cancel — someone joined
541
+ }
542
+ });
182
543
  ```
183
544
 
184
- Available: `bassboost`, `nightcore`, `vaporwave`, `eightD`, `slowmo`, `soft`, `trebleBass`, `tv`, `distort`
545
+ ## Queue Limits & Deduplication
546
+
547
+ ### Max Queue Size
185
548
 
186
- ## Manager Stats
549
+ Prevent memory abuse by limiting the queue:
187
550
 
188
551
  ```ts
189
- const stats = manager.getStats();
190
- // { nodes, players, playingPlayers, cacheSize, cacheMemoryEstimate }
552
+ const player = manager.create({
553
+ guild: guildId,
554
+ voiceChannel: voiceChannelId,
555
+ maxQueueSize: 500, // Max 500 tracks in queue
556
+ });
557
+
558
+ // Check before adding
559
+ if (!player.canAddToQueue(tracks.length)) {
560
+ return message.reply(`Queue is full! Only ${player.queueSpaceRemaining} slots left.`);
561
+ }
562
+ player.queue.add(tracks); // Excess tracks are automatically truncated
191
563
  ```
192
564
 
193
- ## Events
565
+ ### Track Deduplication
566
+
567
+ Prevent the same song from being queued twice:
194
568
 
195
569
  ```ts
196
- manager.on("NodeConnect", (node) => { });
197
- manager.on("NodeDisconnect", (node, reason) => { });
198
- manager.on("NodeError", (node, error) => { });
199
- manager.on("NodeReconnect", (node) => { });
200
- manager.on("NodeRaw", (payload) => { });
201
- manager.on("TrackStart", (player, track, payload) => { });
202
- manager.on("TrackEnd", (player, track, payload) => { });
203
- manager.on("TrackStuck", (player, track, payload) => { });
204
- manager.on("TrackError", (player, track, payload) => { });
205
- manager.on("QueueEnd", (player, track, payload) => { });
206
- manager.on("SocketClosed", (player, payload) => { });
207
- manager.on("PlayerCreate", (player) => { });
208
- manager.on("PlayerDestroy", (player) => { });
209
- manager.on("PlayerMove", (player, oldChannel, newChannel) => { });
210
- manager.on("PlayerDisconnect", (player, oldChannel) => { });
211
- manager.on("PlayerStateUpdate", (oldPlayer, newPlayer) => { });
212
- manager.on("Debug", (message) => { });
570
+ player.queue.noDuplicates = true;
571
+
572
+ // Now queue.add() silently skips tracks that are already queued
573
+ player.queue.add(track); // Added
574
+ player.queue.add(track); // Silently skipped (same URI)
575
+
576
+ // Check manually:
577
+ if (player.queue.isDuplicate(track)) {
578
+ return message.reply("That track is already in the queue!");
579
+ }
213
580
  ```
214
581
 
215
- ## Project Structure
582
+ ## Node Health Monitoring
583
+
584
+ StellaLib can proactively monitor node health and migrate players **before** a node crashes:
216
585
 
586
+ ```ts
587
+ const manager = new StellaManager({
588
+ nodeHealthThresholds: {
589
+ maxCpuLoad: 0.85, // Migrate when CPU exceeds 85%
590
+ maxFrameDeficit: 300, // Migrate when frame deficit exceeds 300
591
+ checkInterval: 30000, // Check every 30 seconds
592
+ },
593
+ // ...
594
+ });
217
595
  ```
218
- src/
219
- Structures/
220
- Manager.ts — Main hub: manages nodes, players, search, voice updates
221
- Node.ts — Lavalink node: WebSocket, reconnect, session resume, autoplay
222
- Player.ts — Guild player: playback, queue, voice readiness, filters
223
- Queue.ts — Queue: extends Array with add/remove/shuffle
224
- Rest.ts — REST client with retry, timeout, and deduplication
225
- Filters.ts — Audio filters and presets
226
- LRUCache.ts — Bounded LRU cache with TTL
227
- SessionStore.ts — FileSessionStore for session persistence
228
- Types.ts — All TypeScript interfaces and types
229
- Utils.ts — TrackUtils, Structure, Plugin helpers
230
- Utils/
231
- FiltersEqualizers.ts — Equalizer band presets
232
- ManagerCheck.ts — Manager option validation
233
- NodeCheck.ts — Node option validation
234
- PlayerCheck.ts — Player option validation
235
- index.ts — Re-exports everything
596
+
597
+ ```
598
+ Health Check (every 30s)
599
+
600
+ Node A: CPU 92% ──► OVERLOADED
601
+ Node B: CPU 40% ──► healthy
602
+
603
+ Migrate players A B (preemptive)
236
604
  ```
237
605
 
238
- ## Multi-Version Support
606
+ This is **proactive** failover — it moves players before they experience audio issues, unlike the reactive auto-failover which only triggers when a node dies.
607
+
608
+ ## Smart Autoplay
239
609
 
240
- StellaLib auto-detects your Lavalink server version on connect and adapts:
610
+ When the queue ends and autoplay is enabled, StellaLib's auto-mix engine picks the best next track.
241
611
 
242
- | Feature | Lavalink v3 | Lavalink v4 |
243
- |---|---|---|
244
- | **WebSocket URL** | `ws://host:port` | `ws://host:port/v4/websocket` |
245
- | **Player ops** | WebSocket ops (`play`, `stop`, `pause`, etc.) | REST PATCH |
246
- | **Session resume** | `Resume-Key` header + WS `configureResuming` | `Session-Id` header + REST PATCH |
247
- | **Load tracks** | `/loadtracks` (normalized to v4 format) | `/v4/loadtracks` |
248
- | **Server info** | `/version` | `/v4/info` |
249
- | **Player sync** | Not available | Full player state sync |
612
+ ```ts
613
+ player.setAutoplay(true, client.user);
614
+ ```
615
+
616
+ **How the engine works:**
617
+
618
+ 1. **Seed collection** Gathers the last 5 played tracks as context seeds
619
+ 2. **Source detection** Identifies if the listener was on Spotify, YouTube, or SoundCloud
620
+ 3. **Recommendation fetch** — Uses Spotify `sprec:` (seed artists + seed tracks) or YouTube Mix
621
+ 4. **Candidate scoring** — Each candidate is scored on:
622
+ - Duration similarity to recent tracks
623
+ - Author/title keyword overlap
624
+ - Remix/cover penalty (avoids non-originals)
625
+ - History check (never replays last 50 tracks)
626
+ 5. **Best transition** — Picks the highest-scoring candidate
627
+ 6. **Cross-platform mirror** — If needed, re-searches on SoundCloud/YouTube for a streamable version
628
+ 7. **Fallback chain** — If recommendations fail, tries theme-based search, then random from same artist
629
+
630
+ ## Search with Fallback
250
631
 
251
632
  ```ts
252
- // No configuration needed — version is auto-detected
253
633
  const manager = new StellaManager({
254
- nodes: [{ host: "my-v3-server.com", port: 2333 }],
634
+ defaultSearchPlatform: "spotify",
635
+ searchFallback: ["soundcloud", "youtube music", "youtube"],
255
636
  // ...
256
637
  });
257
638
 
258
- // Check detected version after connect
259
- manager.on("NodeConnect", (node) => {
260
- console.log(`Connected to Lavalink v${node.version}`); // 3 or 4
261
- });
639
+ // Searches Spotify first. If empty, tries SoundCloud, then YouTube Music, then YouTube.
640
+ const result = await manager.search("natori セレナーデ", userId);
641
+ ```
642
+
643
+ **Supported platforms:** `spotify`, `soundcloud`, `youtube`, `youtube music`, `deezer`, `tidal`, `applemusic`, `bandcamp`, `jiosaavn`
644
+
645
+ ## Audio Filters
646
+
647
+ | Filter | Effect |
648
+ |---|---|
649
+ | `bassboost` | Boosts low frequencies |
650
+ | `nightcore` | Speeds up + higher pitch |
651
+ | `vaporwave` | Slows down + lower pitch |
652
+ | `eightD` | Rotating stereo panning |
653
+ | `slowmo` | Slower playback speed |
654
+ | `soft` | Reduces harsh frequencies |
655
+ | `trebleBass` | Boosts both high and low bands |
656
+ | `tv` | Tinny speaker simulation |
657
+ | `distort` | Audio distortion effect |
658
+
659
+ ## Events Reference
660
+
661
+ | Event | Parameters | Description |
662
+ |---|---|---|
663
+ | `NodeCreate` | `(node)` | Node instance created |
664
+ | `NodeConnect` | `(node)` | WebSocket connection established |
665
+ | `NodeReconnect` | `(node)` | Attempting reconnection |
666
+ | `NodeDisconnect` | `(node, reason)` | WebSocket disconnected |
667
+ | `NodeDestroy` | `(node)` | Node destroyed |
668
+ | `NodeError` | `(node, error)` | Error on node |
669
+ | `NodeRaw` | `(payload)` | Raw WebSocket message |
670
+ | `TrackStart` | `(player, track, payload)` | Track started playing |
671
+ | `TrackEnd` | `(player, track, payload)` | Track finished |
672
+ | `TrackStuck` | `(player, track, payload)` | Track got stuck |
673
+ | `TrackError` | `(player, track, payload)` | Track playback error |
674
+ | `QueueEnd` | `(player, track, payload)` | Queue finished (all tracks played) |
675
+ | `PlayerCreate` | `(player)` | Player created for a guild |
676
+ | `PlayerDestroy` | `(player)` | Player destroyed |
677
+ | `PlayerMove` | `(player, oldChannel, newChannel)` | Bot moved to different voice channel |
678
+ | `PlayerDisconnect` | `(player, oldChannel)` | Bot disconnected from voice |
679
+ | `PlayerStateUpdate` | `(oldPlayer, newPlayer)` | Player state changed |
680
+ | `PlayerFailover` | `(player, oldNode, newNode)` | Player seamlessly moved to a new node |
681
+ | `SocketClosed` | `(player, payload)` | Discord voice WebSocket closed for player |
682
+ | `Debug` | `(message)` | Debug log message |
683
+
684
+ ## Configuration Reference
685
+
686
+ ### Manager Options
687
+
688
+ ```ts
689
+ interface ManagerOptions {
690
+ nodes: NodeOptions[]; // Lavalink server configs (required)
691
+ send: (id: string, payload: Payload) => void; // Discord gateway send (required)
692
+ clientId?: string; // Bot user ID (set by init())
693
+ clientName?: string; // Client identifier sent to Lavalink
694
+ shards?: number; // Shard count
695
+ autoPlay?: boolean; // Enable autoplay on queue end
696
+ defaultSearchPlatform?: SearchPlatform;// Default search source
697
+ searchFallback?: string[]; // Fallback platforms
698
+ sessionStore?: SessionStore; // Session persistence store
699
+ playerStateStore?: PlayerStateStore; // Full player state persistence (auto-detected from sessionStore)
700
+ nodeHealthThresholds?: { // Proactive node health monitoring
701
+ maxCpuLoad?: number; // Max CPU load (0-1), default: 0.9
702
+ maxFrameDeficit?: number; // Max frame deficit, default: 500
703
+ checkInterval?: number; // Check interval (ms), default: 60000
704
+ };
705
+ caches?: {
706
+ enabled: boolean;
707
+ time: number; // TTL in ms
708
+ maxSize: number; // Max cached entries
709
+ };
710
+ plugins?: Plugin[]; // Custom plugins
711
+ }
712
+ ```
713
+
714
+ ### Node Options
715
+
716
+ ```ts
717
+ interface NodeOptions {
718
+ host: string; // Lavalink host
719
+ port: number; // Lavalink port
720
+ password: string; // Lavalink password
721
+ identifier?: string; // Unique node name
722
+ secure?: boolean; // Use wss:// and https://
723
+ retryAmount?: number; // Max reconnect attempts
724
+ retryDelay?: number; // Base delay between retries (ms)
725
+ requestTimeout?: number; // REST request timeout (ms)
726
+ resumeStatus?: boolean; // Enable session resuming
727
+ resumeTimeout?: number; // Seconds Lavalink holds session
728
+ heartbeatInterval?: number;// WebSocket ping interval (ms)
729
+ }
730
+ ```
731
+
732
+ ### Player Options
733
+
734
+ ```ts
735
+ interface PlayerOptions {
736
+ guild: string; // Guild ID (required)
737
+ voiceChannel?: string; // Voice channel ID
738
+ textChannel?: string; // Text channel ID
739
+ node?: string; // Preferred node identifier
740
+ volume?: number; // Initial volume (default: 11)
741
+ selfMute?: boolean; // Self mute in voice
742
+ selfDeafen?: boolean; // Self deafen in voice
743
+ inactivityTimeout?: number; // Auto-disconnect when alone (ms, 0=disabled)
744
+ maxQueueSize?: number; // Max queue tracks (0=unlimited)
745
+ }
262
746
  ```
263
747
 
264
748
  ## Requirements
265
749
 
266
750
  - **Node.js** >= 18.0.0
267
751
  - **Lavalink** v3.x or v4.x
752
+ - **Discord.js** v14+ (or any library that exposes raw gateway events)
753
+
754
+ ## Documentation
755
+
756
+ For detailed guides and API reference, see the [docs/](docs/) folder:
757
+
758
+ - [Getting Started](docs/01-getting-started.md)
759
+ - [Architecture](docs/02-architecture.md)
760
+ - [Manager](docs/03-manager.md)
761
+ - [Node](docs/04-node.md)
762
+ - [Player](docs/05-player.md)
763
+ - [Queue](docs/06-queue.md)
764
+ - [Events](docs/07-events.md)
765
+ - [Filters](docs/08-filters.md)
766
+ - [Session Persistence](docs/09-session-persistence.md)
767
+ - [Multi-Version Support](docs/10-multi-version.md)
768
+ - [Autoplay Engine](docs/11-autoplay.md)
769
+ - [Player State Persistence](docs/12-player-state-persistence.md)
770
+
771
+ ## Changelog
772
+
773
+ See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes per version.
774
+
775
+ ## Credits
776
+
777
+ StellaLib stands on the shoulders of these amazing projects:
778
+
779
+ | Project | Description | Link |
780
+ |---|---|---|
781
+ | **Lavalink** | The audio server that powers everything | [GitHub](https://github.com/lavalink-devs/Lavalink) · [Website](https://lavalink.dev/) |
782
+ | **LithiumX** | Direct upstream — StellaLib is derived from LithiumX by Anantix Network (MIT) | [GitHub](https://github.com/anantix-network/LithiumX) |
783
+ | **Erela.js** | Pioneered the Lavalink client pattern in the JS ecosystem — many design patterns originated here | [GitHub](https://github.com/MenuDocs/erela.js) |
784
+ | **MagmaStream** | Inspiration for advanced features like improved node management and audio quality | [GitHub](https://github.com/Magmastream-NPM/magmastream) |
785
+
786
+ Thank you to all the maintainers and contributors of these projects for making music bots possible.
268
787
 
269
788
  ## License
270
789